snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
package compiler
import (
"sort"
analyzer "github.com/sqlc-dev/sqlc/internal/analysis"
"github.com/sqlc-dev/sqlc/internal/config"
"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/preprocess"
"github.com/sqlc-dev/sqlc/internal/sql/validate"
)
type analysis struct {
Table *ast.TableName
Columns []*Column
Parameters []Parameter
Named *named.ParamSet
Query string
}
func convertTableName(id *analyzer.Identifier) *ast.TableName {
if id == nil {
return nil
}
return &ast.TableName{
Catalog: id.Catalog,
Schema: id.Schema,
Name: id.Name,
}
}
func convertTypeName(id *analyzer.Identifier) *ast.TypeName {
if id == nil {
return nil
}
return &ast.TypeName{
Catalog: id.Catalog,
Schema: id.Schema,
Name: id.Name,
}
}
func convertColumn(c *analyzer.Column) *Column {
length := int(c.Length)
return &Column{
Name: c.Name,
OriginalName: c.OriginalName,
DataType: c.DataType,
NotNull: c.NotNull,
Unsigned: c.Unsigned,
IsArray: c.IsArray,
ArrayDims: int(c.ArrayDims),
Comment: c.Comment,
Length: &length,
IsNamedParam: c.IsNamedParam,
IsFuncCall: c.IsFuncCall,
Scope: c.Scope,
Table: convertTableName(c.Table),
TableAlias: c.TableAlias,
Type: convertTypeName(c.Type),
EmbedTable: convertTableName(c.EmbedTable),
IsSqlcSlice: c.IsSqlcSlice,
}
}
func combineAnalysis(prev *analysis, a *analyzer.Analysis) *analysis {
var cols []*Column
for _, c := range a.Columns {
cols = append(cols, convertColumn(c))
}
var params []Parameter
for _, p := range a.Params {
params = append(params, Parameter{
Number: int(p.Number),
Column: convertColumn(p.Column),
})
}
if len(prev.Columns) == len(cols) {
for i := range prev.Columns {
// Only override column types if the analyzer provides a specific type
// (not "any"), since the catalog-based inference may have better info
if cols[i].DataType != "any" {
prev.Columns[i].DataType = cols[i].DataType
prev.Columns[i].IsArray = cols[i].IsArray
prev.Columns[i].ArrayDims = cols[i].ArrayDims
}
}
} else {
embedding := false
for i := range prev.Columns {
if prev.Columns[i].EmbedTable != nil {
embedding = true
}
}
if !embedding {
prev.Columns = cols
}
}
if len(prev.Parameters) == len(params) {
for i := range prev.Parameters {
// Only override parameter types if the analyzer provides a specific type
// (not "any"), since the catalog-based inference may have better info
if params[i].Column.DataType != "any" {
prev.Parameters[i].Column.DataType = params[i].Column.DataType
prev.Parameters[i].Column.IsArray = params[i].Column.IsArray
prev.Parameters[i].Column.ArrayDims = params[i].Column.ArrayDims
}
}
} else {
prev.Parameters = params
}
return prev
}
func (c *Compiler) analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) {
return c._analyzeQuery(raw, query, pre, true)
}
func (c *Compiler) inferQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) {
return c._analyzeQuery(raw, query, pre, false)
}
func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement, failfast bool) (*analysis, error) {
errors := make([]error, 0)
check := func(err error) error {
if failfast {
return err
}
if err != nil {
errors = append(errors, err)
}
return nil
}
dollar := pre.Dollar
if err := check(pre.ParamErr); err != nil {
return nil, err
}
namedParams := pre.Params
var table *ast.TableName
switch n := raw.Stmt.(type) {
case *ast.InsertStmt:
var err error
table, err = ParseTableName(n.Relation)
if err := check(err); err != nil {
return nil, err
}
if err := check(validate.InsertStmt(c.catalog, table, n)); err != nil {
return nil, err
}
}
if err := check(validate.FuncCall(c.catalog, c.combo, raw)); err != nil {
return nil, err
}
if err := check(validate.Slice(raw, pre.Slices)); err != nil {
return nil, err
}
rvs := rangeVars(raw.Stmt)
refs, errs := findParameters(raw.Stmt)
if len(errs) > 0 {
if failfast {
return nil, errs[0]
}
errors = append(errors, errs...)
}
refs = uniqueParamRefs(refs, dollar)
if c.conf.Engine == config.EngineMySQL || !dollar {
sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Location < refs[j].ref.Location })
} else {
sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number })
}
embeds := pre.Embeds
qc, err := c.buildQueryCatalog(c.catalog, raw.Stmt, embeds)
if err := check(err); err != nil {
return nil, err
}
params, err := c.resolveCatalogRefs(qc, rvs, refs, namedParams, embeds)
if err := check(err); err != nil {
return nil, err
}
cols, err := c.outputColumns(qc, raw.Stmt)
if err := check(err); err != nil {
return nil, err
}
edits, err := c.expand(qc, raw)
if check(err); err != nil {
return nil, err
}
expanded, err := source.Mutate(query, edits)
if err != nil {
return nil, err
}
var rerr error
if len(errors) > 0 {
rerr = errors[0]
}
return &analysis{
Table: table,
Columns: cols,
Parameters: params,
Query: expanded,
Named: namedParams,
}, rerr
}

View File

@@ -0,0 +1,62 @@
package compiler
import (
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
)
// coreResultCatalog dumps the core catalog into the legacy catalog shape a
// Result carries, so codegen sees the same table models either way a query
// set was analyzed. Only relations make the trip: codegen reads tables and
// their columns to build models, and none of the types, functions or
// operators the core catalog also holds.
func coreResultCatalog(c *core.Catalog) (*catalog.Catalog, error) {
cat := catalog.New("public")
namespaces, err := c.Namespaces()
if err != nil {
return nil, err
}
for _, ns := range namespaces {
schema := &catalog.Schema{Name: ns.Name}
tables, err := c.TablesInNamespace(ns.OID)
if err != nil {
return nil, err
}
for _, table := range tables {
cols, err := c.ClassCodegenColumns(table.OID)
if err != nil {
return nil, err
}
t := &catalog.Table{Rel: &ast.TableName{Schema: ns.Name, Name: table.Name}}
for _, col := range cols {
// Codegen reads a data type and an array flag, and renders
// a "[]" per dimension, so an array of arrays of integers
// is the type integer with two dimensions.
expr, err := c.TypeExprOf(col.TypeOID)
if err != nil {
return nil, err
}
inner := expr.Innermost()
column := &catalog.Column{
Name: col.Name,
Type: ast.TypeName{Name: strings.TrimSuffix(inner.Name, " unsigned")},
IsNotNull: col.NotNull,
IsArray: expr.IsArray(),
ArrayDims: expr.ArrayDims(),
IsUnsigned: strings.HasSuffix(inner.Name, " unsigned"),
}
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
l := int(*inner.Args[0].Int)
column.Length = &l
}
t.Columns = append(t.Columns, column)
}
schema.Tables = append(schema.Tables, t)
}
cat.Schemas = append(cat.Schemas, schema)
}
return cat, nil
}

137
internal/compiler/compat.go Normal file
View File

@@ -0,0 +1,137 @@
package compiler
import (
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
)
// This is mainly copy-pasted from internal/postgresql/parse.go
func stringSlice(list *ast.List) []string {
items := []string{}
for _, item := range list.Items {
if n, ok := item.(*ast.String); ok {
items = append(items, n.Str)
}
}
return items
}
type Relation struct {
Catalog string
Schema string
Name string
}
func parseRelation(node ast.Node) (*Relation, error) {
switch n := node.(type) {
case *ast.Boolean:
if n == nil {
return nil, fmt.Errorf("unexpected nil in %T node", n)
}
return &Relation{Name: "bool"}, nil
case *ast.List:
if n == nil {
return nil, fmt.Errorf("unexpected nil in %T node", n)
}
parts := stringSlice(n)
switch len(parts) {
case 1:
return &Relation{
Name: parts[0],
}, nil
case 2:
return &Relation{
Schema: parts[0],
Name: parts[1],
}, nil
case 3:
return &Relation{
Catalog: parts[0],
Schema: parts[1],
Name: parts[2],
}, nil
default:
return nil, fmt.Errorf("invalid name: %s", astutils.Join(n, "."))
}
case *ast.RangeVar:
if n == nil {
return nil, fmt.Errorf("unexpected nil in %T node", n)
}
name := Relation{}
if n.Catalogname != nil {
name.Catalog = *n.Catalogname
}
if n.Schemaname != nil {
name.Schema = *n.Schemaname
}
if n.Relname != nil {
name.Name = *n.Relname
}
return &name, nil
case *ast.TypeName:
if n == nil {
return nil, fmt.Errorf("unexpected nil in %T node", n)
}
if n.Names != nil {
return parseRelation(n.Names)
} else {
return &Relation{Name: n.Name}, nil
}
default:
return nil, fmt.Errorf("unexpected node type: %T", node)
}
}
func ParseTableName(node ast.Node) (*ast.TableName, error) {
rel, err := parseRelation(node)
if err != nil {
return nil, fmt.Errorf("parse table name: %w", err)
}
return &ast.TableName{
Catalog: rel.Catalog,
Schema: rel.Schema,
Name: rel.Name,
}, nil
}
func ParseTypeName(node ast.Node) (*ast.TypeName, error) {
rel, err := parseRelation(node)
if err != nil {
return nil, fmt.Errorf("parse type name: %w", err)
}
return &ast.TypeName{
Catalog: rel.Catalog,
Schema: rel.Schema,
Name: rel.Name,
}, nil
}
func ParseRelationString(name string) (*Relation, error) {
parts := strings.Split(name, ".")
switch len(parts) {
case 1:
return &Relation{
Name: parts[0],
}, nil
case 2:
return &Relation{
Schema: parts[0],
Name: parts[1],
}, nil
case 3:
return &Relation{
Catalog: parts[0],
Schema: parts[1],
Name: parts[2],
}, nil
default:
return nil, fmt.Errorf("invalid name: %s", name)
}
}

View File

@@ -0,0 +1,300 @@
package compiler
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/sync/errgroup"
"github.com/sqlc-dev/sqlc/internal/core"
coreschema "github.com/sqlc-dev/sqlc/internal/core/schema"
"github.com/sqlc-dev/sqlc/internal/migrations"
"github.com/sqlc-dev/sqlc/internal/multierr"
"github.com/sqlc-dev/sqlc/internal/opts"
"github.com/sqlc-dev/sqlc/internal/rpc"
"github.com/sqlc-dev/sqlc/internal/source"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
)
// TODO: Rename this interface Engine
type Parser interface {
Parse(io.Reader) ([]ast.Statement, error)
CommentSyntax() source.CommentSyntax
IsReservedKeyword(string) bool
}
// schemaFile is a schema file's contents, after the rewrites that are applied
// before any of it is parsed.
type schemaFile struct {
name string
contents string
}
func (c *Compiler) parseCatalog(schemas []string) error {
paths, err := sqlpath.Glob(schemas)
if err != nil {
return err
}
merr := multierr.New()
files := make([]schemaFile, 0, len(paths))
for _, path := range paths {
blob, err := os.ReadFile(path)
if err != nil {
merr.Add(path, "", 0, err)
continue
}
contents := migrations.RemoveRollbackStatements(string(blob))
contents = migrations.RemovePsqlMetaCommands(contents)
files = append(files, schemaFile{name: path, contents: contents})
c.schema = append(c.schema, contents)
}
if c.coreAnalysis {
// A schema file that could not be read is not part of the schema the
// action was keyed on, so there is no catalog to restore and nothing
// further to report.
if len(merr.Errs()) > 0 {
return merr
}
if err := c.parseCatalogCore(files, merr); err != nil {
return err
}
} else {
c.parseCatalogLegacy(files, merr)
}
if len(merr.Errs()) > 0 {
return merr
}
return nil
}
func (c *Compiler) parseCatalogLegacy(files []schemaFile, merr *multierr.Error) {
for _, file := range files {
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
if err != nil {
// A schema file and a query file are often the same file, so a
// query's sqlc syntax can fail here. Look for an explanation
// before reporting the syntax error it caused.
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
merr.Add(file.name, file.contents, 0, err)
}
continue
}
for i := range stmts {
if err := c.catalog.Update(stmts[i], c); err != nil {
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
continue
}
}
}
}
// parseCatalogCore builds the catalog the analysis core works against: the
// dialect's seed with the schema's DDL applied. The two are one cached action,
// so a run that has built this catalog before restores it and never parses the
// schema at all.
func (c *Compiler) parseCatalogCore(files []schemaFile, merr *multierr.Error) error {
contents := make([]string, 0, len(files))
for _, file := range files {
contents = append(contents, file.contents)
}
cat, err := core.NewCached(string(c.conf.Engine), contents, func(cat *core.Catalog) error {
for _, file := range files {
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
if err != nil {
// A schema file and a query file are often the same file, so a
// query's sqlc syntax can fail here. Look for an explanation
// before reporting the syntax error it caused.
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
merr.Add(file.name, file.contents, 0, err)
}
continue
}
for i := range stmts {
if err := coreschema.Apply(cat, stmts[i].Raw); err != nil {
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
continue
}
}
}
// A catalog the whole schema did not make it into is not the catalog
// this action describes, so it is not one to keep.
if len(merr.Errs()) > 0 {
return merr
}
return nil
}, c.coreDialect)
if cat == nil {
return fmt.Errorf("%s: init catalog: %w", c.conf.Engine, err)
}
// Whatever apply reported is already in merr, which the caller returns.
c.coreCatalog = cat
// Codegen consumes the catalog through the Result, in the legacy shape.
legacy, err := coreResultCatalog(cat)
if err != nil {
return fmt.Errorf("%s: dump catalog: %w", c.conf.Engine, err)
}
c.catalog = legacy
return nil
}
// statement is a parsed query awaiting analysis, with what error reporting
// needs to place it back in the file it came from.
type statement struct {
filename string
src string
pp *preprocess.Result
raw *ast.RawStmt
}
func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
merr := multierr.New()
files, err := sqlpath.Glob(c.conf.Queries)
if err != nil {
return nil, err
}
var stmts []statement
for _, filename := range files {
blob, err := os.ReadFile(filename)
if err != nil {
merr.Add(filename, "", 0, err)
continue
}
src := string(blob)
// sqlc syntax is not SQL. Rewrite it to native placeholders before the
// engine parser ever sees the query, so parsers only handle SQL.
pp := preprocess.File(c.conf.Engine, src)
parsed, err := c.parser.Parse(strings.NewReader(pp.Text))
if err != nil {
if reported := addSyntaxErrors(merr, filename, src, pp); !reported {
merr.Add(filename, src, 0, err)
}
continue
}
for _, stmt := range parsed {
stmts = append(stmts, statement{filename: filename, src: src, pp: pp, raw: stmt.Raw})
}
}
queries, errs := c.analyzeStatements(stmts, o)
var q []*Query
set := map[string]struct{}{}
for i, stmt := range stmts {
if err := errs[i]; err != nil {
var e *sqlerr.Error
loc := stmt.raw.Pos()
if errors.As(err, &e) && e.Location != 0 {
loc = e.Location
}
// Locations are reported against the rewritten query; map them
// back so errors point at what the user wrote.
loc = stmt.pp.Origin(loc)
if e != nil && e.Location != 0 {
e.Location = loc
}
merr.Add(stmt.filename, stmt.src, loc, err)
// If this rpc unauthenticated error bubbles up, then all future parsing/analysis will fail
if errors.Is(err, rpc.ErrUnauthenticated) {
return nil, merr
}
continue
}
query := queries[i]
if query == nil {
continue
}
query.Metadata.Filename = filepath.Base(stmt.filename)
queryName := query.Metadata.Name
if queryName != "" {
if _, exists := set[queryName]; exists {
merr.Add(stmt.filename, stmt.src, stmt.pp.Origin(stmt.raw.Pos()), fmt.Errorf("duplicate query name: %s", queryName))
continue
}
set[queryName] = struct{}{}
}
q = append(q, query)
}
if len(merr.Errs()) > 0 {
return nil, merr
}
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,
}, nil
}
// analyzeStatements analyzes each statement, returning the results in the
// order the statements were given so that queries and errors come out in
// source order however they were produced.
//
// The analysis core reads the catalog and nothing else, so its statements are
// analyzed concurrently. Every other path holds state that a second goroutine
// would race — a database connection, the legacy catalog — and stays serial.
func (c *Compiler) analyzeStatements(stmts []statement, o opts.Parser) ([]*Query, []error) {
queries := make([]*Query, len(stmts))
errs := make([]error, len(stmts))
analyze := func(i int) {
queries[i], errs[i] = c.parseQuery(stmts[i].raw, stmts[i].pp, o)
}
workers := runtime.GOMAXPROCS(0)
if !c.coreAnalysis || workers < 2 || len(stmts) < 2 {
for i := range stmts {
analyze(i)
}
return queries, errs
}
var g errgroup.Group
g.SetLimit(workers)
for i := range stmts {
g.Go(func() error {
analyze(i)
return nil
})
}
g.Wait()
return queries, errs
}
func addSyntaxErrors(merr *multierr.Error, filename, src string, pp *preprocess.Result) bool {
var found bool
for _, stmt := range pp.Statements() {
if stmt.Err == nil {
continue
}
found = true
loc := pp.Origin(stmt.Start)
var e *sqlerr.Error
if errors.As(stmt.Err, &e) && e.Location != 0 {
loc = pp.Origin(e.Location)
e.Location = loc
}
merr.Add(filename, src, loc, stmt.Err)
}
return found
}

199
internal/compiler/engine.go Normal file
View File

@@ -0,0 +1,199 @@
package compiler
import (
"context"
"fmt"
"github.com/sqlc-dev/sqlc/internal/analyzer"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/core"
"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/duckdb"
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
"github.com/sqlc-dev/sqlc/internal/engine/mssql"
"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"
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"
)
type Compiler struct {
conf config.SQL
combo config.CombinedSettings
catalog *catalog.Catalog
parser Parser
result *Result
analyzer analyzer.Analyzer
client dbmanager.Client
selector selector
coreCatalog *core.Catalog
// coreAnalysis routes every engine through the core catalog and analyzer,
// and coreDialect seeds the catalog it does so with. The catalog itself is
// not built until the schema is loaded, because the two are cached
// together.
coreAnalysis bool
coreDialect core.Option
// newParser builds a parser for the configured engine, and is set for
// every engine either path supports. The core path analyzes statements
// concurrently, and a parser holds enough state that two goroutines cannot
// share one, so a goroutine that has to parse something of its own — the
// query text it just rewrote — builds its own rather than taking c.parser.
newParser func() Parser
schema []string
}
// 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, GoogleSQL, SQL Server and DuckDB have no legacy analysis
// path to fall back to.
switch conf.Engine {
case config.EngineClickHouse, config.EngineGoogleSQL, config.EngineMSSQL, config.EngineDuckDB:
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)
c.client = client
}
switch conf.Engine {
case config.EngineSQLite:
c.newParser = func() Parser { return sqlite.NewParser() }
c.catalog = sqlite.NewCatalog()
c.selector = newSQLiteSelector()
if conf.Database != nil {
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
c.analyzer = analyzer.Cached(
sqliteanalyze.New(*conf.Database),
combo.Global,
*conf.Database,
)
}
}
case config.EngineMySQL:
c.newParser = func() Parser { return dolphin.NewParser() }
c.catalog = dolphin.NewCatalog()
c.selector = newDefaultSelector()
case config.EnginePostgreSQL:
c.newParser = func() Parser { return postgresql.NewParser() }
c.catalog = postgresql.NewCatalog()
c.selector = newDefaultSelector()
if conf.Database != nil {
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
c.analyzer = analyzer.Cached(
pganalyze.New(c.client, *conf.Database),
combo.Global,
*conf.Database,
)
}
}
default:
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
}
c.parser = c.newParser()
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.newParser = func() Parser { return sqlite.NewParser() }
c.selector = newSQLiteSelector()
dialect = sqlite.Dialect()
case config.EngineMySQL:
c.newParser = func() Parser { return dolphin.NewParser() }
c.selector = newDefaultSelector()
dialect = dolphin.Dialect()
case config.EnginePostgreSQL:
c.newParser = func() Parser { return postgresql.NewParser() }
c.selector = newDefaultSelector()
dialect = postgresql.Dialect()
case config.EngineClickHouse:
c.newParser = func() Parser { return clickhouse.NewParser() }
c.selector = newDefaultSelector()
dialect = clickhouse.Dialect()
case config.EngineGoogleSQL:
c.newParser = func() Parser { return googlesql.NewParser() }
c.selector = newDefaultSelector()
dialect = googlesql.Dialect()
case config.EngineMSSQL:
c.newParser = func() Parser { return mssql.NewParser() }
c.selector = newDefaultSelector()
dialect = mssql.Dialect()
case config.EngineDuckDB:
c.newParser = func() Parser { return duckdb.NewParser() }
c.selector = newDefaultSelector()
dialect = duckdb.Dialect()
default:
return fmt.Errorf("unknown engine: %s", c.conf.Engine)
}
c.parser = c.newParser()
c.coreDialect = dialect
return nil
}
func (c *Compiler) Catalog() *catalog.Catalog {
return c.catalog
}
func (c *Compiler) ParseCatalog(schema []string) error {
return c.parseCatalog(schema)
}
func (c *Compiler) ParseQueries(queries []string, o opts.Parser) error {
r, err := c.parseQueries(o)
if err != nil {
return err
}
c.result = r
return nil
}
func (c *Compiler) Result() *Result {
return c.result
}
func (c *Compiler) Close(ctx context.Context) {
if c.analyzer != nil {
c.analyzer.Close(ctx)
}
if c.client != nil {
c.client.Close(ctx)
}
if c.coreCatalog != nil {
c.coreCatalog.Close()
}
}

211
internal/compiler/expand.go Normal file
View File

@@ -0,0 +1,211 @@
package compiler
import (
"fmt"
"regexp"
"strings"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/source"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
)
func (c *Compiler) expand(qc *QueryCatalog, raw *ast.RawStmt) ([]source.Edit, error) {
// Return early if there are no A_Star nodes to expand
stars := astutils.Search(raw, func(node ast.Node) bool {
_, ok := node.(*ast.A_Star)
return ok
})
if len(stars.Items) == 0 {
return nil, nil
}
list := astutils.Search(raw, func(node ast.Node) bool {
switch node.(type) {
case *ast.DeleteStmt:
case *ast.InsertStmt:
case *ast.SelectStmt:
case *ast.UpdateStmt:
default:
return false
}
return true
})
if len(list.Items) == 0 {
return nil, nil
}
var edits []source.Edit
for _, item := range list.Items {
edit, err := c.expandStmt(qc, raw, item)
if err != nil {
return nil, err
}
edits = append(edits, edit...)
}
return edits, nil
}
var validPostgresIdent = regexp.MustCompile(`^[a-z_][a-z0-9_$]*$`)
func (c *Compiler) quoteIdent(ident string) string {
if c.parser.IsReservedKeyword(ident) {
return c.quote(ident)
}
// SQL identifiers and key words must begin with a letter (a-z, but also
// letters with diacritical marks and non-Latin letters) or an underscore
// (_). Subsequent characters in an identifier or key word can be letters,
// underscores, digits (0-9), or dollar signs ($).
//
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
if c.conf.Engine == config.EnginePostgreSQL {
// camelCase means the column is also camelCase
if strings.ToLower(ident) != ident {
return c.quote(ident)
}
if !validPostgresIdent.MatchString(strings.ToLower(ident)) {
return c.quote(ident)
}
}
return ident
}
func (c *Compiler) quote(x string) string {
switch c.conf.Engine {
case config.EngineMySQL:
return "`" + x + "`"
default:
return "\"" + x + "\""
}
}
// starOldFunc measures how much of the query text a star reference occupies,
// so an edit replaces the reference and nothing else. Each part is measured
// both bare and quoted: an embed was rewritten to "table.*" in the query text,
// preserving the way the user quoted the table, so it is measured the same way
// as a star reference the user wrote.
func (c *Compiler) starOldFunc(parts []string) func(string) int {
old := make([]string, 0, len(parts))
for _, p := range parts {
if p == "*" {
old = append(old, p)
} else {
old = append(old, c.quoteIdent(p))
}
}
return func(s string) int {
length := 0
for i, o := range old {
if hasSeparator := i > 0; hasSeparator {
length++
}
if strings.HasPrefix(s[length:], o) {
length += len(o)
} else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) {
length += len(quoted)
} else {
length += len(o)
}
}
return length
}
}
func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) ([]source.Edit, error) {
tables, err := c.sourceTables(qc, node)
if err != nil {
return nil, err
}
// Virtual tables for the OLD and NEW aliases available in a RETURNING
// clause (PostgreSQL 18)
rtables, err := c.returningTables(qc, node)
if err != nil {
return nil, err
}
var targets *ast.List
switch n := node.(type) {
case *ast.DeleteStmt:
targets = n.ReturningList
case *ast.InsertStmt:
targets = n.ReturningList
case *ast.SelectStmt:
targets = n.TargetList
case *ast.UpdateStmt:
targets = n.ReturningList
default:
return nil, fmt.Errorf("outputColumns: unsupported node type: %T", n)
}
var edits []source.Edit
for _, target := range targets.Items {
res, ok := target.(*ast.ResTarget)
if !ok {
continue
}
ref, ok := res.Val.(*ast.ColumnRef)
if !ok {
continue
}
if !hasStarRef(ref) {
continue
}
var parts, cols []string
for _, f := range ref.Fields.Items {
switch field := f.(type) {
case *ast.String:
parts = append(parts, field.Str)
case *ast.A_Star:
parts = append(parts, "*")
default:
return nil, fmt.Errorf("unknown field in ColumnRef: %T", f)
}
}
scope := astutils.Join(ref.Fields, ".")
counts := map[string]int{}
if scope == "" {
for _, t := range tables {
for _, c := range t.Columns {
counts[c.Name] += 1
}
}
}
starTables := tables
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
starTables = []*Table{vt}
}
for _, t := range starTables {
if scope != "" && scope != t.Rel.Name {
continue
}
tableName := c.quoteIdent(t.Rel.Name)
scopeName := c.quoteIdent(scope)
for _, column := range t.Columns {
cname := column.Name
if res.Name != nil {
cname = *res.Name
}
cname = c.quoteIdent(cname)
if scope != "" {
cname = scopeName + "." + cname
}
if counts[cname] > 1 {
cname = tableName + "." + cname
}
// This is important for SQLite in particular which needs to
// wrap jsonb column values with `json(colname)` so they're in a
// publicly usable format (i.e. not jsonb).
cname = c.selector.ColumnExpr(cname, column)
cols = append(cols, cname)
}
}
edits = append(edits, source.Edit{
Location: res.Location - raw.StmtLocation,
OldFunc: c.starOldFunc(parts),
New: strings.Join(cols, ", "),
})
}
return edits, nil
}

View File

@@ -0,0 +1,76 @@
package compiler
import (
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/source"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
// expandCore rewrites the stars in a query's text with the columns the core
// analyzer resolved them to. The analyzer has already walked the statement and
// its subqueries, so there is nothing left to look up here: what remains is
// deciding how each name is written, which is the engine's business and not
// the core's.
func (c *Compiler) expandCore(raw *ast.RawStmt, stars []core.StarExpansion) []source.Edit {
if len(stars) == 0 {
return nil
}
edits := make([]source.Edit, 0, len(stars))
seen := make(map[int]bool, len(stars))
for _, star := range stars {
// The analyzer types an expression again when a later clause refers to
// the output name it was given, so "SELECT (SELECT * FROM t) AS x ...
// GROUP BY x" reports the star in the subquery once per pass. The
// passes agree on what the star covers, and editing the same reference
// twice would overlap, so only the first is kept.
if seen[star.Location] {
continue
}
seen[star.Location] = true
// Everything before the star qualifies it: "foo.*" is scoped to foo,
// while a bare "*" covers every relation in the FROM clause.
scope := strings.Join(star.Fields[:len(star.Fields)-1], ".")
// An unqualified star that covers more than one relation may name the
// same column twice, so those are written with their relation.
counts := map[string]int{}
if scope == "" {
for _, col := range star.Columns {
counts[col.Name]++
}
}
cols := make([]string, 0, len(star.Columns))
for _, col := range star.Columns {
cname := col.Name
if star.Alias != "" {
cname = star.Alias
}
cname = c.quoteIdent(cname)
if scope != "" {
cname = c.quoteIdent(scope) + "." + cname
}
if counts[cname] > 1 {
cname = c.quoteIdent(col.Relation) + "." + cname
}
// This is important for SQLite in particular which needs to wrap
// jsonb column values with `json(colname)` so they're in a publicly
// usable format (i.e. not jsonb).
cols = append(cols, c.selector.ColumnExpr(cname, &Column{
Name: col.Name,
DataType: col.DataType,
}))
}
edits = append(edits, source.Edit{
Location: star.Location - raw.StmtLocation,
OldFunc: c.starOldFunc(star.Fields),
New: strings.Join(cols, ", "),
})
}
return edits
}

View File

@@ -0,0 +1,214 @@
package compiler
import (
"fmt"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
)
func findParameters(root ast.Node) ([]paramRef, []error) {
refs := make([]paramRef, 0)
errors := make([]error, 0)
v := paramSearch{seen: make(map[int]struct{}), refs: &refs, errs: &errors}
astutils.Walk(v, root)
if len(*v.errs) > 0 {
return refs, *v.errs
} else {
return refs, nil
}
}
type paramRef struct {
parent ast.Node
rv *ast.RangeVar
ref *ast.ParamRef
name string // Named parameter support
}
type paramSearch struct {
parent ast.Node
rangeVar *ast.RangeVar
refs *[]paramRef
seen map[int]struct{}
errs *[]error
// XXX: Gross state hack for limit
limitCount ast.Node
limitOffset ast.Node
}
type limitCount struct {
}
func (l *limitCount) Pos() int {
return 0
}
type limitOffset struct {
}
func (l *limitOffset) Pos() int {
return 0
}
func (p paramSearch) Visit(node ast.Node) astutils.Visitor {
switch n := node.(type) {
case *ast.A_Expr:
p.parent = node
case *ast.BetweenExpr:
p.parent = node
case *ast.CallStmt:
p.parent = n.FuncCall
case *ast.DeleteStmt:
if n.LimitCount != nil {
p.limitCount = n.LimitCount
}
case *ast.FuncCall:
p.parent = node
case *ast.InsertStmt:
if s, ok := n.SelectStmt.(*ast.SelectStmt); ok {
for i, item := range s.TargetList.Items {
target, ok := item.(*ast.ResTarget)
if !ok {
continue
}
ref, ok := target.Val.(*ast.ParamRef)
if !ok {
continue
}
if len(n.Cols.Items) <= i {
*p.errs = append(*p.errs, fmt.Errorf("INSERT has more expressions than target columns"))
return p
}
*p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: n.Relation})
p.seen[ref.Location] = struct{}{}
}
for _, item := range s.ValuesLists.Items {
vl, ok := item.(*ast.List)
if !ok {
continue
}
for i, v := range vl.Items {
ref, ok := v.(*ast.ParamRef)
if !ok {
continue
}
if len(n.Cols.Items) <= i {
*p.errs = append(*p.errs, fmt.Errorf("INSERT has more expressions than target columns"))
return p
}
*p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: n.Relation})
p.seen[ref.Location] = struct{}{}
}
}
}
case *ast.UpdateStmt:
for _, item := range n.TargetList.Items {
target, ok := item.(*ast.ResTarget)
if !ok {
continue
}
ref, ok := target.Val.(*ast.ParamRef)
if !ok {
continue
}
for _, relation := range n.Relations.Items {
rv, ok := relation.(*ast.RangeVar)
if !ok {
continue
}
*p.refs = append(*p.refs, paramRef{parent: target, ref: ref, rv: rv})
}
p.seen[ref.Location] = struct{}{}
}
if n.LimitCount != nil {
p.limitCount = n.LimitCount
}
case *ast.RangeVar:
p.rangeVar = n
case *ast.ResTarget:
p.parent = node
case *ast.SelectStmt:
if n.LimitCount != nil {
p.limitCount = n.LimitCount
}
if n.LimitOffset != nil {
p.limitOffset = n.LimitOffset
}
case *ast.TypeCast:
p.parent = node
case *ast.ParamRef:
parent := p.parent
if count, ok := p.limitCount.(*ast.ParamRef); ok {
if n.Number == count.Number {
parent = &limitCount{}
}
}
if offset, ok := p.limitOffset.(*ast.ParamRef); ok {
if n.Number == offset.Number {
parent = &limitOffset{}
}
}
if _, found := p.seen[n.Location]; found {
break
}
// Special, terrible case for *ast.MultiAssignRef
set := true
if res, ok := parent.(*ast.ResTarget); ok {
if multi, ok := res.Val.(*ast.MultiAssignRef); ok {
set = false
if row, ok := multi.Source.(*ast.RowExpr); ok {
for i, arg := range row.Args.Items {
if ref, ok := arg.(*ast.ParamRef); ok {
if multi.Colno == i+1 && ref.Number == n.Number {
set = true
}
}
}
}
}
}
if set {
*p.refs = append(*p.refs, paramRef{parent: parent, ref: n, rv: p.rangeVar})
p.seen[n.Location] = struct{}{}
}
return nil
case *ast.In:
if n.Sel == nil {
p.parent = node
} else {
if sel, ok := n.Sel.(*ast.SelectStmt); ok && sel.FromClause != nil && len(sel.FromClause.Items) > 0 {
from := sel.FromClause
if schema, ok := from.Items[0].(*ast.RangeVar); ok && schema != nil {
p.rangeVar = &ast.RangeVar{
Catalogname: schema.Catalogname,
Schemaname: schema.Schemaname,
Relname: schema.Relname,
}
}
}
}
if _, ok := n.Expr.(*ast.ParamRef); ok {
p.Visit(n.Expr)
}
}
return p
}

View File

@@ -0,0 +1,812 @@
package compiler
import (
"errors"
"fmt"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
"github.com/sqlc-dev/sqlc/internal/sql/lang"
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
)
// OutputColumns determines which columns a statement will output
func (c *Compiler) OutputColumns(stmt ast.Node) ([]*catalog.Column, error) {
qc, err := c.buildQueryCatalog(c.catalog, stmt, nil)
if err != nil {
return nil, err
}
cols, err := c.outputColumns(qc, stmt)
if err != nil {
return nil, err
}
catCols := make([]*catalog.Column, 0, len(cols))
for _, col := range cols {
catCols = append(catCols, &catalog.Column{
Name: col.Name,
Type: ast.TypeName{Name: col.DataType},
IsNotNull: col.NotNull,
IsUnsigned: col.Unsigned,
IsArray: col.IsArray,
ArrayDims: col.ArrayDims,
Comment: col.Comment,
Length: col.Length,
})
}
return catCols, nil
}
func hasStarRef(cf *ast.ColumnRef) bool {
for _, item := range cf.Fields.Items {
if _, ok := item.(*ast.A_Star); ok {
return true
}
}
return false
}
// Compute the output columns for a statement.
//
// Return an error if column references are ambiguous
// Return an error if column references don't exist
func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, error) {
tables, err := c.sourceTables(qc, node)
if err != nil {
return nil, err
}
// Virtual tables for the OLD and NEW aliases available in a RETURNING
// clause (PostgreSQL 18)
rtables, err := c.returningTables(qc, node)
if err != nil {
return nil, err
}
targets := &ast.List{}
switch n := node.(type) {
case *ast.DeleteStmt:
targets = n.ReturningList
case *ast.InsertStmt:
targets = n.ReturningList
case *ast.SelectStmt:
targets = n.TargetList
isUnion := len(targets.Items) == 0 && n.Larg != nil
if n.GroupClause != nil {
for _, item := range n.GroupClause.Items {
if err := c.findColumnForNode(item, tables, targets); err != nil {
return nil, err
}
}
}
validateOrderBy := true
if c.conf.StrictOrderBy != nil {
validateOrderBy = *c.conf.StrictOrderBy
}
if !isUnion && validateOrderBy {
if n.SortClause != nil {
for _, item := range n.SortClause.Items {
sb, ok := item.(*ast.SortBy)
if !ok {
continue
}
if err := c.findColumnForNode(sb.Node, tables, targets); err != nil {
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
}
}
}
if n.WindowClause != nil {
for _, item := range n.WindowClause.Items {
sb, ok := item.(*ast.List)
if !ok {
continue
}
for _, single := range sb.Items {
caseExpr, ok := single.(*ast.CaseExpr)
if !ok {
continue
}
if err := c.findColumnForNode(caseExpr.Xpr, tables, targets); err != nil {
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
}
}
}
}
}
// For UNION queries, targets is empty and we need to look for the
// columns in Largs.
if isUnion {
return c.outputColumns(qc, n.Larg)
}
case *ast.UpdateStmt:
targets = n.ReturningList
}
var cols []*Column
for _, target := range targets.Items {
res, ok := target.(*ast.ResTarget)
if !ok {
continue
}
switch n := res.Val.(type) {
case *ast.A_Const:
name := ""
if res.Name != nil {
name = *res.Name
}
switch n.Val.(type) {
case *ast.String:
cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true})
case *ast.Integer:
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
case *ast.Float:
cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true})
case *ast.Boolean:
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
default:
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
}
case *ast.A_Expr:
name := ""
if res.Name != nil {
name = *res.Name
}
switch op := astutils.Join(n.Name, ""); {
case lang.IsComparisonOperator(op):
// TODO: Generate a name for these operations
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
case lang.IsMathematicalOperator(op):
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
default:
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
}
case *ast.BoolExpr:
name := ""
if res.Name != nil {
name = *res.Name
}
notNull := false
if len(n.Args.Items) == 1 {
switch n.Boolop {
case ast.BoolExprTypeIsNull, ast.BoolExprTypeIsNotNull:
notNull = true
case ast.BoolExprTypeNot:
sublink, ok := n.Args.Items[0].(*ast.SubLink)
if ok && sublink.SubLinkType == ast.EXISTS_SUBLINK {
notNull = true
if name == "" {
name = "not_exists"
}
}
}
}
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: notNull})
case *ast.CaseExpr:
name := ""
if res.Name != nil {
name = *res.Name
}
// TODO: The TypeCase and A_Const code has been copied from below. Instead, we
// need a recurse function to get the type of a node.
if tc, ok := n.Defresult.(*ast.TypeCast); ok {
if tc.TypeName == nil {
return nil, errors.New("no type name type cast")
}
name := ""
if ref, ok := tc.Arg.(*ast.ColumnRef); ok {
name = astutils.Join(ref.Fields, "_")
}
if res.Name != nil {
name = *res.Name
}
// TODO Validate column names
col := toColumn(tc.TypeName)
col.Name = name
cols = append(cols, col)
} else if aconst, ok := n.Defresult.(*ast.A_Const); ok {
switch aconst.Val.(type) {
case *ast.String:
cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true})
case *ast.Integer:
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
case *ast.Float:
cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true})
case *ast.Boolean:
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
default:
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
}
} else {
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
}
case *ast.CoalesceExpr:
name := "coalesce"
if res.Name != nil {
name = *res.Name
}
var firstColumn *Column
var shouldNotBeNull bool
for _, arg := range n.Args.Items {
if _, ok := arg.(*ast.A_Const); ok {
shouldNotBeNull = true
continue
}
if ref, ok := arg.(*ast.ColumnRef); ok {
columns, err := outputColumnRefs(res, tablesForRef(ref, tables, rtables), ref)
if err != nil {
return nil, err
}
for _, c := range columns {
if firstColumn == nil {
firstColumn = c
}
shouldNotBeNull = shouldNotBeNull || c.NotNull
}
}
}
if firstColumn != nil {
firstColumn.NotNull = shouldNotBeNull
firstColumn.skipTableRequiredCheck = true
cols = append(cols, firstColumn)
} else {
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
}
case *ast.ColumnRef:
if hasStarRef(n) {
// add a column with a reference to an embedded table
if embed, ok := qc.embeds.Find(n.Location, res.Location); ok {
cols = append(cols, &Column{
Name: embed.Table.Name,
EmbedTable: embed.Table,
})
continue
}
// TODO: This code is copied in func expand()
scope := astutils.Join(n.Fields, ".")
starTables := tables
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
starTables = []*Table{vt}
}
for _, t := range starTables {
if scope != "" && scope != t.Rel.Name {
continue
}
for _, c := range t.Columns {
cname := c.Name
if res.Name != nil {
cname = *res.Name
}
cols = append(cols, &Column{
Name: cname,
OriginalName: c.Name,
Type: c.Type,
Scope: scope,
Table: c.Table,
TableAlias: t.Rel.Name,
DataType: c.DataType,
NotNull: c.NotNull,
Unsigned: c.Unsigned,
IsArray: c.IsArray,
ArrayDims: c.ArrayDims,
Length: c.Length,
})
}
}
continue
}
columns, err := outputColumnRefs(res, tablesForRef(n, tables, rtables), n)
if err != nil {
return nil, err
}
cols = append(cols, columns...)
case *ast.FuncCall:
rel := n.Func
name := rel.Name
if res.Name != nil {
name = *res.Name
}
fun, err := qc.catalog.ResolveFuncCall(n)
if err == nil {
cols = append(cols, &Column{
Name: name,
DataType: dataType(fun.ReturnType),
NotNull: !fun.ReturnTypeNullable,
IsFuncCall: true,
})
} else {
cols = append(cols, &Column{
Name: name,
DataType: "any",
IsFuncCall: true,
})
}
case *ast.SubLink:
name := "exists"
if res.Name != nil {
name = *res.Name
}
switch n.SubLinkType {
case ast.EXISTS_SUBLINK:
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
case ast.EXPR_SUBLINK:
subcols, err := c.outputColumns(qc, n.Subselect)
if err != nil {
return nil, err
}
first := subcols[0]
if res.Name != nil {
first.Name = *res.Name
}
cols = append(cols, first)
default:
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
}
case *ast.TypeCast:
if n.TypeName == nil {
return nil, errors.New("no type name type cast")
}
name := ""
if ref, ok := n.Arg.(*ast.ColumnRef); ok {
name = astutils.Join(ref.Fields, "_")
}
if res.Name != nil {
name = *res.Name
}
// TODO Validate column names
col := toColumn(n.TypeName)
col.Name = name
// TODO Add correct, real type inference
if constant, ok := n.Arg.(*ast.A_Const); ok {
if _, ok := constant.Val.(*ast.Null); ok {
col.NotNull = false
}
}
cols = append(cols, col)
case *ast.SelectStmt:
subcols, err := c.outputColumns(qc, n)
if err != nil {
return nil, err
}
first := subcols[0]
if res.Name != nil {
first.Name = *res.Name
}
cols = append(cols, first)
default:
name := ""
if res.Name != nil {
name = *res.Name
}
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
}
}
if n, ok := node.(*ast.SelectStmt); ok {
for _, col := range cols {
if !col.NotNull || col.Table == nil || col.skipTableRequiredCheck {
continue
}
for _, f := range n.FromClause.Items {
res := isTableRequired(f, col, tableRequired)
if res != tableNotFound {
col.NotNull = res == tableRequired
break
}
}
}
}
return cols, nil
}
const (
tableNotFound = iota
tableRequired
tableOptional
)
func isTableRequired(n ast.Node, col *Column, prior int) int {
switch n := n.(type) {
case *ast.RangeVar:
tableMatch := *n.Relname == col.Table.Name
aliasMatch := true
if n.Alias != nil && col.TableAlias != "" {
aliasMatch = *n.Alias.Aliasname == col.TableAlias
}
if aliasMatch && tableMatch {
return prior
}
case *ast.JoinExpr:
helper := func(l, r int) int {
if res := isTableRequired(n.Larg, col, l); res != tableNotFound {
return res
}
if res := isTableRequired(n.Rarg, col, r); res != tableNotFound {
return res
}
return tableNotFound
}
switch n.Jointype {
case ast.JoinTypeLeft:
return helper(tableRequired, tableOptional)
case ast.JoinTypeRight:
return helper(tableOptional, tableRequired)
case ast.JoinTypeFull:
return helper(tableOptional, tableOptional)
case ast.JoinTypeInner, ast.JoinTypeCross, ast.JoinTypeComma:
return helper(tableRequired, tableRequired)
}
case *ast.List:
for _, item := range n.Items {
if res := isTableRequired(item, col, prior); res != tableNotFound {
return res
}
}
}
return tableNotFound
}
type tableVisitor struct {
list ast.List
}
func (r *tableVisitor) Visit(n ast.Node) astutils.Visitor {
switch n.(type) {
case *ast.RangeVar, *ast.RangeFunction:
r.list.Items = append(r.list.Items, n)
return r
case *ast.RangeSubselect:
r.list.Items = append(r.list.Items, n)
return nil
default:
return r
}
}
// Compute the output columns for a statement.
//
// Return an error if column references are ambiguous
// Return an error if column references don't exist
// Return an error if a table is referenced twice
// Return an error if an unknown column is referenced
func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
list := &ast.List{}
switch n := node.(type) {
case *ast.DeleteStmt:
if n.Relations != nil {
list = n.Relations
} else if n.FromClause != nil {
// Multi-table DELETE: walk FromClause to find tables
var tv tableVisitor
astutils.Walk(&tv, n.FromClause)
list = &tv.list
}
case *ast.InsertStmt:
list = &ast.List{
Items: []ast.Node{n.Relation},
}
case *ast.SelectStmt:
var tv tableVisitor
astutils.Walk(&tv, n.FromClause)
list = &tv.list
case *ast.TruncateStmt:
list = astutils.Search(n.Relations, func(node ast.Node) bool {
_, ok := node.(*ast.RangeVar)
return ok
})
case *ast.RefreshMatViewStmt:
list = astutils.Search(n.Relation, func(node ast.Node) bool {
_, ok := node.(*ast.RangeVar)
return ok
})
case *ast.UpdateStmt:
var tv tableVisitor
astutils.Walk(&tv, n.FromClause)
astutils.Walk(&tv, n.Relations)
list = &tv.list
}
var tables []*Table
for _, item := range list.Items {
switch n := item.(type) {
case *ast.RangeFunction:
var funcCall *ast.FuncCall
switch f := n.Functions.Items[0].(type) {
case *ast.List:
switch fi := f.Items[0].(type) {
case *ast.FuncCall:
funcCall = fi
case *ast.SQLValueFunction:
continue // TODO handle this correctly
default:
continue
}
case *ast.FuncCall:
funcCall = f
default:
return nil, fmt.Errorf("sourceTables: unsupported function call type %T", n.Functions.Items[0])
}
// If the function or table can't be found, don't error out. There
// are many queries that depend on functions unknown to sqlc.
fn, err := qc.GetFunc(funcCall.Func)
if err != nil {
continue
}
var table *Table
if fn.ReturnType != nil {
table, err = qc.GetTable(&ast.TableName{
Catalog: fn.ReturnType.Catalog,
Schema: fn.ReturnType.Schema,
Name: fn.ReturnType.Name,
})
}
if table == nil || err != nil {
if n.Alias != nil && len(n.Alias.Colnames.Items) > 0 {
table = &Table{}
for _, colName := range n.Alias.Colnames.Items {
table.Columns = append(table.Columns, &Column{
Name: colName.(*ast.String).Str,
DataType: "any",
})
}
} else {
colName := fn.Rel.Name
if n.Alias != nil {
colName = *n.Alias.Aliasname
}
table = &Table{
Rel: &ast.TableName{
Catalog: fn.Rel.Catalog,
Schema: fn.Rel.Schema,
Name: fn.Rel.Name,
},
}
if len(fn.Outs) > 0 {
for _, arg := range fn.Outs {
table.Columns = append(table.Columns, &Column{
Name: arg.Name,
DataType: arg.Type.Name,
})
}
}
if fn.ReturnType != nil {
table.Columns = []*Column{
{
Name: colName,
DataType: fn.ReturnType.Name,
},
}
}
}
}
if n.Alias != nil {
table.Rel = &ast.TableName{
Name: *n.Alias.Aliasname,
}
}
tables = append(tables, table)
case *ast.RangeSubselect:
cols, err := c.outputColumns(qc, n.Subquery)
if err != nil {
return nil, err
}
var tableName string
if n.Alias != nil {
tableName = *n.Alias.Aliasname
}
tables = append(tables, &Table{
Rel: &ast.TableName{
Name: tableName,
},
Columns: cols,
})
case *ast.RangeVar:
fqn, err := ParseTableName(n)
if err != nil {
return nil, err
}
if qc == nil {
return nil, fmt.Errorf("query catalog is empty")
}
table, cerr := qc.GetTable(fqn)
if cerr != nil {
// TODO: Update error location
// cerr.Location = n.Location
// return nil, *cerr
return nil, cerr
}
if n.Alias != nil {
table.Rel = &ast.TableName{
Catalog: table.Rel.Catalog,
Schema: table.Rel.Schema,
Name: *n.Alias.Aliasname,
}
}
tables = append(tables, table)
default:
return nil, fmt.Errorf("sourceTable: unsupported list item type: %T", n)
}
}
return tables, nil
}
func outputColumnRefs(res *ast.ResTarget, tables []*Table, node *ast.ColumnRef) ([]*Column, error) {
parts := stringSlice(node.Fields)
var schema, name, alias string
switch {
case len(parts) == 1:
name = parts[0]
case len(parts) == 2:
alias = parts[0]
name = parts[1]
case len(parts) == 3:
schema = parts[0]
alias = parts[1]
name = parts[2]
default:
return nil, fmt.Errorf("unknown number of fields: %d", len(parts))
}
var cols []*Column
var found int
for _, t := range tables {
if schema != "" && t.Rel.Schema != schema {
continue
}
if alias != "" && t.Rel.Name != alias {
continue
}
for _, c := range t.Columns {
if c.Name == name {
found += 1
cname := c.Name
if res.Name != nil {
cname = *res.Name
}
cols = append(cols, &Column{
Name: cname,
Type: c.Type,
Table: c.Table,
TableAlias: alias,
DataType: c.DataType,
NotNull: c.NotNull,
Unsigned: c.Unsigned,
IsArray: c.IsArray,
ArrayDims: c.ArrayDims,
Length: c.Length,
EmbedTable: c.EmbedTable,
OriginalName: c.Name,
})
}
}
}
if found == 0 {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("column %q does not exist", name),
Location: res.Location,
}
}
if found > 1 {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("column reference %q is ambiguous", name),
Location: res.Location,
}
}
return cols, nil
}
func (c *Compiler) findColumnForNode(item ast.Node, tables []*Table, targetList *ast.List) error {
ref, ok := item.(*ast.ColumnRef)
if !ok {
return nil
}
if c.isImplicitColumnRef(ref) {
return nil
}
return findColumnForRef(ref, tables, targetList)
}
// isImplicitColumnRef reports whether ref names a column that every table in
// the dialect has without declaring it, and which therefore never appears in
// a catalog built from the schema. SQLite gives each ordinary table a rowid,
// reachable under three spellings.
func (c *Compiler) isImplicitColumnRef(ref *ast.ColumnRef) bool {
if c.conf.Engine != config.EngineSQLite {
return false
}
parts := stringSlice(ref.Fields)
if len(parts) == 0 {
return false
}
switch parts[len(parts)-1] {
case "rowid", "_rowid_", "oid":
return true
}
return false
}
func findColumnForRef(ref *ast.ColumnRef, tables []*Table, targetList *ast.List) error {
parts := stringSlice(ref.Fields)
var alias, name string
if len(parts) == 1 {
name = parts[0]
} else if len(parts) == 2 {
alias = parts[0]
name = parts[1]
}
var found int
for _, t := range tables {
if alias != "" && t.Rel.Name != alias {
continue
}
// Find matching column
for _, c := range t.Columns {
if c.Name == name {
found++
break
}
}
}
// Find matching alias if necessary
if found == 0 {
for _, c := range targetList.Items {
resTarget, ok := c.(*ast.ResTarget)
if !ok {
continue
}
if resTarget.Name != nil && *resTarget.Name == name {
found++
}
}
}
if found == 0 {
return &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("column reference %q not found", name),
Location: ref.Location,
}
}
if found > 1 {
return &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("column reference %q is ambiguous", name),
Location: ref.Location,
}
}
return nil
}

201
internal/compiler/parse.go Normal file
View File

@@ -0,0 +1,201 @@
package compiler
import (
"context"
"errors"
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/opts"
"github.com/sqlc-dev/sqlc/internal/source"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
"github.com/sqlc-dev/sqlc/internal/sql/validate"
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
)
var debugDumpAST = sqlcdebug.New("dumpast")
func (c *Compiler) parseQuery(stmt ast.Node, pp *preprocess.Result, o opts.Parser) (*Query, error) {
raw, ok := stmt.(*ast.RawStmt)
if !ok {
return nil, errors.New("node is not a statement")
}
// The preprocessor already replaced every sqlc.* construct with native SQL
// and recorded what it replaced.
pre := pp.Statement(raw.StmtLocation)
if pre.Err != nil {
return nil, pre.Err
}
// Engines number bind parameters in whatever order they convert the AST.
// Restore the numbering the preprocessor assigned in source order.
renumberParams(raw, pre.Numbers)
if c.coreCatalog != nil {
return c.parseQueryCore(raw, pp.Text, pre)
}
ctx := context.Background()
if debugDumpAST.Value() == "1" {
debug.Dump(stmt)
}
rawSQL, err := source.Pluck(pp.Text, raw.StmtLocation, raw.StmtLen)
if err != nil {
return nil, err
}
if rawSQL == "" {
return nil, errors.New("missing semicolon at end of file")
}
name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax()))
if err != nil {
return nil, err
}
if name == "" {
return nil, nil
}
if err := validate.Cmd(raw.Stmt, name, cmd); err != nil {
return nil, err
}
md := metadata.Metadata{
Name: name,
Cmd: cmd,
}
// TODO eventually can use this for name and type/cmd parsing too
cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax())
if err != nil {
return nil, err
}
md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
if err != nil {
return nil, err
}
var anlys *analysis
if c.analyzer != nil {
inference, _ := c.inferQuery(raw, rawSQL, pre)
if inference == nil {
inference = &analysis{}
}
if inference.Query == "" {
inference.Query = rawSQL
}
result, err := c.analyzer.Analyze(ctx, raw, inference.Query, c.schema, inference.Named)
if err != nil {
return nil, err
}
// If the query uses star expansion, verify that it was edited. If not,
// return an error.
stars := astutils.Search(raw, func(node ast.Node) bool {
_, ok := node.(*ast.A_Star)
return ok
})
hasStars := len(stars.Items) > 0
unchanged := inference.Query == rawSQL
if unchanged && hasStars {
return nil, fmt.Errorf("star expansion failed for query")
}
// FOOTGUN: combineAnalysis mutates inference
anlys = combineAnalysis(inference, result)
} else {
anlys, err = c.analyzeQuery(raw, rawSQL, pre)
if err != nil {
return nil, err
}
}
expanded := anlys.Query
// If the query string was edited, make sure the syntax is valid
if expanded != rawSQL {
if _, err := c.parser.Parse(strings.NewReader(expanded)); err != nil {
return nil, fmt.Errorf("edited query syntax is invalid: %w", err)
}
}
trimmed, comments, err := source.StripComments(expanded)
if err != nil {
return nil, err
}
md.Comments = comments
return &Query{
RawStmt: raw,
Metadata: md,
Params: anlys.Parameters,
Columns: anlys.Columns,
SQL: trimmed,
InsertIntoTable: anlys.Table,
}, nil
}
// renumberParams applies the parameter numbers the preprocessor assigned,
// matching each node by the location of its placeholder in the query text.
func renumberParams(raw *ast.RawStmt, numbers map[int]int) {
if len(numbers) == 0 {
return
}
astutils.Walk(astutils.VisitorFunc(func(node ast.Node) {
ref, ok := node.(*ast.ParamRef)
if !ok {
return
}
if n, ok := numbers[ref.Location]; ok {
ref.Number = n
}
}), raw)
}
func rangeVars(root ast.Node) []*ast.RangeVar {
var vars []*ast.RangeVar
find := astutils.VisitorFunc(func(node ast.Node) {
switch n := node.(type) {
case *ast.RangeVar:
vars = append(vars, n)
}
})
astutils.Walk(find, root)
return vars
}
func uniqueParamRefs(in []paramRef, dollar bool) []paramRef {
m := make(map[int]bool, len(in))
o := make([]paramRef, 0, len(in))
for _, v := range in {
if !m[v.ref.Number] {
m[v.ref.Number] = true
if v.ref.Number != 0 {
o = append(o, v)
}
}
}
if !dollar {
start := 1
for _, v := range in {
if v.ref.Number == 0 {
for m[start] {
start++
}
v.ref.Number = start
o = append(o, v)
}
}
}
return o
}

View File

@@ -0,0 +1,165 @@
package compiler
import (
"errors"
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
coreanalyzer "github.com/sqlc-dev/sqlc/internal/core/analyzer"
"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/preprocess"
"github.com/sqlc-dev/sqlc/internal/sql/validate"
)
func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess.Statement) (*Query, error) {
rawSQL, err := source.Pluck(src, raw.StmtLocation, raw.StmtLen)
if err != nil {
return nil, err
}
if strings.TrimSpace(rawSQL) == "" {
return nil, errors.New("missing semicolon at end of file")
}
name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax()))
if err != nil {
return nil, err
}
if name == "" {
return nil, nil
}
if err := validate.Cmd(raw.Stmt, name, cmd); err != nil {
return nil, err
}
md := metadata.Metadata{Name: name, Cmd: cmd}
cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax())
if err != nil {
return nil, err
}
md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
if err != nil {
return nil, err
}
if pre.ParamErr != nil {
return nil, pre.ParamErr
}
namedParams := pre.Params
expanded := rawSQL
var cols []*Column
var params []Parameter
switch raw.Stmt.(type) {
case *ast.SelectStmt, *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt:
res, err := coreanalyzer.Prepare(c.coreCatalog, raw)
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, namedParams)})
}
expanded, err = source.Mutate(rawSQL, c.expandCore(raw, res.Stars))
if err != nil {
return nil, err
}
}
// If the query string was edited, make sure the syntax is valid
if expanded != rawSQL {
if _, err := c.newParser().Parse(strings.NewReader(expanded)); err != nil {
return nil, fmt.Errorf("edited query syntax is invalid: %w", err)
}
}
trimmed, comments, err := source.StripComments(expanded)
if err != nil {
return nil, err
}
md.Comments = comments
var insertTable *ast.TableName
if ins, ok := raw.Stmt.(*ast.InsertStmt); ok {
insertTable, _ = ParseTableName(ins.Relation)
}
return &Query{
RawStmt: raw,
Metadata: md,
Params: params,
Columns: cols,
SQL: trimmed,
InsertIntoTable: insertTable,
}, nil
}
func coreColumn(c core.Column) *Column {
col := &Column{
Name: c.Name,
DataType: c.DataType,
NotNull: c.NotNull,
IsArray: c.IsArray,
TypeExpr: c.Type,
}
describeType(col, c.Type)
if c.Source != nil && c.Source.Table != "" {
col.Table = &ast.TableName{Schema: c.Source.Schema, Name: c.Source.Table}
col.TableAlias = c.Source.TableAlias
col.OriginalName = c.Source.Column
}
return col
}
// describeType fills in what codegen reads about a type from its
// expression: one array dimension per nesting, the length that is the
// innermost type's first integer argument (which is how a MySQL tinyint(1)
// is told from a tinyint), and whether the innermost type is unsigned.
func describeType(col *Column, t *core.TypeExpr) {
if t == nil {
if col.IsArray {
col.ArrayDims = 1
}
return
}
col.ArrayDims = t.ArrayDims()
inner := t.Innermost()
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
l := int(*inner.Args[0].Int)
col.Length = &l
}
col.Unsigned = strings.HasSuffix(inner.Name, " unsigned")
}
func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
col := &Column{
Name: p.Name,
DataType: p.DataType,
NotNull: p.NotNull,
IsArray: p.IsArray,
TypeExpr: p.Type,
}
describeType(col, p.Type)
if p.Source != nil && p.Source.Table != "" {
col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table}
col.OriginalName = p.Source.Column
}
if col.Name == "" && p.Source != nil {
col.Name = p.Source.Column
}
// Merge in what the user asked for: sqlc.narg() makes the parameter
// nullable and sqlc.slice() marks it as a slice, whichever way the
// analyzer typed it.
if param, isNamed := params.FetchMerge(p.Number, named.NewInferredParam(col.Name, p.NotNull)); isNamed {
col.Name = param.Name()
col.NotNull = param.NotNull()
col.IsSqlcSlice = param.IsSqlcSlice()
col.IsNamedParam = true
}
return col
}

View File

@@ -0,0 +1,67 @@
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"
)
type Function struct {
Rel *ast.FuncName
ReturnType *ast.TypeName
Outs []*catalog.Argument
}
type Table struct {
Rel *ast.TableName
Columns []*Column
}
type Column struct {
Name string
OriginalName string
DataType string
NotNull bool
Unsigned bool
IsArray bool
ArrayDims int
Comment string
Length *int
IsNamedParam bool
IsFuncCall bool
// XXX: Figure out what PostgreSQL calls `foo.id`
Scope string
Table *ast.TableName
TableAlias string
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
}
type Query struct {
SQL string
Metadata metadata.Metadata
Columns []*Column
Params []Parameter
// Needed for CopyFrom
InsertIntoTable *ast.TableName
// Needed for vet
RawStmt *ast.RawStmt
}
type Parameter struct {
Number int
Column *Column
}

View File

@@ -0,0 +1,109 @@
package compiler
import (
"fmt"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
)
type QueryCatalog struct {
catalog *catalog.Catalog
ctes map[string]*Table
embeds preprocess.EmbedSet
}
func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embeds preprocess.EmbedSet) (*QueryCatalog, error) {
var with *ast.WithClause
switch n := node.(type) {
case *ast.DeleteStmt:
with = n.WithClause
case *ast.InsertStmt:
with = n.WithClause
case *ast.UpdateStmt:
with = n.WithClause
case *ast.SelectStmt:
with = n.WithClause
default:
with = nil
}
qc := &QueryCatalog{catalog: c, ctes: map[string]*Table{}, embeds: embeds}
if with != nil {
for _, item := range with.Ctes.Items {
if cte, ok := item.(*ast.CommonTableExpr); ok {
cols, err := comp.outputColumns(qc, cte.Ctequery)
if err != nil {
return nil, err
}
var names []string
if cte.Aliascolnames != nil {
for _, item := range cte.Aliascolnames.Items {
if val, ok := item.(*ast.String); ok {
names = append(names, val.Str)
} else {
names = append(names, "")
}
}
}
rel := &ast.TableName{Name: *cte.Ctename}
for i := range cols {
cols[i].Table = rel
if len(names) > i {
cols[i].Name = names[i]
}
}
qc.ctes[*cte.Ctename] = &Table{
Rel: rel,
Columns: cols,
}
}
}
}
return qc, nil
}
func ConvertColumn(rel *ast.TableName, c *catalog.Column) *Column {
return &Column{
Table: rel,
Name: c.Name,
DataType: dataType(&c.Type),
NotNull: c.IsNotNull,
Unsigned: c.IsUnsigned,
IsArray: c.IsArray,
ArrayDims: c.ArrayDims,
Type: &c.Type,
Length: c.Length,
}
}
func (qc QueryCatalog) GetTable(rel *ast.TableName) (*Table, error) {
cte, exists := qc.ctes[rel.Name]
if exists {
return &Table{Rel: rel, Columns: cte.Columns}, nil
}
src, err := qc.catalog.GetTable(rel)
if err != nil {
return nil, err
}
var cols []*Column
for _, c := range src.Columns {
cols = append(cols, ConvertColumn(rel, c))
}
return &Table{Rel: rel, Columns: cols}, nil
}
func (qc QueryCatalog) GetFunc(rel *ast.FuncName) (*Function, error) {
funcs, err := qc.catalog.ListFuncsByName(rel)
if err != nil {
return nil, err
}
if len(funcs) == 0 {
return nil, fmt.Errorf("function not found: %s", rel.Name)
}
return &Function{
Rel: rel,
Outs: funcs[0].OutArgs(),
ReturnType: funcs[0].ReturnType,
}, nil
}

View File

@@ -0,0 +1,638 @@
package compiler
import (
"fmt"
"log/slog"
"strconv"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
"github.com/sqlc-dev/sqlc/internal/sql/named"
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
)
func dataType(n *ast.TypeName) string {
if n.Schema != "" {
return n.Schema + "." + n.Name
} else {
return n.Name
}
}
func (comp *Compiler) resolveCatalogRefs(qc *QueryCatalog, rvs []*ast.RangeVar, args []paramRef, params *named.ParamSet, embeds preprocess.EmbedSet) ([]Parameter, error) {
c := comp.catalog
aliasMap := map[string]*ast.TableName{}
// TODO: Deprecate defaultTable
var defaultTable *ast.TableName
var tables []*ast.TableName
typeMap := map[string]map[string]map[string]*catalog.Column{}
indexTable := func(table catalog.Table) error {
tables = append(tables, table.Rel)
if defaultTable == nil {
defaultTable = table.Rel
}
schema := table.Rel.Schema
if schema == "" {
schema = c.DefaultSchema
}
if _, exists := typeMap[schema]; !exists {
typeMap[schema] = map[string]map[string]*catalog.Column{}
}
typeMap[schema][table.Rel.Name] = map[string]*catalog.Column{}
for _, c := range table.Columns {
cc := c
typeMap[schema][table.Rel.Name][c.Name] = cc
}
return nil
}
for _, rv := range rvs {
if rv.Relname == nil {
continue
}
fqn, err := ParseTableName(rv)
if err != nil {
return nil, err
}
if _, found := aliasMap[fqn.Name]; found {
continue
}
table, err := c.GetTable(fqn)
if err != nil {
if qc == nil {
continue
}
// If the table name doesn't exist, first check if it's a CTE
if _, qcerr := qc.GetTable(fqn); qcerr != nil {
return nil, err
}
continue
}
err = indexTable(table)
if err != nil {
return nil, err
}
if rv.Alias != nil {
aliasMap[*rv.Alias.Aliasname] = fqn
}
}
// resolve a table for an embed
for _, embed := range embeds {
table, err := c.GetTable(embed.Table)
if err == nil {
embed.Table = table.Rel
continue
}
if alias, ok := aliasMap[embed.Table.Name]; ok {
embed.Table = alias
continue
}
return nil, fmt.Errorf("unable to resolve table with %q: %w", embed.Orig(), err)
}
var a []Parameter
addUnknownParam := func(ref paramRef) {
defaultP := named.NewInferredParam(ref.name, false)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
DataType: "any",
IsNamedParam: isNamed,
},
})
}
for _, ref := range args {
switch n := ref.parent.(type) {
case *limitOffset:
defaultP := named.NewInferredParam("offset", true)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
DataType: "integer",
NotNull: p.NotNull(),
IsNamedParam: isNamed,
},
})
case *limitCount:
defaultP := named.NewInferredParam("limit", true)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
DataType: "integer",
NotNull: p.NotNull(),
IsNamedParam: isNamed,
},
})
case *ast.A_Expr:
// TODO: While this works for a wide range of simple expressions,
// more complicated expressions will cause this logic to fail.
list := astutils.Search(n.Lexpr, func(node ast.Node) bool {
_, ok := node.(*ast.ColumnRef)
return ok
})
if len(list.Items) == 0 {
list = astutils.Search(n.Rexpr, func(node ast.Node) bool {
_, ok := node.(*ast.ColumnRef)
return ok
})
}
if len(list.Items) == 0 {
// TODO: Move this to database-specific engine package
dataType := "any"
if astutils.Join(n.Name, ".") == "||" {
dataType = "text"
}
defaultP := named.NewParam("")
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
DataType: dataType,
IsNamedParam: isNamed,
NotNull: p.NotNull(),
IsSqlcSlice: p.IsSqlcSlice(),
},
})
continue
}
switch node := list.Items[0].(type) {
case *ast.ColumnRef:
items := stringSlice(node.Fields)
var key, alias string
switch len(items) {
case 1:
key = items[0]
case 2:
alias = items[0]
key = items[1]
case 3:
// schema := items[0]
alias = items[1]
key = items[2]
default:
panic("too many field items: " + strconv.Itoa(len(items)))
}
search := tables
if alias != "" {
if original, ok := aliasMap[alias]; ok {
search = []*ast.TableName{original}
} else {
var located bool
for _, fqn := range tables {
if fqn.Name == alias {
located = true
search = []*ast.TableName{fqn}
}
}
if !located {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("table alias %q does not exist", alias),
Location: node.Location,
}
}
}
}
var found int
for _, table := range search {
schema := table.Schema
if schema == "" {
schema = c.DefaultSchema
}
if c, ok := typeMap[schema][table.Name][key]; ok {
found += 1
if ref.name != "" {
key = ref.name
}
defaultP := named.NewInferredParam(key, c.IsNotNull)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
OriginalName: c.Name,
DataType: dataType(&c.Type),
NotNull: p.NotNull(),
Unsigned: c.IsUnsigned,
IsArray: c.IsArray,
ArrayDims: c.ArrayDims,
Length: c.Length,
Table: table,
IsNamedParam: isNamed,
IsSqlcSlice: p.IsSqlcSlice(),
},
})
}
}
if found == 0 {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("column %q does not exist", key),
Location: node.Location,
}
}
if found > 1 {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("column reference %q is ambiguous", key),
Location: node.Location,
}
}
}
case *ast.BetweenExpr:
if n == nil || n.Expr == nil || n.Left == nil || n.Right == nil {
fmt.Println("ast.BetweenExpr is nil")
continue
}
var key string
if ref, ok := n.Expr.(*ast.ColumnRef); ok {
itemsCount := len(ref.Fields.Items)
if str, ok := ref.Fields.Items[itemsCount-1].(*ast.String); ok {
key = str.Str
}
}
for _, table := range tables {
schema := table.Schema
if schema == "" {
schema = c.DefaultSchema
}
if c, ok := typeMap[schema][table.Name][key]; ok {
defaultP := named.NewInferredParam(key, c.IsNotNull)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
var namePrefix string
if !isNamed {
if ref.ref == n.Left {
namePrefix = "from_"
} else if ref.ref == n.Right {
namePrefix = "to_"
}
}
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: namePrefix + p.Name(),
DataType: dataType(&c.Type),
NotNull: p.NotNull(),
Unsigned: c.IsUnsigned,
IsArray: c.IsArray,
ArrayDims: c.ArrayDims,
Table: table,
IsNamedParam: isNamed,
IsSqlcSlice: p.IsSqlcSlice(),
},
})
}
}
case *ast.FuncCall:
fun, err := c.ResolveFuncCall(n)
if err != nil {
// Synthesize a function on the fly to avoid returning with an error
// for an unknown Postgres function (e.g. defined in an extension)
var args []*catalog.Argument
for range n.Args.Items {
args = append(args, &catalog.Argument{
Type: &ast.TypeName{Name: "any"},
})
}
fun = &catalog.Function{
Name: n.Func.Name,
Args: args,
ReturnType: &ast.TypeName{Name: "any"},
}
}
var added bool
for i, item := range n.Args.Items {
funcName := fun.Name
var argName string
switch inode := item.(type) {
case *ast.ParamRef:
if inode.Number != ref.ref.Number {
continue
}
case *ast.TypeCast:
pr, ok := inode.Arg.(*ast.ParamRef)
if !ok {
continue
}
if pr.Number != ref.ref.Number {
continue
}
case *ast.NamedArgExpr:
pr, ok := inode.Arg.(*ast.ParamRef)
if !ok {
continue
}
if pr.Number != ref.ref.Number {
continue
}
if inode.Name != nil {
argName = *inode.Name
}
default:
continue
}
if fun.Args == nil {
defaultName := funcName
if argName != "" {
defaultName = argName
}
defaultP := named.NewInferredParam(defaultName, false)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
added = true
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
DataType: "any",
IsNamedParam: isNamed,
NotNull: p.NotNull(),
IsSqlcSlice: p.IsSqlcSlice(),
},
})
continue
}
var paramName string
var paramType *ast.TypeName
if argName == "" {
if i < len(fun.Args) {
paramName = fun.Args[i].Name
paramType = fun.Args[i].Type
}
} else {
paramName = argName
for _, arg := range fun.Args {
if arg.Name == argName {
paramType = arg.Type
}
}
if paramType == nil {
panic(fmt.Sprintf("named argument %s has no type", paramName))
}
}
if paramName == "" {
paramName = funcName
}
if paramType == nil {
paramType = &ast.TypeName{Name: ""}
}
defaultP := named.NewInferredParam(paramName, true)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
added = true
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
DataType: dataType(paramType),
NotNull: p.NotNull(),
IsNamedParam: isNamed,
IsSqlcSlice: p.IsSqlcSlice(),
},
})
}
if fun.ReturnType == nil {
if !added {
addUnknownParam(ref)
}
continue
}
table, err := c.GetTable(&ast.TableName{
Catalog: fun.ReturnType.Catalog,
Schema: fun.ReturnType.Schema,
Name: fun.ReturnType.Name,
})
if err != nil {
if !added {
addUnknownParam(ref)
}
continue
}
err = indexTable(table)
if err != nil {
return nil, err
}
case *ast.ResTarget:
if n.Name == nil {
return nil, fmt.Errorf("*ast.ResTarget has nil name")
}
key := *n.Name
var schema, rel string
// TODO: Deprecate defaultTable
if defaultTable != nil {
schema = defaultTable.Schema
rel = defaultTable.Name
}
if ref.rv != nil {
fqn, err := ParseTableName(ref.rv)
if err != nil {
return nil, err
}
schema = fqn.Schema
rel = fqn.Name
}
if schema == "" {
schema = c.DefaultSchema
}
tableMap, ok := typeMap[schema][rel]
if !ok {
return nil, sqlerr.RelationNotFound(rel)
}
if c, ok := tableMap[key]; ok {
defaultP := named.NewInferredParam(key, c.IsNotNull)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
a = append(a, Parameter{
Number: ref.ref.Number,
Column: &Column{
Name: p.Name(),
OriginalName: c.Name,
DataType: dataType(&c.Type),
NotNull: p.NotNull(),
Unsigned: c.IsUnsigned,
IsArray: c.IsArray,
ArrayDims: c.ArrayDims,
Table: &ast.TableName{Schema: schema, Name: rel},
Length: c.Length,
IsNamedParam: isNamed,
IsSqlcSlice: p.IsSqlcSlice(),
},
})
} else {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("column %q does not exist", key),
Location: n.Location,
}
}
case *ast.TypeCast:
if n.TypeName == nil {
return nil, fmt.Errorf("*ast.TypeCast has nil type name")
}
col := toColumn(n.TypeName)
defaultP := named.NewInferredParam(col.Name, col.NotNull)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
col.Name = p.Name()
col.NotNull = p.NotNull()
col.IsNamedParam = isNamed
a = append(a, Parameter{
Number: ref.ref.Number,
Column: col,
})
case *ast.ParamRef:
a = append(a, Parameter{Number: ref.ref.Number})
case *ast.In:
if n == nil || n.List == nil {
fmt.Println("ast.In is nil")
continue
}
number := 0
if pr, ok := n.List[0].(*ast.ParamRef); ok {
number = pr.Number
}
location := 0
var key, alias string
var items []string
if left, ok := n.Expr.(*ast.ColumnRef); ok {
location = left.Location
items = stringSlice(left.Fields)
} else if left, ok := n.Expr.(*ast.ParamRef); ok {
if len(n.List) <= 0 {
continue
}
if right, ok := n.List[0].(*ast.ColumnRef); ok {
location = left.Location
items = stringSlice(right.Fields)
} else {
continue
}
} else {
continue
}
switch len(items) {
case 1:
key = items[0]
case 2:
alias = items[0]
key = items[1]
default:
panic("too many field items: " + strconv.Itoa(len(items)))
}
var found int
if n.Sel == nil {
search := tables
if alias != "" {
if original, ok := aliasMap[alias]; ok {
search = []*ast.TableName{original}
} else {
for _, fqn := range tables {
if fqn.Name == alias {
search = []*ast.TableName{fqn}
}
}
}
}
for _, table := range search {
schema := table.Schema
if schema == "" {
schema = c.DefaultSchema
}
if c, ok := typeMap[schema][table.Name][key]; ok {
found += 1
if ref.name != "" {
key = ref.name
}
defaultP := named.NewInferredParam(key, c.IsNotNull)
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
a = append(a, Parameter{
Number: number,
Column: &Column{
Name: p.Name(),
OriginalName: c.Name,
DataType: dataType(&c.Type),
NotNull: c.IsNotNull,
Unsigned: c.IsUnsigned,
IsArray: c.IsArray,
ArrayDims: c.ArrayDims,
Table: table,
IsNamedParam: isNamed,
IsSqlcSlice: p.IsSqlcSlice(),
},
})
}
}
}
if found == 0 {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("396: column %q does not exist", key),
Location: location,
}
}
if found > 1 {
return nil, &sqlerr.Error{
Code: "42703",
Message: fmt.Sprintf("in same name column reference %q is ambiguous", key),
Location: location,
}
}
default:
slog.Debug("unsupported reference type", "type", fmt.Sprintf("%T", n))
addUnknownParam(ref)
}
}
return a, nil
}

View File

@@ -0,0 +1,10 @@
package compiler
import (
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
)
type Result struct {
Catalog *catalog.Catalog
Queries []*Query
}

View File

@@ -0,0 +1,138 @@
package compiler
import (
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
// returningTables builds virtual tables for the OLD and NEW aliases that
// PostgreSQL 18 makes available in the RETURNING clause of INSERT, UPDATE and
// DELETE statements. Each alias exposes the columns of the statement's target
// table. For INSERT there is usually no old row and for DELETE there is no
// new row, so every column reached through those aliases becomes nullable.
func (c *Compiler) returningTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
if c.conf.Engine != config.EnginePostgreSQL {
return nil, nil
}
var rv *ast.RangeVar
var returning *ast.List
oldAlias, newAlias := "old", "new"
var oldNullable, newNullable bool
switch n := node.(type) {
case *ast.DeleteStmt:
rv = firstRangeVar(n.Relations)
returning = n.ReturningList
if n.ReturningOldAlias != "" {
oldAlias = n.ReturningOldAlias
}
if n.ReturningNewAlias != "" {
newAlias = n.ReturningNewAlias
}
// A deleted row has no new value
newNullable = true
case *ast.InsertStmt:
rv = n.Relation
returning = n.ReturningList
if n.ReturningOldAlias != "" {
oldAlias = n.ReturningOldAlias
}
if n.ReturningNewAlias != "" {
newAlias = n.ReturningNewAlias
}
// An inserted row has no old value, except when an ON CONFLICT
// clause updates an existing row instead
oldNullable = true
case *ast.UpdateStmt:
rv = firstRangeVar(n.Relations)
returning = n.ReturningList
if n.ReturningOldAlias != "" {
oldAlias = n.ReturningOldAlias
}
if n.ReturningNewAlias != "" {
newAlias = n.ReturningNewAlias
}
default:
return nil, nil
}
if rv == nil || returning == nil || len(returning.Items) == 0 {
return nil, nil
}
fqn, err := ParseTableName(rv)
if err != nil {
return nil, err
}
build := func(alias string, nullable bool) *Table {
table, err := qc.GetTable(fqn)
if err != nil {
// An unresolvable target table is reported by the regular
// source table lookup, so ignore the error here
return nil
}
table.Rel = &ast.TableName{Name: alias}
if nullable {
for _, col := range table.Columns {
col.NotNull = false
}
}
return table
}
var tables []*Table
if t := build(oldAlias, oldNullable); t != nil {
tables = append(tables, t)
}
if t := build(newAlias, newNullable); t != nil {
tables = append(tables, t)
}
return tables, nil
}
func firstRangeVar(list *ast.List) *ast.RangeVar {
if list == nil {
return nil
}
for _, item := range list.Items {
if rv, ok := item.(*ast.RangeVar); ok && rv != nil {
return rv
}
}
return nil
}
// returningTableForScope returns the OLD or NEW virtual table named by scope.
// A source table with the same name shadows the virtual table, matching
// PostgreSQL, where the implicit aliases are only available when no relation
// in the query is already known under that name.
func returningTableForScope(tables, rtables []*Table, scope string) *Table {
if scope == "" {
return nil
}
for _, t := range tables {
if t.Rel.Name == scope {
return nil
}
}
for _, t := range rtables {
if t.Rel.Name == scope {
return t
}
}
return nil
}
// tablesForRef resolves a column reference against the source tables,
// extended with the OLD or NEW virtual table when the reference is qualified
// with one of their names.
func tablesForRef(ref *ast.ColumnRef, tables, rtables []*Table) []*Table {
parts := stringSlice(ref.Fields)
if len(parts) != 2 {
return tables
}
if vt := returningTableForScope(tables, rtables, parts[0]); vt != nil {
return append(append([]*Table{}, tables...), vt)
}
return tables
}

View File

@@ -0,0 +1,48 @@
package compiler
import "strings"
// selector is an interface used by a compiler for generating expressions for
// output columns in a `SELECT ...` or `RETURNING ...` statement.
//
// This interface is exclusively needed at the moment for SQLite, which must
// wrap output `jsonb` columns with a `json(column_name)` invocation so that a
// publicly consumable format (i.e. not jsonb) is returned.
type selector interface {
// ColumnExpr generates output to be used in a `SELECT ...` or `RETURNING
// ...` statement based on input column name and metadata.
ColumnExpr(name string, column *Column) string
}
// defaultSelector is a selector implementation that does the simpliest possible
// pass through when generating column expressions. Its use is suitable for all
// database engines not requiring additional customization.
type defaultSelector struct{}
func newDefaultSelector() *defaultSelector {
return &defaultSelector{}
}
func (s *defaultSelector) ColumnExpr(name string, column *Column) string {
return name
}
type sqliteSelector struct{}
func newSQLiteSelector() *sqliteSelector {
return &sqliteSelector{}
}
func (s *sqliteSelector) ColumnExpr(name string, column *Column) string {
// Under SQLite, neither json nor jsonb are real data types, and rather just
// of type blob, so database drivers just return whatever raw binary is
// stored as values. This is a problem for jsonb, which is considered an
// internal format to SQLite and no attempt should be made to parse it
// outside of the database itself. For jsonb columns in SQLite, wrap values
// in `json(col)` to coerce the internal binary format to JSON parsable by
// the user-space application.
if strings.EqualFold(column.DataType, "jsonb") {
return "json(" + name + ")"
}
return name
}

View File

@@ -0,0 +1,35 @@
package compiler
import "testing"
func TestSelector(t *testing.T) {
t.Parallel()
selectorExpectColumnExpr := func(t *testing.T, selector selector, expected, name string, column *Column) {
if actual := selector.ColumnExpr(name, column); expected != actual {
t.Errorf("Expected %v, got %v for data type %v", expected, actual, column.DataType)
}
}
t.Run("DefaultSelectorColumnExpr", func(t *testing.T) {
t.Parallel()
selector := newDefaultSelector()
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "jsonb"})
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
})
t.Run("SQLiteSelectorColumnExpr", func(t *testing.T) {
t.Parallel()
selector := newSQLiteSelector()
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
selectorExpectColumnExpr(t, selector, "json(my_column)", "my_column", &Column{DataType: "jsonb"})
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
})
}

View File

@@ -0,0 +1,33 @@
package compiler
import (
"strings"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
)
func arrayDims(n *ast.TypeName) int {
if n == nil || n.ArrayBounds == nil {
return 0
}
return len(n.ArrayBounds.Items)
}
func toColumn(n *ast.TypeName) *Column {
if n == nil {
panic("can't build column for nil type name")
}
typ, err := ParseTypeName(n)
if err != nil {
panic("toColumn: " + err.Error())
}
arrayDims := arrayDims(n)
return &Column{
Type: typ,
DataType: strings.TrimPrefix(astutils.Join(n.Names, "."), "."),
NotNull: true, // XXX: How do we know if this should be null?
IsArray: arrayDims > 0,
ArrayDims: arrayDims,
}
}