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

82
internal/core/analysis.go Normal file
View File

@@ -0,0 +1,82 @@
package core
type Command string
const (
CommandSelect Command = "SELECT"
CommandInsert Command = "INSERT"
CommandUpdate Command = "UPDATE"
CommandDelete Command = "DELETE"
)
type PrepareResult struct {
Command Command `json:"command,omitempty"`
Columns []Column `json:"columns"`
Parameters []Parameter `json:"parameters"`
Stars []StarExpansion `json:"stars,omitempty"`
}
// StarExpansion is what a star in a target list stands for. The analyzer
// resolves the reference against the query's scope and reports the columns it
// covers; rewriting the query text with them is the caller's to do, since only
// it knows how the engine quotes an identifier.
type StarExpansion struct {
// Location is where the target the star belongs to starts, measured the
// way the AST measures a node: from the beginning of the file the
// statement was parsed from.
Location int `json:"location"`
// Fields is the reference as it was written, with the star as its last
// element: ["*"] for a bare star and ["foo", "*"] for a qualified one.
Fields []string `json:"fields"`
// Alias is the output name the target was given, if any.
Alias string `json:"alias,omitempty"`
Columns []StarColumn `json:"columns"`
}
// StarColumn is a single column a star expanded to.
type StarColumn struct {
// Relation is the name the column's relation goes by in the query, which
// is its alias when it was given one.
Relation string `json:"relation,omitempty"`
Name string `json:"name"`
DataType string `json:"data_type,omitempty"`
}
type ColumnSource struct {
Schema string `json:"schema,omitempty"`
Table string `json:"table,omitempty"`
TableAlias string `json:"table_alias,omitempty"`
Column string `json:"column,omitempty"`
}
type Column struct {
Name string `json:"name"`
DataType string `json:"data_type"`
// Type is the column's type as an expression, carrying what DataType
// and IsArray flatten away: arguments, nesting and inner nullability.
Type *TypeExpr `json:"type,omitempty"`
TypeOID int64 `json:"type_oid,omitempty"`
NotNull bool `json:"not_null"`
IsArray bool `json:"is_array,omitempty"`
SourceClassOID int64 `json:"source_class_oid,omitempty"`
SourceAttributeOID int64 `json:"source_attribute_oid,omitempty"`
Source *ColumnSource `json:"source,omitempty"`
DeclType string `json:"decl_type,omitempty"`
IsPrimaryKey bool `json:"is_primary_key,omitempty"`
IsUnique bool `json:"is_unique,omitempty"`
IsAutoIncrement bool `json:"is_auto_increment,omitempty"`
}
type Parameter struct {
Number int `json:"number"`
Name string `json:"name,omitempty"`
DataType string `json:"data_type,omitempty"`
Type *TypeExpr `json:"type,omitempty"`
TypeOID int64 `json:"type_oid,omitempty"`
NotNull bool `json:"not_null"`
IsArray bool `json:"is_array,omitempty"`
Source *ColumnSource `json:"source,omitempty"`
}

View File

@@ -0,0 +1,389 @@
package analyzer
import (
"fmt"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) {
if rs, ok := stmt.(*ast.RawStmt); ok {
stmt = rs.Stmt
}
a := &analyzer{
cat: cat,
params: map[int]core.Parameter{},
stars: &[]core.StarExpansion{},
}
switch s := stmt.(type) {
case *ast.SelectStmt:
if err := a.analyzeSelect(s); err != nil {
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)
}
return a.result(), nil
}
type analyzer struct {
cat *core.Catalog
scope *scope
columns []core.Column
params map[int]core.Parameter
command core.Command
// outer is the scope of the query this one is nested in, which a
// correlated subquery refers to.
outer *scope
// ctes are the relations a WITH clause defined, visible to this query and
// to the ones nested in it.
ctes map[string]scopeRel
// aliases are the names the target list gives its expressions. GROUP BY,
// HAVING and ORDER BY may refer to a result column by its output name.
aliases map[string]ast.Node
// resolving guards against an alias that refers to itself.
resolving map[string]bool
// stars are the expansions every star in the statement asked for, shared
// with the analyzers of the queries nested in it so one statement reports
// all of them.
stars *[]core.StarExpansion
}
func (a *analyzer) recordStar(s core.StarExpansion) {
if a.stars == nil {
return
}
*a.stars = append(*a.stars, s)
}
// subquery analyzes a nested SELECT. It shares the parameter set, so a
// placeholder inside the subquery is reported with the rest, and it sees this
// query's scope, so a correlated reference resolves.
func (a *analyzer) subquery(s *ast.SelectStmt) (*analyzer, error) {
sub := &analyzer{
cat: a.cat,
params: a.params,
outer: a.scope,
ctes: a.ctes,
stars: a.stars,
}
if err := sub.analyzeSelect(s); err != nil {
return nil, err
}
return sub, nil
}
func (a *analyzer) subqueryColumns(s *ast.SelectStmt) ([]core.Column, error) {
sub, err := a.subquery(s)
if err != nil {
return nil, err
}
return sub.columns, nil
}
// derivedRel turns a subquery's result columns into a relation, so that the
// query selecting from it resolves columns the same way as from a table.
func derivedRel(alias string, cols []core.Column) scopeRel {
rel := scopeRel{alias: alias, cols: make([]core.ClassColumn, 0, len(cols))}
for _, col := range cols {
rel.cols = append(rel.cols, core.ClassColumn{
AttOID: col.SourceAttributeOID,
Name: col.Name,
TypeOID: col.TypeOID,
Type: col.Type.WithNullable(false),
NotNull: col.NotNull,
})
}
return rel
}
func (a *analyzer) result() core.PrepareResult {
// A placeholder nothing constrained takes the dialect's type for one, when
// the dialect has such a type.
if oid, ok := a.cat.UntypedTypeOID(); ok {
for n, p := range a.params {
if p.TypeOID == 0 && p.Type == nil {
t := exprType{typeOID: oid, nullable: true}
p.TypeOID = oid
p.DataType, p.IsArray = a.typeNameOf(t)
p.NotNull = false
p.Type = a.typeExprOf(t)
a.params[n] = p
}
}
}
res := core.PrepareResult{
Command: a.command,
Columns: a.columns,
Parameters: orderedParams(a.params),
}
if a.stars != nil {
res.Stars = *a.stars
}
return res
}
func orderedParams(m map[int]core.Parameter) []core.Parameter {
if len(m) == 0 {
return nil
}
maxN := 0
for n := range m {
if n > maxN {
maxN = n
}
}
out := make([]core.Parameter, 0, len(m))
for i := 1; i <= maxN; i++ {
if p, ok := m[i]; ok {
out = append(out, p)
}
}
return out
}
func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
if err := a.bindCTEs(s.WithClause); err != nil {
return err
}
// A set operation reports the columns of its first branch. The other
// branches still get analyzed, for the placeholders they hold.
if s.Larg != nil && s.Rarg != nil {
if err := a.analyzeSetOperation(s); err != nil {
return err
}
return nil
}
sc, err := a.buildScope(s.FromClause)
if err != nil {
return err
}
a.scope = sc
a.bindAliases(s.TargetList)
for _, item := range listItems(s.FromClause) {
if err := a.typeJoinConditions(item); err != nil {
return fmt.Errorf("join: %w", err)
}
}
if s.WhereClause != nil {
if _, err := a.typeExpr(s.WhereClause); err != nil {
return fmt.Errorf("where: %w", err)
}
}
if items := listItems(s.GroupClause); items != nil {
for _, g := range items {
if _, err := a.typeExpr(g); err != nil {
return fmt.Errorf("group by: %w", err)
}
}
}
if s.HavingClause != nil {
if _, err := a.typeExpr(s.HavingClause); err != nil {
return fmt.Errorf("having: %w", err)
}
}
targets := listItems(s.TargetList)
if targets == nil {
// A VALUES list produces rows without naming columns.
if s.ValuesLists != nil {
return a.typeValuesLists(s.ValuesLists)
}
return fmt.Errorf("select: empty target list")
}
a.columns = make([]core.Column, 0, len(targets))
for _, t := range targets {
rt, ok := t.(*ast.ResTarget)
if !ok {
continue
}
if err := a.projectTarget(rt); err != nil {
return err
}
}
for _, item := range listItems(s.SortClause) {
if sb, ok := item.(*ast.SortBy); ok {
if _, err := a.typeExpr(sb.Node); err != nil {
return fmt.Errorf("order by: %w", err)
}
}
}
for _, n := range []ast.Node{s.LimitCount, s.LimitOffset} {
if err := a.typeLimit(n); err != nil {
return fmt.Errorf("limit: %w", err)
}
}
return nil
}
// typeLimit types a LIMIT or OFFSET count. A bare placeholder there holds
// whatever the dialect counts rows in.
func (a *analyzer) typeLimit(n ast.Node) error {
if n == nil {
return nil
}
if pr, ok := n.(*ast.ParamRef); ok {
oid, err := a.cat.LimitTypeOID()
if err != nil {
return err
}
a.inferParam(pr.Number, exprType{typeOID: oid})
return nil
}
_, err := a.typeExpr(n)
return err
}
func (a *analyzer) typeValuesLists(l *ast.List) error {
for _, row := range listItems(l) {
if _, err := a.typeExpr(row); err != nil {
return err
}
}
return nil
}
// analyzeSetOperation types both sides of UNION, INTERSECT or EXCEPT and
// reports the first branch's columns, the way the databases name the result.
func (a *analyzer) analyzeSetOperation(s *ast.SelectStmt) error {
left, err := a.subquery(s.Larg)
if err != nil {
return err
}
if _, err := a.subquery(s.Rarg); err != nil {
return err
}
a.columns = left.columns
a.scope = left.scope
return nil
}
// bindCTEs analyzes a WITH clause, making each common table expression
// available to the query as a relation.
func (a *analyzer) bindCTEs(with *ast.WithClause) error {
if with == nil {
return nil
}
for _, item := range listItems(with.Ctes) {
cte, ok := item.(*ast.CommonTableExpr)
if !ok || cte.Ctename == nil {
continue
}
sel, ok := cte.Ctequery.(*ast.SelectStmt)
if !ok {
continue
}
cols, err := a.subqueryColumns(sel)
if err != nil {
return fmt.Errorf("with %s: %w", *cte.Ctename, err)
}
rel := derivedRel(*cte.Ctename, cols)
renameColumns(&rel, cte.Aliascolnames)
if a.ctes == nil {
a.ctes = map[string]scopeRel{}
}
a.ctes[*cte.Ctename] = rel
}
return nil
}
// bindAliases records the output names the target list assigns, so a later
// clause can refer to a result column by name.
func (a *analyzer) bindAliases(targets *ast.List) {
for _, item := range listItems(targets) {
rt, ok := item.(*ast.ResTarget)
if !ok || rt.Name == nil || *rt.Name == "" {
continue
}
if a.aliases == nil {
a.aliases = map[string]ast.Node{}
}
a.aliases[*rt.Name] = rt.Val
}
}
// typeAlias types the expression an output name stands for, which is how a
// GROUP BY or HAVING clause refers to a result column.
func (a *analyzer) typeAlias(name string) (exprType, bool, error) {
val, ok := a.aliases[name]
if !ok || a.resolving[name] {
return exprType{}, false, nil
}
if a.resolving == nil {
a.resolving = map[string]bool{}
}
a.resolving[name] = true
defer delete(a.resolving, name)
t, err := a.typeExpr(val)
if err != nil {
return exprType{}, false, err
}
return t, true, nil
}
// renameColumns applies the column aliases a derived relation was declared
// with, as in "WITH t(a, b) AS (...)".
func renameColumns(rel *scopeRel, names *ast.List) {
for i, item := range listItems(names) {
if i >= len(rel.cols) {
break
}
if s, ok := item.(*ast.String); ok && s.Str != "" {
rel.cols[i].Name = s.Str
}
}
}
func listItems(l *ast.List) []ast.Node {
if l == nil {
return nil
}
return l.Items
}
func (a *analyzer) typeJoinConditions(item ast.Node) error {
je, ok := item.(*ast.JoinExpr)
if !ok {
return nil
}
if err := a.typeJoinConditions(je.Larg); err != nil {
return err
}
if err := a.typeJoinConditions(je.Rarg); err != nil {
return err
}
if je.Quals != nil {
if _, err := a.typeExpr(je.Quals); err != nil {
return fmt.Errorf("ON: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,110 @@
package analyzer_test
import (
"strings"
"testing"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/core/analyzer"
coreschema "github.com/sqlc-dev/sqlc/internal/core/schema"
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
const benchSchema = `
CREATE TABLE users (
id INT64 NOT NULL,
name STRING NOT NULL,
bio STRING,
) PRIMARY KEY (id);
CREATE TABLE posts (
id INT64 NOT NULL,
user_id INT64 NOT NULL,
title STRING(255),
created TIMESTAMP NOT NULL,
) PRIMARY KEY (id);
`
const benchQueries = `
SELECT * FROM users;
SELECT COUNT(*) AS total FROM users;
SELECT u.name, p.* FROM users u JOIN posts p ON p.user_id = u.id WHERE u.name = @name;
SELECT id, name, bio FROM users WHERE id = @id AND name = @name;
SELECT p.title, p.created FROM posts p WHERE p.user_id = @uid AND p.title = @t;
`
func parseAll(t testing.TB, src string) []ast.Node {
p := googlesql.NewParser()
stmts, err := p.Parse(strings.NewReader(src))
if err != nil {
t.Fatal(err)
}
out := make([]ast.Node, 0, len(stmts))
for i := range stmts {
out = append(out, stmts[i].Raw)
}
return out
}
// parseQueries mirrors the compiler pipeline. GoogleSQL is not preprocessed —
// "@name" is native syntax the converter turns into a ParamRef — so parsing is
// all it takes.
func parseQueries(t testing.TB, src string) []ast.Node {
return parseAll(t, src)
}
func newBenchCatalog(t testing.TB) (*core.Catalog, []ast.Node) {
cat, err := core.New(googlesql.Dialect())
if err != nil {
t.Fatal(err)
}
for _, n := range parseAll(t, benchSchema) {
if err := coreschema.Apply(cat, n); err != nil {
t.Fatal(err)
}
}
return cat, parseQueries(t, benchQueries)
}
// BenchmarkCatalogNew measures per-compile catalog setup: open the SQLite
// catalog, install the schema, and run the dialect seed.
func BenchmarkCatalogNew(b *testing.B) {
for b.Loop() {
cat, err := core.New(googlesql.Dialect())
if err != nil {
b.Fatal(err)
}
cat.Close()
}
}
// BenchmarkApplySchema measures loading user DDL into the catalog.
func BenchmarkApplySchema(b *testing.B) {
stmts := parseAll(b, benchSchema)
for b.Loop() {
cat, err := core.New(googlesql.Dialect())
if err != nil {
b.Fatal(err)
}
for _, n := range stmts {
if err := coreschema.Apply(cat, n); err != nil {
b.Fatal(err)
}
}
cat.Close()
}
}
// BenchmarkPrepare measures steady-state query analysis against a warm catalog.
func BenchmarkPrepare(b *testing.B) {
cat, queries := newBenchCatalog(b)
defer cat.Close()
for b.Loop() {
for _, q := range queries {
if _, err := analyzer.Prepare(cat, q); err != nil {
b.Fatal(err)
}
}
}
}

View File

@@ -0,0 +1,211 @@
package analyzer
import (
"fmt"
"github.com/sqlc-dev/sqlc/internal/core"
"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 {
if err := a.bindCTEs(s.WithClause); err != nil {
return err
}
sc, err := a.relationScope(s.Relations, s.FromClause, nil)
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 {
if err := a.bindCTEs(s.WithClause); err != nil {
return err
}
sc, err := a.relationScope(s.Relations, s.UsingClause, s.FromClause)
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)
}
// relationScope builds the scope a DML statement operates on: the relations it
// targets, whatever a USING or FROM clause joins in, and — for the engines that
// report a multi-table DELETE that way — a single FROM node.
func (a *analyzer) relationScope(relations, extra *ast.List, from ast.Node) (*scope, error) {
sc := &scope{}
defer a.binding(sc)()
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 from != nil {
if err := a.appendFromItem(sc, from); 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) ([]core.ClassColumn, error) {
items := listItems(cols)
if len(items) == 0 {
out := make([]core.ClassColumn, 0, len(rel.cols))
for _, col := range rel.cols {
if !col.Hidden {
out = append(out, col)
}
}
return out, nil
}
out := make([]core.ClassColumn, 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 []core.ClassColumn) error {
if n == nil {
return nil
}
sel, ok := n.(*ast.SelectStmt)
if !ok {
return fmt.Errorf("insert: unsupported source %T", n)
}
// INSERT ... SELECT inserts whatever the query returns. The rows are not
// the statement's result, but the query still holds placeholders.
if sel.ValuesLists == nil {
_, err := a.subqueryColumns(sel)
return err
}
for _, row := range listItems(sel.ValuesLists) {
values, ok := row.(*ast.List)
if !ok {
continue
}
for i, v := range values.Items {
var target *core.ClassColumn
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 *core.ClassColumn, 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) (core.ClassColumn, bool) {
for _, col := range rel.cols {
if col.Name == name {
return col, true
}
}
return core.ClassColumn{}, false
}
func columnType(rel scopeRel, col core.ClassColumn) exprType {
return exprType{
typeOID: col.TypeOID,
expr: col.Type,
nullable: !col.NotNull,
sourceClassOID: rel.classOID,
sourceAttributeOID: col.AttOID,
sourceTableAlias: rel.alias,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
package analyzer
import (
"slices"
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
func (a *analyzer) projectTarget(rt *ast.ResTarget) error {
// A column reference's field list is flattened once here and threaded
// through the star check, the star expansion and the output name.
var fields []string
if cr, ok := rt.Val.(*ast.ColumnRef); ok {
fields = flattenFields(cr.Fields)
if isStar(fields) {
a.emitStar(rt, fields)
return nil
}
}
t, err := a.typeExpr(rt.Val)
if err != nil {
return err
}
// A placeholder selected directly is named by its alias and, when nothing
// constrains it, typed as the dialect types such a placeholder.
if pr, ok := rt.Val.(*ast.ParamRef); ok {
if rt.Name != nil && *rt.Name != "" {
if p := a.params[pr.Number]; p.Name == "" {
p.Name = *rt.Name
a.params[pr.Number] = p
}
}
if t.typeOID == 0 && t.expr == nil {
if oid, ok := a.cat.UntypedTypeOID(); ok {
t = exprType{typeOID: oid, nullable: true}
}
}
}
col := core.Column{
Name: targetName(rt, fields),
TypeOID: t.typeOID,
NotNull: !t.nullable,
SourceClassOID: t.sourceClassOID,
SourceAttributeOID: t.sourceAttributeOID,
}
col.DataType, col.IsArray = a.typeNameOf(t)
col.Type = a.typeExprOf(t)
a.decorateSource(&col, t.sourceAttributeOID, t.sourceTableAlias)
if rt.Name == nil || *rt.Name == "" {
// A column whose own name is dotted, as ClickHouse's n.a is, is
// reported under that name rather than its last part.
if col.Source != nil && strings.Contains(col.Source.Column, ".") {
col.Name = col.Source.Column
}
a.qualifyDuplicate(&col, t.sourceTableAlias)
}
a.columns = append(a.columns, col)
return nil
}
// qualifyDuplicate names a column after its relation when an earlier result
// column from another relation already has its name, in a dialect that
// does so.
func (a *analyzer) qualifyDuplicate(col *core.Column, alias string) {
if alias == "" || !a.cat.QualifiesDuplicateColumns() {
return
}
for _, prev := range a.columns {
if prev.Name != col.Name {
continue
}
prevAlias := ""
if prev.Source != nil {
prevAlias = prev.Source.TableAlias
}
if prevAlias != alias {
col.Name = alias + "." + col.Name
return
}
}
}
func (a *analyzer) decorateSource(col *core.Column, attOID int64, tableAlias string) {
if attOID == 0 {
return
}
ad, err := a.cat.LookupAttribute(attOID)
if err != nil {
return
}
col.Source = &core.ColumnSource{
Schema: ad.Schema,
Table: ad.Table,
TableAlias: tableAlias,
Column: ad.Column,
}
col.DeclType = ad.DeclType
col.IsPrimaryKey = ad.IsPrimaryKey
col.IsUnique = ad.IsUnique
col.IsAutoIncrement = ad.AutoIncrement
}
// targetName picks the output name for a target. fields is the already
// flattened field list when rt.Val is a column reference, and nil otherwise.
func targetName(rt *ast.ResTarget, fields []string) string {
if rt.Name != nil && *rt.Name != "" {
return *rt.Name
}
if len(fields) > 0 {
return fields[len(fields)-1]
}
if fc, ok := rt.Val.(*ast.FuncCall); ok {
if name := funcCallName(fc); name != "" {
return name
}
}
return "?column?"
}
func isStar(fields []string) bool {
return len(fields) > 0 && fields[len(fields)-1] == "*"
}
func (a *analyzer) emitStar(rt *ast.ResTarget, fields []string) {
relName := ""
if len(fields) > 1 {
relName = fields[0]
}
// The star is reported along with the columns it covers, so the query text
// can be rewritten to name them.
star := core.StarExpansion{Location: rt.Location, Fields: fields}
if rt.Name != nil {
star.Alias = *rt.Name
}
for _, rel := range a.scope.rels {
if relName != "" && rel.alias != relName {
continue
}
a.columns = slices.Grow(a.columns, len(rel.cols))
star.Columns = slices.Grow(star.Columns, len(rel.cols))
for _, c := range rel.cols {
if c.Hidden {
continue
}
col := core.Column{
Name: c.Name,
TypeOID: c.TypeOID,
NotNull: c.NotNull,
SourceClassOID: rel.classOID,
SourceAttributeOID: c.AttOID,
}
t := exprType{typeOID: c.TypeOID, expr: c.Type, nullable: !c.NotNull}
col.DataType, col.IsArray = a.typeNameOf(t)
col.Type = a.typeExprOf(t)
a.decorateSource(&col, c.AttOID, rel.alias)
a.qualifyDuplicate(&col, rel.alias)
a.columns = append(a.columns, col)
star.Columns = append(star.Columns, core.StarColumn{
Relation: rel.alias,
Name: c.Name,
DataType: col.DataType,
})
}
}
a.recordStar(star)
}

View File

@@ -0,0 +1,243 @@
package analyzer
import (
"fmt"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
type scope struct {
rels []scopeRel
}
type scopeRel struct {
alias string
classOID int64
// cols is the catalog's column list, held as-is rather than copied into a
// scope-local column type.
cols []core.ClassColumn
}
func (a *analyzer) buildScope(from *ast.List) (*scope, error) {
items := listItems(from)
sc := &scope{rels: make([]scopeRel, 0, len(items))}
defer a.binding(sc)()
for _, item := range items {
if err := a.appendFromItem(sc, item); err != nil {
return nil, err
}
}
return sc, nil
}
// binding makes the scope under construction the one the analyzer resolves
// against, and returns the func that puts back the scope it replaced. A FROM
// item can refer to the ones before it — a set-returning function takes its
// arguments from them — so binding an item has to see what is bound so far
// rather than no scope at all.
func (a *analyzer) binding(sc *scope) func() {
prev := a.scope
a.scope = sc
return func() { a.scope = prev }
}
func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error {
switch v := item.(type) {
case *ast.RangeVar:
rel, err := a.bindRangeVar(v)
if err != nil {
return err
}
sc.rels = append(sc.rels, rel)
return nil
case *ast.JoinExpr:
if err := a.appendFromItem(sc, v.Larg); err != nil {
return err
}
return a.appendFromItem(sc, v.Rarg)
case *ast.RangeFunction:
rel, err := a.bindRangeFunction(v)
if err != nil {
return err
}
sc.rels = append(sc.rels, rel)
return nil
case *ast.RangeSubselect:
rel, err := a.bindRangeSubselect(v)
if err != nil {
return err
}
sc.rels = append(sc.rels, rel)
return nil
case *ast.List:
// Some engines report a comma-separated FROM as a nested list.
for _, item := range listItems(v) {
if err := a.appendFromItem(sc, item); err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("scope: unsupported FROM item %T", item)
}
}
// bindRangeFunction binds a function called in FROM. A set-returning function
// stands in for a relation of a single column named after it.
func (a *analyzer) bindRangeFunction(rf *ast.RangeFunction) (scopeRel, error) {
call := findFuncCall(rf.Functions)
if call == nil {
return scopeRel{}, fmt.Errorf("range function: no function call")
}
name := funcCallName(call)
overloads, err := a.cat.FindProcs(name, nil)
if err != nil {
return scopeRel{}, err
}
if len(overloads) == 0 {
return scopeRel{}, fmt.Errorf("unknown function %q", name)
}
// The arguments are typed against the scope built so far, which is what
// gives a placeholder passed to the function its type.
if _, err := a.typeFuncCall(call); err != nil {
return scopeRel{}, err
}
p := overloads[0]
rel := scopeRel{
alias: name,
cols: []core.ClassColumn{{Name: name, TypeOID: p.ReturnTypeOID, NotNull: !p.ReturnNullable}},
}
if rf.Alias != nil && rf.Alias.Aliasname != nil && *rf.Alias.Aliasname != "" {
rel.alias = *rf.Alias.Aliasname
}
return rel, nil
}
func findFuncCall(l *ast.List) *ast.FuncCall {
for _, item := range listItems(l) {
switch v := item.(type) {
case *ast.FuncCall:
return v
case *ast.List:
if call := findFuncCall(v); call != nil {
return call
}
}
}
return nil
}
// bindRangeSubselect binds a subquery used as a relation in FROM.
func (a *analyzer) bindRangeSubselect(rs *ast.RangeSubselect) (scopeRel, error) {
sel, ok := rs.Subquery.(*ast.SelectStmt)
if !ok {
return scopeRel{}, fmt.Errorf("subquery: unsupported %T", rs.Subquery)
}
cols, err := a.subqueryColumns(sel)
if err != nil {
return scopeRel{}, err
}
alias := ""
if rs.Alias != nil && rs.Alias.Aliasname != nil {
alias = *rs.Alias.Aliasname
}
rel := derivedRel(alias, cols)
if rs.Alias != nil {
renameColumns(&rel, rs.Alias.Colnames)
}
return rel, nil
}
func (a *analyzer) bindRangeVar(rv *ast.RangeVar) (scopeRel, error) {
if rv.Relname == nil {
return scopeRel{}, fmt.Errorf("range var: missing relation name")
}
relName := *rv.Relname
// A WITH clause defines a relation that shadows nothing but is not in the
// catalog, so it is resolved before one.
if cte, ok := a.ctes[relName]; ok && (rv.Schemaname == nil || *rv.Schemaname == "") {
if rv.Alias != nil && rv.Alias.Aliasname != nil && *rv.Alias.Aliasname != "" {
cte.alias = *rv.Alias.Aliasname
}
return cte, nil
}
schema := ""
if rv.Schemaname != nil {
schema = *rv.Schemaname
}
if schema == "" {
schema = "public"
}
nsOID, err := a.cat.NamespaceOID(schema)
if err != nil {
return scopeRel{}, fmt.Errorf("schema %q: %w", schema, err)
}
classOID, err := a.cat.ClassOID(nsOID, relName)
if err != nil {
return scopeRel{}, fmt.Errorf("relation %q.%q: %w", schema, relName, err)
}
rel := scopeRel{
alias: relName,
classOID: classOID,
}
if rv.Alias != nil && rv.Alias.Aliasname != nil && *rv.Alias.Aliasname != "" {
rel.alias = *rv.Alias.Aliasname
}
cols, err := a.cat.ClassColumns(classOID)
if err != nil {
return scopeRel{}, err
}
rel.cols = cols
return rel, nil
}
// resolveColumn finds a column in this query's scope, falling back to the scope
// of the query it is nested in, which is what a correlated subquery refers to.
func (a *analyzer) resolveColumn(relation, column string) (scopeRel, core.ClassColumn, bool, error) {
rel, col, ok, err := a.scope.resolveColumn(relation, column)
if err != nil || ok {
return rel, col, ok, err
}
if a.outer != nil {
return a.outer.resolveColumn(relation, column)
}
return rel, col, false, nil
}
// resolveColumn finds the single column named column, optionally qualified by
// relation. It reports an error when more than one relation in scope offers
// that name.
func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col core.ClassColumn, ok bool, err error) {
// A statement whose scope is not built yet offers no columns. Report the
// column as unresolved and let the caller say so, rather than crashing on
// the way to the same answer.
if s == nil {
return rel, col, false, nil
}
found := 0
for _, r := range s.rels {
if relation != "" && r.alias != relation {
continue
}
for _, c := range r.cols {
if c.Name != column {
continue
}
found++
if found > 1 {
return scopeRel{}, core.ClassColumn{}, false, fmt.Errorf("ambiguous column reference %q", column)
}
rel, col = r, c
}
}
if found == 0 {
return scopeRel{}, core.ClassColumn{}, false, nil
}
return rel, col, true, nil
}

287
internal/core/attribute.go Normal file
View File

@@ -0,0 +1,287 @@
package core
import (
"context"
"fmt"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
type AttributeSpec struct {
ClassOID int64
Name string
TypeOID int64
Num int
NotNull bool
HasDefault bool
DeclType string
AutoIncrement bool
IsPrimaryKey bool
IsUnique bool
// Hidden columns resolve by name but stay out of a star expansion and
// the relation's model, like the column an sqlite fts5 table names
// after itself.
Hidden bool
}
func (c *Catalog) CreateAttributeSpec(s AttributeSpec) error {
err := c.q.CreateAttribute(context.Background(), catalogdb.CreateAttributeParams{
ClassOid: s.ClassOID,
Name: s.Name,
TypeOid: s.TypeOID,
NotNull: boolToInt64(s.NotNull),
HasDefault: boolToInt64(s.HasDefault),
Num: int64(s.Num),
DeclType: s.DeclType,
AutoIncrement: boolToInt64(s.AutoIncrement),
IsPrimaryKey: boolToInt64(s.IsPrimaryKey),
IsUnique: boolToInt64(s.IsUnique),
Hidden: boolToInt64(s.Hidden),
})
if err != nil {
return fmt.Errorf("create attribute %q on class %d: %w", s.Name, s.ClassOID, err)
}
return nil
}
func (c *Catalog) CreateAttribute(classOID int64, name string, typeOID int64, notNull bool, hasDefault bool, num int) error {
return c.CreateAttributeSpec(AttributeSpec{
ClassOID: classOID,
Name: name,
TypeOID: typeOID,
Num: num,
NotNull: notNull,
HasDefault: hasDefault,
})
}
// DropAttribute removes a column from a relation.
func (c *Catalog) DropAttribute(classOID int64, name string) error {
err := c.q.DeleteAttribute(context.Background(), catalogdb.DeleteAttributeParams{
ClassOid: classOID,
Name: name,
})
if err != nil {
return fmt.Errorf("drop attribute %q on class %d: %w", name, classOID, err)
}
return nil
}
// RenameAttribute renames a column.
func (c *Catalog) RenameAttribute(classOID int64, name, newName string) error {
err := c.q.RenameAttribute(context.Background(), catalogdb.RenameAttributeParams{
ClassOid: classOID,
Name: name,
NewName: newName,
})
if err != nil {
return fmt.Errorf("rename attribute %q on class %d: %w", name, classOID, err)
}
return nil
}
// SetAttributeType changes a column's type.
func (c *Catalog) SetAttributeType(classOID int64, name string, typeOID int64, declType string) error {
err := c.q.SetAttributeType(context.Background(), catalogdb.SetAttributeTypeParams{
ClassOid: classOID,
Name: name,
TypeOid: typeOID,
DeclType: declType,
})
if err != nil {
return fmt.Errorf("set type of attribute %q on class %d: %w", name, classOID, err)
}
return nil
}
// SetAttributeNotNull records whether a column accepts NULL.
func (c *Catalog) SetAttributeNotNull(classOID int64, name string, notNull bool) error {
err := c.q.SetAttributeNotNull(context.Background(), catalogdb.SetAttributeNotNullParams{
ClassOid: classOID,
Name: name,
NotNull: boolToInt64(notNull),
})
if err != nil {
return fmt.Errorf("set null of attribute %q on class %d: %w", name, classOID, err)
}
return nil
}
// NextAttributeNum is the ordinal a column added to the relation takes.
func (c *Catalog) NextAttributeNum(classOID int64) (int, error) {
num, err := c.q.MaxAttributeNum(context.Background(), classOID)
if err != nil {
return 0, fmt.Errorf("attribute count for class %d: %w", classOID, err)
}
return int(num) + 1, nil
}
func (c *Catalog) SetAttributePrimaryKey(classOID int64, columns []string) error {
ctx := context.Background()
for _, name := range columns {
err := c.q.SetAttributePrimaryKey(ctx, catalogdb.SetAttributePrimaryKeyParams{
ClassOid: classOID,
Name: name,
})
if err != nil {
return fmt.Errorf("mark pk %s on class %d: %w", name, classOID, err)
}
}
return nil
}
func (c *Catalog) SetAttributeUnique(classOID int64, columns []string) error {
ctx := context.Background()
for _, name := range columns {
err := c.q.SetAttributeUnique(ctx, catalogdb.SetAttributeUniqueParams{
ClassOid: classOID,
Name: name,
})
if err != nil {
return fmt.Errorf("mark unique %s on class %d: %w", name, classOID, err)
}
}
return nil
}
type ColumnInfo struct {
Name string
TypeName string
TypeOID int64
NotNull bool
DeclType string
AutoIncrement bool
IsPrimaryKey bool
IsUnique bool
AttributeOID int64
ClassOID int64
}
func (c *Catalog) ResolveColumn(table, column string) (*ColumnInfo, error) {
r, err := c.q.ResolveColumn(context.Background(), catalogdb.ResolveColumnParams{
TableName: table,
ColumnName: column,
})
if err != nil {
return nil, fmt.Errorf("resolve %s.%s: %w", table, column, err)
}
info := ColumnInfo{
AttributeOID: r.Oid,
ClassOID: r.ClassOid,
Name: r.Name,
TypeName: r.TypeName,
TypeOID: r.TypeOid,
NotNull: r.NotNull != 0,
DeclType: r.DeclType,
AutoIncrement: r.AutoIncrement != 0,
IsPrimaryKey: r.IsPrimaryKey != 0,
IsUnique: r.IsUnique != 0,
}
return &info, nil
}
func (c *Catalog) TableColumns(table string) ([]ColumnInfo, error) {
rows, err := c.q.TableColumns(context.Background(), table)
if err != nil {
return nil, fmt.Errorf("table columns %q: %w", table, err)
}
cols := make([]ColumnInfo, 0, len(rows))
for _, r := range rows {
cols = append(cols, ColumnInfo{
AttributeOID: r.Oid,
ClassOID: r.ClassOid,
Name: r.Name,
TypeName: r.TypeName,
TypeOID: r.TypeOid,
NotNull: r.NotNull != 0,
DeclType: r.DeclType,
AutoIncrement: r.AutoIncrement != 0,
IsPrimaryKey: r.IsPrimaryKey != 0,
IsUnique: r.IsUnique != 0,
})
}
return cols, nil
}
type ClassColumn struct {
AttOID int64
Name string
TypeOID int64
// Type is the column's type as an expression, set for a column of a
// derived relation whose type the catalog holds no row for, or holds
// only the family of.
Type *TypeExpr
NotNull bool
Hidden bool
}
// ClassColumns returns a relation's columns in ordinal order.
func (c *Catalog) ClassColumns(classOID int64) ([]ClassColumn, error) {
rows, err := c.q.ClassAttributes(context.Background(), classOID)
if err != nil {
return nil, fmt.Errorf("class columns %d: %w", classOID, err)
}
out := make([]ClassColumn, 0, len(rows))
for _, r := range rows {
out = append(out, ClassColumn{
AttOID: r.Oid,
Name: r.Name,
TypeOID: r.TypeOid,
NotNull: r.NotNull != 0,
Hidden: r.Hidden != 0,
})
}
return out, nil
}
type CodegenColumn struct {
Name string
TypeOID int64
NotNull bool
}
func (c *Catalog) ClassCodegenColumns(classOID int64) ([]CodegenColumn, error) {
rows, err := c.q.ListClassColumns(context.Background(), classOID)
if err != nil {
return nil, fmt.Errorf("class codegen columns %d: %w", classOID, err)
}
out := make([]CodegenColumn, 0, len(rows))
for _, r := range rows {
out = append(out, CodegenColumn{
Name: r.ColumnName,
TypeOID: r.TypeOid,
NotNull: r.NotNull != 0,
})
}
return out, nil
}
type AttributeDetails struct {
Schema string
Table string
Column string
Num int
DeclType string
AutoIncrement bool
IsPrimaryKey bool
IsUnique bool
NotNull bool
}
func (c *Catalog) LookupAttribute(attOID int64) (AttributeDetails, error) {
r, err := c.q.LookupAttribute(context.Background(), attOID)
if err != nil {
return AttributeDetails{}, fmt.Errorf("lookup attribute %d: %w", attOID, err)
}
return AttributeDetails{
Schema: r.SchemaName,
Table: r.TableName,
Column: r.ColumnName,
Num: int(r.Num),
DeclType: r.DeclType,
AutoIncrement: r.AutoIncrement != 0,
IsPrimaryKey: r.IsPrimaryKey != 0,
IsUnique: r.IsUnique != 0,
NotNull: r.NotNull != 0,
}, nil
}

161
internal/core/cache.go Normal file
View File

@@ -0,0 +1,161 @@
package core
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/ext/serdes"
"github.com/sqlc-dev/sqlc/internal/cache"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
// The name of the sole output blob a CoreCatalog action produces.
const catalogOutput = "catalog.db"
// rawConn is the driver's escape hatch to the SQLite connection underneath a
// database/sql connection, which the serialization API needs.
type rawConn interface {
Raw() *sqlite3.Conn
}
// NewCached returns the catalog a dialect and a schema produce, restored from
// the cache when a run has produced it before.
//
// Producing one means seeding the dialect — thousands of rows, since
// PostgreSQL's functions and system catalogs alone run to five figures — and
// then parsing the schema and applying its DDL. The result is the same every
// time, so the finished database is stored in the cache and opened directly on
// the next run, turning all of that work into opening a file.
// It is a single action: the dialect and the schema together are what the
// catalog is, so they are one key, and a hit means apply is never called.
//
// The dialect data is embedded in the sqlc binary, whose digest is an input to
// every action, so the dialect only has to be named. The schema is hashed in
// the order it is applied, since DDL order decides what the catalog holds.
//
// A catalog whose apply fails is returned but not cached, so that a caller can
// report the failure against the catalog it got. A nil catalog means the
// catalog could not be created at all. A cache that cannot be opened, read or
// written is not an error: the catalog is simply built the long way.
func NewCached(dialect string, schema []string, apply func(*Catalog) error, opts ...Option) (*Catalog, error) {
store, action := openAction(dialect, schema)
if store != nil {
defer store.Close()
if cat, err := restore(store, action); err == nil {
return cat, nil
} else if !errors.Is(err, cache.ErrNotFound) {
slog.Debug("restoring the catalog from the cache failed", "err", err)
}
}
cat, err := New(opts...)
if err != nil {
return nil, err
}
if err := apply(cat); err != nil {
return cat, err
}
if store != nil {
if err := save(store, action, cat); err != nil {
slog.Debug("saving the catalog to the cache failed", "err", err)
}
}
return cat, nil
}
// openAction opens the cache and digests the action that produces a catalog. A
// nil store means the cache is unavailable and the catalog has to be built.
func openAction(dialect string, schema []string) (*cache.Cache, cache.Digest) {
store, err := cache.Open()
if err != nil {
slog.Debug("opening the cache failed", "err", err)
return nil, cache.Digest{}
}
action := store.NewAction("CoreCatalog").AddInput("dialect", []byte(dialect))
for _, s := range schema {
action.AddInput("schema", []byte(s))
}
return store, action.Digest()
}
// restore opens the catalog a previous run cached. The stored blob is opened
// where it lies rather than copied: analysis only ever reads the catalog, so
// every run that wants this one reads the same file.
func restore(store *cache.Cache, action cache.Digest) (*Catalog, error) {
result, err := store.Actions.Get(action)
if err != nil {
return nil, err
}
path, ok := store.CAS.Filename(result.Outputs[catalogOutput])
if !ok {
return nil, cache.ErrNotFound
}
db, err := openFile(path)
if err != nil {
return nil, err
}
stmts := newStmtCache(db)
cat := &Catalog{db: db, stmts: stmts, q: catalogdb.New(stmts)}
// The seeds recorded the dialect on the catalog as they ran. A restored
// catalog reads it back from the database instead.
row, err := cat.q.SeededDialect(context.Background())
if err != nil {
cat.Close()
return nil, fmt.Errorf("core: restored catalog has no dialect: %w", err)
}
cat.dialectOID = row.Oid
return cat, nil
}
// save stores a finished catalog for the next run.
func save(store *cache.Cache, action cache.Digest, cat *Catalog) error {
blob, err := serialize(cat.db)
if err != nil {
return err
}
digest, err := store.CAS.Put(blob)
if err != nil {
return err
}
return store.Actions.Put(action, &cache.ActionResult{
Outputs: map[string]cache.Digest{catalogOutput: digest},
})
}
// serialize returns the bytes a database being built in memory would be
// written to disk as, which is what gets stored.
func serialize(db *sql.DB) ([]byte, error) {
var blob []byte
err := withRawConn(db, func(c *sqlite3.Conn) error {
var err error
blob, err = serdes.Serialize(c, "main")
return err
})
if err != nil {
return nil, fmt.Errorf("core: serialize catalog: %w", err)
}
return blob, nil
}
func withRawConn(db *sql.DB, fn func(*sqlite3.Conn) error) error {
conn, err := db.Conn(context.Background())
if err != nil {
return err
}
defer conn.Close()
return conn.Raw(func(driverConn any) error {
r, ok := driverConn.(rawConn)
if !ok {
return fmt.Errorf("driver does not expose its SQLite connection")
}
return fn(r.Raw())
})
}

69
internal/core/cast.go Normal file
View File

@@ -0,0 +1,69 @@
package core
import (
"context"
"fmt"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
type CastSpec struct {
SourceTypeOID int64
TargetTypeOID int64
ProcOID int64
Context string
DialectOID int64
}
func (c *Catalog) CreateCast(cs CastSpec) error {
if cs.Context == "" {
cs.Context = "e"
}
err := c.q.CreateCast(context.Background(), catalogdb.CreateCastParams{
SourceTypeOid: cs.SourceTypeOID,
TargetTypeOid: cs.TargetTypeOID,
ProcOid: nullInt64(cs.ProcOID),
Context: cs.Context,
DialectOid: nullInt64(cs.DialectOID),
})
if err != nil {
return fmt.Errorf("create cast %d->%d: %w", cs.SourceTypeOID, cs.TargetTypeOID, err)
}
return nil
}
func (c *Catalog) FindCast(src, tgt int64) (CastSpec, bool, error) {
row, err := c.q.FindCast(context.Background(), catalogdb.FindCastParams{
SourceTypeOid: src,
TargetTypeOid: tgt,
})
if err != nil {
return CastSpec{}, false, nil
}
return CastSpec{
SourceTypeOID: row.SourceTypeOid,
TargetTypeOID: row.TargetTypeOid,
ProcOID: orZero(row.ProcOid),
Context: row.Context,
DialectOID: orZero(row.DialectOid),
}, true, nil
}
func (c *Catalog) CastAllowed(src, tgt int64, ctx string) (bool, error) {
if src == tgt {
return true, nil
}
cs, ok, err := c.FindCast(src, tgt)
if err != nil || !ok {
return false, err
}
switch ctx {
case "i":
return cs.Context == "i", nil
case "a":
return cs.Context == "i" || cs.Context == "a", nil
case "e":
return true, nil
}
return false, fmt.Errorf("unknown cast context %q", ctx)
}

189
internal/core/catalog.go Normal file
View File

@@ -0,0 +1,189 @@
package core
import (
"context"
"database/sql"
"fmt"
"runtime"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
"github.com/sqlc-dev/sqlc/internal/core/catalogdef"
_ "github.com/ncruces/go-sqlite3/driver"
)
//go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc generate
type Catalog struct {
db *sql.DB
stmts *stmtCache
q *catalogdb.Queries
// dialectOID is the dialect this catalog was seeded with. A catalog is
// built for one dialect, so dialect-wide lookups need no other input.
dialectOID int64
// loadExtension applies a named extension's seed, installed by the
// dialect's seed for the engines that ship extension data. It runs at most
// once per extension name: a schema is free to say CREATE EXTENSION twice.
loadExtension func(name string) error
extensions map[string]bool
// types remembers the rows and expressions looked up so far, and rules
// what the dialect does to a type before storing it.
types typeCache
rules rules
}
type Option func(*Catalog) error
func WithSeed(fn func(*Catalog) error) Option {
return func(c *Catalog) error { return fn(c) }
}
func New(opts ...Option) (*Catalog, error) {
db, err := openDB()
if err != nil {
return nil, err
}
// catalogdef.Schema is a batch of DDL statements, so it goes to the
// connection directly rather than through the prepared-statement cache.
if _, err := db.Exec(catalogdef.Schema); err != nil {
db.Close()
return nil, fmt.Errorf("core: init schema: %w", err)
}
stmts := newStmtCache(db)
c := &Catalog{db: db, stmts: stmts, q: catalogdb.New(stmts)}
// Run the seeds inside one transaction rather than paying a commit for each
// of the hundreds of rows a dialect registers.
ctx := context.Background()
if err := stmts.begin(ctx); err != nil {
db.Close()
return nil, fmt.Errorf("core: begin setup: %w", err)
}
if err := c.setup(opts); err != nil {
stmts.rollback()
db.Close()
return nil, err
}
if err := stmts.commit(); err != nil {
db.Close()
return nil, fmt.Errorf("core: commit setup: %w", err)
}
return c, nil
}
// openFile opens a catalog held in a file, read only. The file is named after
// the hash of its contents, so it can never change under a reader: SQLite is
// told as much, which lets any number of processes share it with no locking
// and no copy.
func openFile(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", "file:"+path+"?mode=ro&immutable=1")
if err != nil {
return nil, fmt.Errorf("core: open catalog %s: %w", path, err)
}
// An immutable file has no locking to contend for, so queries analyzed
// concurrently each get their own connection rather than queueing on one.
db.SetMaxOpenConns(runtime.GOMAXPROCS(0))
db.SetMaxIdleConns(runtime.GOMAXPROCS(0))
db.SetConnMaxIdleTime(0)
db.SetConnMaxLifetime(0)
return db, nil
}
// pinPool holds a catalog being built to a single connection that is never
// retired: every ":memory:" connection is its own empty database, so a second
// one would see a catalog with no tables in it.
func pinPool(db *sql.DB) {
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxIdleTime(0)
db.SetConnMaxLifetime(0)
}
// openDB opens the empty SQLite database a catalog is built in.
func openDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
return nil, fmt.Errorf("core: open catalog: %w", err)
}
pinPool(db)
// The catalog being built is scratch state that is never read back from
// disk, so durability buys nothing and costs a journal write per statement.
// Foreign keys are declared in the schema as documentation but not
// enforced: dialect seeds carry dangling references (a proc whose return
// type was never registered), and enforcement would also charge an index
// lookup per seeded row. The driver compiles SQLite with enforcement on by
// default, so it is switched off explicitly.
for _, pragma := range []string{
"PRAGMA journal_mode = OFF",
"PRAGMA synchronous = OFF",
"PRAGMA foreign_keys = OFF",
} {
if _, err := db.Exec(pragma); err != nil {
db.Close()
return nil, fmt.Errorf("core: %s: %w", pragma, err)
}
}
return db, nil
}
func (c *Catalog) setup(opts []Option) error {
if err := c.bootstrap(); err != nil {
return fmt.Errorf("core: bootstrap: %w", err)
}
for i, opt := range opts {
if err := opt(c); err != nil {
return fmt.Errorf("core: option %d: %w", i, err)
}
}
return nil
}
func (c *Catalog) Close() error {
err := c.stmts.Close()
if cerr := c.db.Close(); err == nil {
err = cerr
}
return err
}
func (c *Catalog) DB() *sql.DB {
return c.db
}
func (c *Catalog) bootstrap() error {
_, err := c.CreateNamespace("public")
return err
}
// SeededDialectOID returns the dialect this catalog was seeded with, which is
// what anything registered after seeding — an extension's types and functions
// — records as its dialect.
func (c *Catalog) SeededDialectOID() int64 {
return c.dialectOID
}
// SetExtensionLoader installs the function CREATE EXTENSION calls to apply an
// extension's seed. A dialect without extension data leaves it unset.
func (c *Catalog) SetExtensionLoader(fn func(name string) error) {
c.loadExtension = fn
}
// LoadExtension applies the named extension's seed to the catalog. An
// extension already applied, or a dialect with no loader, contributes
// nothing; an extension the loader does not know is the loader's to ignore.
func (c *Catalog) LoadExtension(name string) error {
if c.loadExtension == nil || c.extensions[name] {
return nil
}
if c.extensions == nil {
c.extensions = map[string]bool{}
}
c.extensions[name] = true
return c.loadExtension(name)
}

View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package catalogdb
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...any) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -0,0 +1,142 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package catalogdb
import (
"database/sql"
)
type SqlAttribute struct {
Oid int64
ClassOid int64
Name string
TypeOid int64
NotNull int64
HasDefault int64
Num int64
DeclType string
AutoIncrement int64
IsPrimaryKey int64
IsUnique int64
Hidden int64
}
type SqlCast struct {
SourceTypeOid int64
TargetTypeOid int64
ProcOid sql.NullInt64
Context string
DialectOid sql.NullInt64
}
type SqlClass struct {
Oid int64
NamespaceOid int64
Name string
Kind string
}
type SqlConstraint struct {
Oid int64
ClassOid int64
Name string
Kind string
Columns string
}
type SqlDialect struct {
Oid int64
Name string
}
type SqlDialectFlag struct {
DialectOid int64
Key string
Value string
}
type SqlNamespace struct {
Oid int64
Name string
}
type SqlOperator struct {
Oid int64
NamespaceOid sql.NullInt64
DialectOid sql.NullInt64
Name string
LeftTypeOid sql.NullInt64
RightTypeOid sql.NullInt64
ResultTypeOid int64
ProcOid sql.NullInt64
CommutatorOid sql.NullInt64
NegatorOid sql.NullInt64
}
type SqlProc struct {
Oid int64
NamespaceOid sql.NullInt64
DialectOid sql.NullInt64
Name string
Kind string
ReturnTypeOid int64
ReturnSet int64
ReturnNullable int64
ReturnTemplate string
Strict int64
VariadicKind string
}
type SqlProcArg struct {
ProcOid int64
Ord int64
Name string
TypeOid int64
Mode string
HasDefault int64
}
type SqlType struct {
Oid int64
NamespaceOid int64
DialectOid sql.NullInt64
Name string
Expr string
Typtype string
Category sql.NullString
Preferred int64
FamilyOid sql.NullInt64
ElementOid sql.NullInt64
BaseOid sql.NullInt64
CanonicalOid sql.NullInt64
NotNull int64
}
type SqlTypeAffinity struct {
DialectOid int64
Ord int64
Words string
TypeOid int64
}
type SqlTypeArg struct {
TypeOid int64
Ord int64
Label string
ArgTypeOid sql.NullInt64
Nullable int64
IntValue sql.NullInt64
BoolValue sql.NullInt64
StringValue sql.NullString
Ident sql.NullString
}
type SqlTypeRewrite struct {
DialectOid int64
Ord int64
Pattern string
Template string
Cond string
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
package catalogdef
import _ "embed"
//go:embed schema.sql
var Schema string

View File

@@ -0,0 +1,284 @@
-- Queries against sqlc's own sql_* catalog tables, compiled by sqlc's
-- SQLite engine. Regenerate with `go generate ./internal/core/...`.
-- ============================ sql_namespace ============================
-- name: CreateNamespace :execlastid
INSERT INTO sql_namespace (name) VALUES (?);
-- name: NamespaceOID :one
SELECT oid FROM sql_namespace WHERE name = ?;
-- name: ListNamespaces :many
SELECT oid, name FROM sql_namespace ORDER BY oid;
-- ============================== sql_dialect ============================
-- name: CreateDialect :execlastid
INSERT INTO sql_dialect (name) VALUES (?);
-- name: DialectOID :one
SELECT oid FROM sql_dialect WHERE name = ?;
-- name: SeededDialect :one
-- The dialect a catalog was seeded with. A catalog is built for exactly one,
-- so a restored catalog finds it without being told its name.
SELECT oid, name FROM sql_dialect ORDER BY oid LIMIT 1;
-- name: SetDialectFlag :exec
INSERT INTO sql_dialect_flag (dialect_oid, key, value)
VALUES (?, ?, ?)
ON CONFLICT(dialect_oid, key) DO UPDATE SET value = excluded.value;
-- name: DialectFlag :one
SELECT value FROM sql_dialect_flag WHERE dialect_oid = ? AND key = ?;
-- =============================== sql_type ==============================
-- name: CreateType :execlastid
INSERT INTO sql_type
(name, expr, typtype, category, preferred, namespace_oid, dialect_oid,
family_oid, element_oid, base_oid, canonical_oid, not_null)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-- name: CreateTypeArg :exec
INSERT INTO sql_type_arg
(type_oid, ord, label, arg_type_oid, nullable, int_value, bool_value, string_value, ident)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
-- name: TypeArgs :many
SELECT ord, label, arg_type_oid, nullable, int_value, bool_value, string_value, ident
FROM sql_type_arg
WHERE type_oid = ?
ORDER BY ord;
-- name: TypeOIDByExpr :one
SELECT t.oid
FROM sql_type t
JOIN sql_namespace ns ON ns.oid = t.namespace_oid
WHERE t.expr = sqlc.arg(expr)
ORDER BY
CASE ns.name
WHEN 'pg_catalog' THEN 0
WHEN 'public' THEN 1
ELSE 2
END,
ns.name
LIMIT 1;
-- name: TypeOIDsByNameInNamespaces :many
SELECT oid, namespace_oid FROM sql_type
WHERE name = sqlc.arg(name) AND family_oid IS NULL
AND namespace_oid IN (sqlc.slice(namespace_oids));
-- name: TypeOIDByNameInNamespace :one
SELECT oid FROM sql_type
WHERE namespace_oid = ? AND name = ? AND family_oid IS NULL;
-- name: TypeOIDByExprInNamespace :one
SELECT oid FROM sql_type WHERE namespace_oid = ? AND expr = ?;
-- name: TypeOIDByName :one
-- The family spelled name: an instance carries its family's name too, and
-- is found by its expression instead.
SELECT t.oid
FROM sql_type t
JOIN sql_namespace ns ON ns.oid = t.namespace_oid
WHERE t.name = sqlc.arg(name) AND t.family_oid IS NULL
ORDER BY
CASE ns.name
WHEN 'pg_catalog' THEN 0
WHEN 'public' THEN 1
ELSE 2
END,
ns.name
LIMIT 1;
-- name: TypeNameByOID :one
SELECT name FROM sql_type WHERE oid = ?;
-- name: TypeOIDsInCategory :many
-- The families of a category: an instance inherits its family's category
-- and an alias stands for the row it points at, so neither is listed.
SELECT oid FROM sql_type
WHERE dialect_oid = ? AND category = ?
AND family_oid IS NULL AND canonical_oid IS NULL
ORDER BY oid;
-- name: LookupType :one
SELECT oid, namespace_oid, name, expr, category, typtype, preferred,
family_oid, element_oid, base_oid, canonical_oid, not_null
FROM sql_type
WHERE oid = ?;
-- name: CreateTypeRewrite :exec
INSERT INTO sql_type_rewrite (dialect_oid, ord, pattern, template, cond)
VALUES (?, ?, ?, ?, ?);
-- name: ListTypeRewrites :many
SELECT pattern, template, cond FROM sql_type_rewrite
WHERE dialect_oid = ? ORDER BY ord;
-- name: CreateTypeAffinity :exec
INSERT INTO sql_type_affinity (dialect_oid, ord, words, type_oid)
VALUES (?, ?, ?, ?);
-- name: ListTypeAffinities :many
SELECT words, type_oid FROM sql_type_affinity
WHERE dialect_oid = ? ORDER BY ord;
-- =============================== sql_class =============================
-- name: CreateClass :execlastid
INSERT INTO sql_class (namespace_oid, name, kind) VALUES (?, ?, ?);
-- name: ClassOID :one
SELECT oid FROM sql_class WHERE namespace_oid = ? AND name = ?;
-- name: ClassOIDByName :one
SELECT oid FROM sql_class WHERE name = ? LIMIT 1;
-- name: ListTablesInNamespace :many
SELECT oid, name FROM sql_class
WHERE namespace_oid = ? AND kind = 'r'
ORDER BY oid;
-- name: DeleteClass :exec
DELETE FROM sql_class WHERE oid = ?;
-- name: RenameClass :exec
UPDATE sql_class SET name = sqlc.arg(new_name) WHERE oid = sqlc.arg(oid);
-- ============================= sql_attribute ===========================
-- name: CreateAttribute :exec
INSERT INTO sql_attribute (
class_oid, name, type_oid, not_null, has_default, num,
decl_type, auto_increment, is_primary_key, is_unique, hidden
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-- name: SetAttributePrimaryKey :exec
UPDATE sql_attribute SET is_primary_key = 1, not_null = 1
WHERE class_oid = ? AND name = ?;
-- name: SetAttributeUnique :exec
UPDATE sql_attribute SET is_unique = 1
WHERE class_oid = ? AND name = ?;
-- name: DeleteAttributesByClass :exec
DELETE FROM sql_attribute WHERE class_oid = ?;
-- name: DeleteAttribute :exec
DELETE FROM sql_attribute WHERE class_oid = ? AND name = ?;
-- name: RenameAttribute :exec
UPDATE sql_attribute SET name = sqlc.arg(new_name)
WHERE class_oid = sqlc.arg(class_oid) AND name = sqlc.arg(name);
-- name: SetAttributeType :exec
UPDATE sql_attribute SET type_oid = sqlc.arg(type_oid), decl_type = sqlc.arg(decl_type)
WHERE class_oid = sqlc.arg(class_oid) AND name = sqlc.arg(name);
-- name: SetAttributeNotNull :exec
UPDATE sql_attribute SET not_null = sqlc.arg(not_null)
WHERE class_oid = sqlc.arg(class_oid) AND name = sqlc.arg(name);
-- name: MaxAttributeNum :one
SELECT CAST(COALESCE(MAX(num), 0) AS INTEGER) AS num FROM sql_attribute WHERE class_oid = ?;
-- name: ResolveColumn :one
SELECT a.oid, a.class_oid, a.name, t.name AS type_name, a.type_oid, a.not_null,
a.decl_type, a.auto_increment, a.is_primary_key, a.is_unique
FROM sql_attribute a
JOIN sql_class c ON c.oid = a.class_oid
JOIN sql_type t ON t.oid = a.type_oid
WHERE c.name = sqlc.arg(table_name) AND a.name = sqlc.arg(column_name);
-- name: TableColumns :many
SELECT a.oid, a.class_oid, a.name, t.name AS type_name, a.type_oid, a.not_null,
a.decl_type, a.auto_increment, a.is_primary_key, a.is_unique
FROM sql_attribute a
JOIN sql_class c ON c.oid = a.class_oid
JOIN sql_type t ON t.oid = a.type_oid
WHERE c.name = ?
ORDER BY a.num;
-- name: ClassAttributes :many
SELECT oid, name, type_oid, not_null, hidden
FROM sql_attribute
WHERE class_oid = ?
ORDER BY num;
-- name: ListClassColumns :many
SELECT a.name AS column_name, a.type_oid, a.not_null
FROM sql_attribute a
JOIN sql_type t ON t.oid = a.type_oid
WHERE a.class_oid = ? AND a.hidden = 0
ORDER BY a.num;
-- name: LookupAttribute :one
SELECT ns.name AS schema_name, cls.name AS table_name, a.name AS column_name, a.num,
a.decl_type, a.auto_increment, a.is_primary_key, a.is_unique, a.not_null
FROM sql_attribute a
JOIN sql_class cls ON cls.oid = a.class_oid
JOIN sql_namespace ns ON ns.oid = cls.namespace_oid
WHERE a.oid = ?;
-- ============================ sql_constraint ===========================
-- name: CreateConstraint :exec
INSERT INTO sql_constraint (class_oid, name, kind, columns) VALUES (?, ?, ?, ?);
-- =============================== sql_proc ==============================
-- name: CreateProc :execlastid
INSERT INTO sql_proc
(namespace_oid, dialect_oid, name, kind,
return_type_oid, return_set, return_nullable, return_template, strict, variadic_kind)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-- name: CreateProcArg :exec
INSERT INTO sql_proc_arg (proc_oid, ord, name, type_oid, mode, has_default)
VALUES (?, ?, ?, ?, ?, ?);
-- name: ProcArgTypes :many
SELECT type_oid FROM sql_proc_arg
WHERE proc_oid = ? AND mode IN ('i', 'b', 'v')
ORDER BY ord;
-- name: FindProcsAnyNamespace :many
SELECT oid, name, kind, return_type_oid, return_nullable, return_template
FROM sql_proc
WHERE name = ?;
-- name: FindProcsInNamespaces :many
SELECT oid, name, kind, return_type_oid, return_nullable, return_template
FROM sql_proc
WHERE name = sqlc.arg(name)
AND namespace_oid IN (sqlc.slice(namespace_oids));
-- ============================= sql_operator ============================
-- name: CreateOperator :execlastid
INSERT INTO sql_operator
(namespace_oid, dialect_oid, name,
left_type_oid, right_type_oid, result_type_oid, proc_oid)
VALUES (?, ?, ?, ?, ?, ?, ?);
-- name: FindOperators :many
SELECT oid, name, left_type_oid, right_type_oid, result_type_oid, proc_oid
FROM sql_operator
WHERE name = sqlc.arg(name)
AND (sqlc.arg(left_type_oid) = 0 OR left_type_oid = sqlc.arg(left_type_oid))
AND (sqlc.arg(right_type_oid) = 0 OR right_type_oid = sqlc.arg(right_type_oid));
-- =============================== sql_cast ==============================
-- name: CreateCast :exec
INSERT INTO sql_cast (source_type_oid, target_type_oid, proc_oid, context, dialect_oid)
VALUES (?, ?, ?, ?, ?);
-- name: FindCast :one
SELECT source_type_oid, target_type_oid, proc_oid, context, dialect_oid
FROM sql_cast
WHERE source_type_oid = ? AND target_type_oid = ?;

View File

@@ -0,0 +1,227 @@
-- sql_namespace: schemas / namespaces
CREATE TABLE sql_namespace (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
-- sql_dialect: registered SQL dialects (postgresql, sqlite, mysql, ...).
CREATE TABLE sql_dialect (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
-- sql_dialect_flag: per-dialect configuration knobs (case-folding,
-- identifier quoting, alias scoping rules, etc.). Values are opaque
-- strings; the analyzer interprets them per key.
CREATE TABLE sql_dialect_flag (
dialect_oid INTEGER NOT NULL REFERENCES sql_dialect(oid),
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (dialect_oid, key)
);
-- sql_type: data types, one row per type expression. Modeled on pg_type,
-- with the arguments PostgreSQL keeps as a typmod on the use site held in
-- the row instead.
--
-- A row is a family — a name the dialect or the schema declares: numeric,
-- array, mood — or an instance, a family applied to arguments: numeric(10, 2),
-- array(integer). expr is the canonical spelling of the whole expression and
-- the row's identity; name is the family's name, on an instance too, so a
-- lookup by name finds the family and an instance points at it.
--
-- typtype: 'b'ase | 'c'omposite | 'd'omain | 'e'num | 'p'seudo | 'r'ange
-- category: 'N'umeric | 'S'tring | 'B'oolean | 'D'atetime | 'A'rray |
-- 'C'omposite | 'E'num | 'U'serdef | 'X'unknown
-- preferred: tie-breaker for implicit cast resolution within a category
-- family_oid: NULL on a family; the family on an instance
-- element_oid: what the type holds: an array's element, a map's value,
-- a range's subtype
-- base_oid: what the type stands on: a domain's or alias type's base,
-- a wrapper's inner type, a SQLite spelling's affinity
-- canonical_oid: the row the engine reports this one as: an alias spelling
-- points at the type it names
-- not_null: a domain or alias type declared NOT NULL
-- dialect_oid: NULL = standard / shared across dialects
CREATE TABLE sql_type (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
namespace_oid INTEGER NOT NULL REFERENCES sql_namespace(oid),
dialect_oid INTEGER REFERENCES sql_dialect(oid),
name TEXT NOT NULL,
expr TEXT NOT NULL,
typtype TEXT NOT NULL DEFAULT 'b',
category TEXT,
preferred INTEGER NOT NULL DEFAULT 0,
family_oid INTEGER REFERENCES sql_type(oid),
element_oid INTEGER REFERENCES sql_type(oid),
base_oid INTEGER REFERENCES sql_type(oid),
canonical_oid INTEGER REFERENCES sql_type(oid),
not_null INTEGER NOT NULL DEFAULT 0,
UNIQUE (namespace_oid, expr)
);
CREATE INDEX idx_sql_type_name ON sql_type(name);
-- sql_type_arg: the arguments of an instance, or the fields, labels or
-- members of a declared composite, enum or set, in order. Exactly one of
-- arg_type_oid, int_value, bool_value, string_value and ident is set.
-- label: a struct field, tuple element or enum label
-- nullable: the argument type is nullable at this position, as the inner
-- type of Array(Nullable(String)) is
-- ident: a bare word that is not a type: max, sum, day to second
CREATE TABLE sql_type_arg (
type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
ord INTEGER NOT NULL,
label TEXT NOT NULL DEFAULT '',
arg_type_oid INTEGER REFERENCES sql_type(oid),
nullable INTEGER NOT NULL DEFAULT 0,
int_value INTEGER,
bool_value INTEGER,
string_value TEXT,
ident TEXT,
PRIMARY KEY (type_oid, ord)
);
-- sql_type_rewrite: the rewrites a dialect applies to a type before it is
-- interned, in order, which are how a dialect stores what only it spells:
-- SQL Server keeps float(24) as real and a bare decimal as decimal(18,0),
-- ClickHouse keeps Decimal32(4) as Decimal(9, 4). pattern is a type
-- expression whose arguments may be $1, $2... binding whatever stands there;
-- template is the expression the match becomes, with the bindings
-- substituted; cond bounds a binding, as "$1 <= 24" does.
CREATE TABLE sql_type_rewrite (
dialect_oid INTEGER NOT NULL REFERENCES sql_dialect(oid),
ord INTEGER NOT NULL,
pattern TEXT NOT NULL,
template TEXT NOT NULL,
cond TEXT NOT NULL DEFAULT '',
PRIMARY KEY (dialect_oid, ord)
);
-- sql_type_affinity: the rule a dialect resolves an unseeded type family
-- by, in order: the first row one of whose words the family's name
-- contains names the type it stands on, and a row with no words is the
-- default. SQLite gives every declared spelling one of five affinities
-- this way.
CREATE TABLE sql_type_affinity (
dialect_oid INTEGER NOT NULL REFERENCES sql_dialect(oid),
ord INTEGER NOT NULL,
words TEXT NOT NULL DEFAULT '', -- comma-separated, upper case
type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
PRIMARY KEY (dialect_oid, ord)
);
-- sql_class: relations (tables, views, indexes).
-- kind: 'r' = table, 'v' = view, 'i' = index, 'c' = composite type, 'f' = foreign
CREATE TABLE sql_class (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
namespace_oid INTEGER NOT NULL REFERENCES sql_namespace(oid),
name TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'r',
UNIQUE(namespace_oid, name)
);
-- sql_attribute: columns of a relation.
-- decl_type: the type as the schema spelled it, before
-- canonicalization (VARCHAR(10), BIGINT UNSIGNED), which
-- is what a formatter prints back and what SQLite reports.
-- auto_increment: rowid alias (sqlite INTEGER PRIMARY KEY), AUTOINCREMENT,
-- pg serial/bigserial/identity, mysql AUTO_INCREMENT.
-- is_primary_key: this column participates in the relation's primary key.
-- Set both for inline-column PK and for table-level PK.
-- is_unique: column has a UNIQUE constraint or a single-column UNIQUE
-- table constraint.
-- hidden: resolvable by name but absent from a star expansion and
-- from the relation's model, like the column an sqlite
-- fts5 table names after itself.
CREATE TABLE sql_attribute (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
class_oid INTEGER NOT NULL REFERENCES sql_class(oid),
name TEXT NOT NULL,
type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
not_null INTEGER NOT NULL DEFAULT 0,
has_default INTEGER NOT NULL DEFAULT 0,
num INTEGER NOT NULL, -- ordinal position (1-based)
decl_type TEXT NOT NULL DEFAULT '',
auto_increment INTEGER NOT NULL DEFAULT 0,
is_primary_key INTEGER NOT NULL DEFAULT 0,
is_unique INTEGER NOT NULL DEFAULT 0,
hidden INTEGER NOT NULL DEFAULT 0,
UNIQUE(class_oid, name),
UNIQUE(class_oid, num)
);
-- sql_constraint: constraints on a relation.
-- kind: 'p' = primary key, 'f' = foreign key, 'u' = unique, 'c' = check
CREATE TABLE sql_constraint (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
class_oid INTEGER NOT NULL REFERENCES sql_class(oid),
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL,
columns TEXT NOT NULL DEFAULT '' -- comma-separated attribute nums
);
-- sql_proc: functions, aggregates, window functions, procedures.
-- Modeled on pg_proc.
-- kind: 'f' = function, 'a' = aggregate, 'w' = window, 'p' = procedure
-- variadic_kind: 'n' = none, 'a' = array (VARIADIC any[]), 'v' = variadic-any
-- return_set: 1 if SETOF / table-returning
-- return_template: the result as an expression over the call's arguments,
-- when it depends on their values: Decimal(18, $2) for
-- toDecimal64(x, s). Empty for a result the type says.
CREATE TABLE sql_proc (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
namespace_oid INTEGER REFERENCES sql_namespace(oid),
dialect_oid INTEGER REFERENCES sql_dialect(oid),
name TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'f',
return_type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
return_set INTEGER NOT NULL DEFAULT 0,
return_nullable INTEGER NOT NULL DEFAULT 1,
return_template TEXT NOT NULL DEFAULT '',
strict INTEGER NOT NULL DEFAULT 0,
variadic_kind TEXT NOT NULL DEFAULT 'n'
);
-- sql_proc_arg: ordered argument list for a proc.
-- mode: 'i' = in, 'o' = out, 'b' = both, 't' = table, 'v' = variadic
CREATE TABLE sql_proc_arg (
proc_oid INTEGER NOT NULL REFERENCES sql_proc(oid),
ord INTEGER NOT NULL,
name TEXT NOT NULL DEFAULT '',
type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
mode TEXT NOT NULL DEFAULT 'i',
has_default INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (proc_oid, ord)
);
-- sql_operator: operator overloads.
-- left_type_oid is NULL for prefix unary; right_type_oid is NULL for postfix.
CREATE TABLE sql_operator (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
namespace_oid INTEGER REFERENCES sql_namespace(oid),
dialect_oid INTEGER REFERENCES sql_dialect(oid),
name TEXT NOT NULL,
left_type_oid INTEGER REFERENCES sql_type(oid),
right_type_oid INTEGER REFERENCES sql_type(oid),
result_type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
proc_oid INTEGER REFERENCES sql_proc(oid),
commutator_oid INTEGER REFERENCES sql_operator(oid),
negator_oid INTEGER REFERENCES sql_operator(oid)
);
-- sql_cast: type coercion rules.
-- context: 'i' = implicit, 'a' = assignment-only, 'e' = explicit-only
-- proc_oid NULL = binary-coercible (no function needed)
CREATE TABLE sql_cast (
source_type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
target_type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
proc_oid INTEGER REFERENCES sql_proc(oid),
context TEXT NOT NULL DEFAULT 'e',
dialect_oid INTEGER REFERENCES sql_dialect(oid),
PRIMARY KEY (source_type_oid, target_type_oid)
);
-- Resolution-speed indexes.
CREATE INDEX idx_sql_proc_name ON sql_proc(name, namespace_oid);
CREATE INDEX idx_sql_operator_name ON sql_operator(name, left_type_oid, right_type_oid);
CREATE INDEX idx_sql_attribute_name ON sql_attribute(name);

78
internal/core/class.go Normal file
View File

@@ -0,0 +1,78 @@
package core
import (
"context"
"fmt"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
func (c *Catalog) CreateClass(namespaceOID int64, name string, kind string) (int64, error) {
oid, err := c.q.CreateClass(context.Background(), catalogdb.CreateClassParams{
NamespaceOid: namespaceOID,
Name: name,
Kind: kind,
})
if err != nil {
return 0, fmt.Errorf("create class %q: %w", name, err)
}
return oid, nil
}
func (c *Catalog) ClassOID(namespaceOID int64, name string) (int64, error) {
oid, err := c.q.ClassOID(context.Background(), catalogdb.ClassOIDParams{
NamespaceOid: namespaceOID,
Name: name,
})
if err != nil {
return 0, fmt.Errorf("class %q: %w", name, err)
}
return oid, nil
}
func (c *Catalog) ClassOIDByName(name string) (int64, error) {
oid, err := c.q.ClassOIDByName(context.Background(), name)
if err != nil {
return 0, fmt.Errorf("class %q: %w", name, err)
}
return oid, nil
}
func (c *Catalog) DropClass(classOID int64) error {
ctx := context.Background()
if err := c.q.DeleteAttributesByClass(ctx, classOID); err != nil {
return fmt.Errorf("drop class %d attributes: %w", classOID, err)
}
if err := c.q.DeleteClass(ctx, classOID); err != nil {
return fmt.Errorf("drop class %d: %w", classOID, err)
}
return nil
}
func (c *Catalog) RenameClass(classOID int64, name string) error {
err := c.q.RenameClass(context.Background(), catalogdb.RenameClassParams{
Oid: classOID,
NewName: name,
})
if err != nil {
return fmt.Errorf("rename class %d: %w", classOID, err)
}
return nil
}
type ClassInfo struct {
OID int64
Name string
}
func (c *Catalog) TablesInNamespace(namespaceOID int64) ([]ClassInfo, error) {
rows, err := c.q.ListTablesInNamespace(context.Background(), namespaceOID)
if err != nil {
return nil, fmt.Errorf("list tables in namespace %d: %w", namespaceOID, err)
}
out := make([]ClassInfo, 0, len(rows))
for _, r := range rows {
out = append(out, ClassInfo{OID: r.Oid, Name: r.Name})
}
return out, nil
}

View File

@@ -0,0 +1,21 @@
package core
import (
"context"
"fmt"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
func (c *Catalog) CreateConstraint(classOID int64, name string, kind string, columns string) error {
err := c.q.CreateConstraint(context.Background(), catalogdb.CreateConstraintParams{
ClassOid: classOID,
Name: name,
Kind: kind,
Columns: columns,
})
if err != nil {
return fmt.Errorf("create constraint %q on class %d: %w", name, classOID, err)
}
return nil
}

171
internal/core/db.go Normal file
View File

@@ -0,0 +1,171 @@
package core
import (
"context"
"database/sql"
"sync"
)
// stmtCache is a catalogdb.DBTX that keeps one prepared statement per distinct
// query string.
//
// The catalog runs the same dozen or so statements over and over — seeding a
// dialect is hundreds of inserts through a handful of queries, and analysis
// issues one lookup per table, column, type, operator and function a statement
// touches. Handing the raw *sql.DB to catalogdb means SQLite re-parses and
// re-plans that SQL text on every call, which dominated both paths. Preparing
// once and rebinding is the whole win.
//
// Statements live for the lifetime of the catalog and are released by Close.
type stmtCache struct {
db *sql.DB
mu sync.RWMutex
stmts map[string]*sql.Stmt
// While a transaction is open every statement must be prepared on the
// transaction's own connection: the catalog pins the pool to a single
// connection, so reaching for the *sql.DB mid-transaction would block
// forever waiting for a connection the transaction already holds. These
// statements are owned by the transaction and die with it.
tx *sql.Tx
txStmts map[string]*sql.Stmt
}
func newStmtCache(db *sql.DB) *stmtCache {
return &stmtCache{db: db, stmts: make(map[string]*sql.Stmt)}
}
func (c *stmtCache) prepared(ctx context.Context, query string) (*sql.Stmt, error) {
c.mu.RLock()
tx, cache := c.tx, c.stmts
if tx != nil {
cache = c.txStmts
}
stmt, ok := cache[query]
c.mu.RUnlock()
if ok {
return stmt, nil
}
if tx != nil {
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
return nil, err
}
c.mu.Lock()
defer c.mu.Unlock()
// Bail out if the transaction ended while we were preparing; the
// statement is dead either way.
if c.tx != tx {
return stmt, nil
}
if existing, ok := c.txStmts[query]; ok {
stmt.Close()
return existing, nil
}
c.txStmts[query] = stmt
return stmt, nil
}
stmt, err := c.db.PrepareContext(ctx, query)
if err != nil {
return nil, err
}
c.mu.Lock()
defer c.mu.Unlock()
// Another goroutine may have prepared the same query while we were not
// holding the lock; keep the winner so each query has a single statement.
if existing, ok := c.stmts[query]; ok {
stmt.Close()
return existing, nil
}
c.stmts[query] = stmt
return stmt, nil
}
func (c *stmtCache) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
stmt, err := c.prepared(ctx, query)
if err != nil {
return nil, err
}
return stmt.ExecContext(ctx, args...)
}
func (c *stmtCache) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
stmt, err := c.prepared(ctx, query)
if err != nil {
return nil, err
}
return stmt.QueryContext(ctx, args...)
}
func (c *stmtCache) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
stmt, err := c.prepared(ctx, query)
if err != nil {
// *sql.Row carries an error but has no exported constructor, so fall
// back to the unprepared path and let it report the failure on Scan.
return c.db.QueryRowContext(ctx, query, args...)
}
return stmt.QueryRowContext(ctx, args...)
}
// PrepareContext returns a statement owned by the caller; it is deliberately
// not cached, since the caller is responsible for closing it.
func (c *stmtCache) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
return c.db.PrepareContext(ctx, query)
}
// begin opens a transaction that every subsequent statement runs inside, until
// commit or rollback. Batching writes this way saves SQLite a commit per
// statement, which is most of the cost of installing a dialect seed.
func (c *stmtCache) begin(ctx context.Context) error {
tx, err := c.db.BeginTx(ctx, nil)
if err != nil {
return err
}
c.mu.Lock()
c.tx = tx
c.txStmts = make(map[string]*sql.Stmt)
c.mu.Unlock()
return nil
}
// endTx detaches the current transaction and returns it, or nil if none is
// open. The transaction's statements are released along with it.
func (c *stmtCache) endTx() *sql.Tx {
c.mu.Lock()
defer c.mu.Unlock()
tx := c.tx
c.tx, c.txStmts = nil, nil
return tx
}
func (c *stmtCache) commit() error {
if tx := c.endTx(); tx != nil {
return tx.Commit()
}
return nil
}
func (c *stmtCache) rollback() {
if tx := c.endTx(); tx != nil {
tx.Rollback()
}
}
func (c *stmtCache) Close() error {
c.rollback()
c.mu.Lock()
defer c.mu.Unlock()
var firstErr error
for query, stmt := range c.stmts {
if err := stmt.Close(); err != nil && firstErr == nil {
firstErr = err
}
delete(c.stmts, query)
}
return firstErr
}

210
internal/core/dialect.go Normal file
View File

@@ -0,0 +1,210 @@
package core
import (
"context"
"fmt"
"slices"
"strings"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
func (c *Catalog) CreateDialect(name string) (int64, error) {
oid, err := c.q.CreateDialect(context.Background(), name)
if err != nil {
return 0, fmt.Errorf("create dialect %q: %w", name, err)
}
c.dialectOID = oid
return oid, nil
}
func (c *Catalog) DialectOID(name string) (int64, error) {
oid, err := c.q.DialectOID(context.Background(), name)
if err != nil {
return 0, fmt.Errorf("dialect %q: %w", name, err)
}
return oid, nil
}
func (c *Catalog) SetDialectFlag(dialectOID int64, key, value string) error {
err := c.q.SetDialectFlag(context.Background(), catalogdb.SetDialectFlagParams{
DialectOid: dialectOID,
Key: key,
Value: value,
})
if err != nil {
return fmt.Errorf("set dialect flag %s.%s: %w", key, value, err)
}
return nil
}
func (c *Catalog) DialectFlag(dialectOID int64, key string) (string, error) {
value, err := c.q.DialectFlag(context.Background(), catalogdb.DialectFlagParams{
DialectOid: dialectOID,
Key: key,
})
if err != nil {
return "", nil
}
return value, nil
}
// FlagComparisonOperators holds the dialect's comparison operators as a
// comma-separated list, so that a type declared by a schema can be given the
// same ones the seeded types have.
const FlagComparisonOperators = "operators.comparison"
// FlagBoolType holds the type a comparison or predicate yields, when it is
// not the type a boolean literal has: ClickHouse compares to UInt8 while
// writing true as Bool.
const FlagBoolType = "types.bool"
// FlagLimitType holds the type a LIMIT or OFFSET count has, which is what a
// placeholder in one is typed as.
const FlagLimitType = "types.limit"
// FlagUntypedType holds the type a placeholder takes when nothing in the
// query constrains it, for a dialect that gives such a placeholder one.
const FlagUntypedType = "types.untyped"
// FlagPropagateNullable is set when a function's result is nullable whenever
// one of its arguments is, the way ClickHouse's ordinary functions behave.
const FlagPropagateNullable = "functions.propagate_nullable"
// FlagQualifyDuplicateColumns is set for a dialect that names a result
// column after its relation when an earlier result column from another
// relation has the same name, as ClickHouse names the second id of a join
// e.id.
const FlagQualifyDuplicateColumns = "columns.qualify_duplicates"
// FlagDefaultSchema holds the schema the dialect puts an unqualified object
// in, when that is not the catalog's own default: a type in it is reported
// without its schema.
const FlagDefaultSchema = "schema.default"
// FlagCastCategories holds the categories whose types are all implicitly
// castable to one another, as the dialect's seed declared them, so that a type
// arriving after the seed — an extension's, say — can join its category.
const FlagCastCategories = "casts.categories"
// Constant kinds. A literal in a query has no declared type, so each dialect
// names the type its literals take on.
const (
ConstInteger = "integer"
ConstFloat = "float"
ConstString = "string"
ConstBool = "bool"
)
// defaultConstTypes are the PostgreSQL type names, used by any dialect that
// does not name its own.
var defaultConstTypes = map[string]string{
ConstInteger: "int4",
ConstFloat: "numeric",
ConstString: "text",
ConstBool: "bool",
}
// IsComparisonOperator reports whether the dialect counts name among the
// operators that compare two values and yield a boolean.
func (c *Catalog) IsComparisonOperator(name string) bool {
if c.dialectOID == 0 {
return false
}
ops, _ := c.DialectFlag(c.dialectOID, FlagComparisonOperators)
if ops == "" {
return false
}
return slices.Contains(strings.Split(ops, ","), name)
}
// SetConstType records the type a literal of the given kind takes on in this
// dialect.
func (c *Catalog) SetConstType(dialectOID int64, kind, typeName string) error {
return c.SetDialectFlag(dialectOID, "const."+kind, typeName)
}
// ConstTypeOID returns the type a literal of the given kind takes on.
func (c *Catalog) ConstTypeOID(kind string) (int64, error) {
if c.dialectOID != 0 {
if name, _ := c.DialectFlag(c.dialectOID, "const."+kind); name != "" {
return c.TypeOID(name)
}
}
name, ok := defaultConstTypes[kind]
if !ok {
return 0, fmt.Errorf("unknown constant kind %q", kind)
}
return c.TypeOID(name)
}
// BoolTypeOID returns the type a comparison or predicate yields.
func (c *Catalog) BoolTypeOID() (int64, error) {
if c.dialectOID != 0 {
if name, _ := c.DialectFlag(c.dialectOID, FlagBoolType); name != "" {
return c.TypeOID(name)
}
}
return c.ConstTypeOID(ConstBool)
}
// LimitTypeOID returns the type a LIMIT or OFFSET count has, falling back to
// the type of an integer literal.
func (c *Catalog) LimitTypeOID() (int64, error) {
if c.dialectOID != 0 {
if name, _ := c.DialectFlag(c.dialectOID, FlagLimitType); name != "" {
return c.TypeOID(name)
}
}
return c.ConstTypeOID(ConstInteger)
}
// UntypedTypeOID returns the type an unconstrained placeholder takes, and
// whether the dialect gives it one at all.
func (c *Catalog) UntypedTypeOID() (int64, bool) {
if c.dialectOID == 0 {
return 0, false
}
name, _ := c.DialectFlag(c.dialectOID, FlagUntypedType)
if name == "" {
return 0, false
}
oid, err := c.TypeOID(name)
if err != nil {
return 0, false
}
return oid, true
}
// PropagatesNullable reports whether a function's result is nullable
// whenever one of its arguments is.
func (c *Catalog) PropagatesNullable() bool {
if c.dialectOID == 0 {
return false
}
v, _ := c.DialectFlag(c.dialectOID, FlagPropagateNullable)
return v == "true"
}
// QualifiesDuplicateColumns reports whether a result column that repeats an
// earlier one's name from another relation is named after its relation.
func (c *Catalog) QualifiesDuplicateColumns() bool {
if c.dialectOID == 0 {
return false
}
v, _ := c.DialectFlag(c.dialectOID, FlagQualifyDuplicateColumns)
return v == "true"
}
// DefaultNamespaces lists the namespaces a type is reported from without
// qualification: the catalog's default, PostgreSQL's system catalog, and
// the dialect's own default schema when it names one.
func (c *Catalog) DefaultNamespaces() []string {
out := []string{"public", "pg_catalog"}
if c.dialectOID != 0 {
if name, _ := c.DialectFlag(c.dialectOID, FlagDefaultSchema); name != "" {
out = append(out, name)
}
}
return out
}

View File

@@ -0,0 +1,39 @@
package core
import (
"context"
"fmt"
)
func (c *Catalog) CreateNamespace(name string) (int64, error) {
oid, err := c.q.CreateNamespace(context.Background(), name)
if err != nil {
return 0, fmt.Errorf("create namespace %q: %w", name, err)
}
return oid, nil
}
func (c *Catalog) NamespaceOID(name string) (int64, error) {
oid, err := c.q.NamespaceOID(context.Background(), name)
if err != nil {
return 0, fmt.Errorf("namespace %q: %w", name, err)
}
return oid, nil
}
type NamespaceInfo struct {
OID int64
Name string
}
func (c *Catalog) Namespaces() ([]NamespaceInfo, error) {
rows, err := c.q.ListNamespaces(context.Background())
if err != nil {
return nil, fmt.Errorf("list namespaces: %w", err)
}
out := make([]NamespaceInfo, 0, len(rows))
for _, r := range rows {
out = append(out, NamespaceInfo{OID: r.Oid, Name: r.Name})
}
return out, nil
}

68
internal/core/operator.go Normal file
View File

@@ -0,0 +1,68 @@
package core
import (
"context"
"fmt"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
type OperatorSpec struct {
Name string
NamespaceOID int64
DialectOID int64
LeftTypeOID int64
RightTypeOID int64
ResultTypeOID int64
ProcOID int64
}
func (c *Catalog) CreateOperator(o OperatorSpec) (int64, error) {
oid, err := c.q.CreateOperator(context.Background(), catalogdb.CreateOperatorParams{
NamespaceOid: nullInt64(o.NamespaceOID),
DialectOid: nullInt64(o.DialectOID),
Name: o.Name,
LeftTypeOid: nullInt64(o.LeftTypeOID),
RightTypeOid: nullInt64(o.RightTypeOID),
ResultTypeOid: o.ResultTypeOID,
ProcOid: nullInt64(o.ProcOID),
})
if err != nil {
return 0, fmt.Errorf("create operator %q: %w", o.Name, err)
}
return oid, nil
}
type OperatorOverload struct {
OID int64
Name string
LeftTypeOID int64
RightTypeOID int64
ResultTypeOID int64
ProcOID int64
}
// FindOperators returns the overloads of name matching the given operand types,
// where a zero OID means "any".
func (c *Catalog) FindOperators(name string, leftTypeOID, rightTypeOID int64) ([]OperatorOverload, error) {
rows, err := c.q.FindOperators(context.Background(), catalogdb.FindOperatorsParams{
Name: name,
LeftTypeOid: leftTypeOID,
RightTypeOid: rightTypeOID,
})
if err != nil {
return nil, fmt.Errorf("find operators %q: %w", name, err)
}
out := make([]OperatorOverload, 0, len(rows))
for _, r := range rows {
out = append(out, OperatorOverload{
OID: r.Oid,
Name: r.Name,
LeftTypeOID: orZero(r.LeftTypeOid),
RightTypeOID: orZero(r.RightTypeOid),
ResultTypeOID: r.ResultTypeOid,
ProcOID: orZero(r.ProcOid),
})
}
return out, nil
}

173
internal/core/proc.go Normal file
View File

@@ -0,0 +1,173 @@
package core
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
type ProcSpec struct {
Name string
NamespaceOID int64
DialectOID int64
Kind string
ReturnTypeOID int64
ReturnSet bool
ReturnNullable bool
// NeverNull marks a function whose result is never NULL even when an
// argument is, in a dialect that otherwise propagates nullability.
NeverNull bool
// ReturnTemplate is the result as an expression over the call's
// arguments, when it depends on their values: Decimal(18, $2) for
// toDecimal64(x, s).
ReturnTemplate string
Strict bool
VariadicKind string
Args []ProcArg
}
// The proc table stores nullability as one integer: 0 leaves it to the
// dialect's rule, 1 is always nullable and 2 is never nullable.
const (
nullableDefault int64 = 0
nullableAlways int64 = 1
nullableNever int64 = 2
)
type ProcArg struct {
Name string
TypeOID int64
Mode string
HasDefault bool
}
func (c *Catalog) CreateProc(p ProcSpec) (int64, error) {
if p.Kind == "" {
p.Kind = "f"
}
if p.VariadicKind == "" {
p.VariadicKind = "n"
}
ctx := context.Background()
procOID, err := c.q.CreateProc(ctx, catalogdb.CreateProcParams{
NamespaceOid: nullInt64(p.NamespaceOID),
DialectOid: nullInt64(p.DialectOID),
Name: strings.ToLower(p.Name),
Kind: p.Kind,
ReturnTypeOid: p.ReturnTypeOID,
ReturnSet: boolToInt64(p.ReturnSet),
ReturnNullable: returnNullable(p),
ReturnTemplate: p.ReturnTemplate,
Strict: boolToInt64(p.Strict),
VariadicKind: p.VariadicKind,
})
if err != nil {
return 0, fmt.Errorf("create proc %q: %w", p.Name, err)
}
for i, a := range p.Args {
mode := a.Mode
if mode == "" {
mode = "i"
}
err := c.q.CreateProcArg(ctx, catalogdb.CreateProcArgParams{
ProcOid: procOID,
Ord: int64(i + 1),
Name: a.Name,
TypeOid: a.TypeOID,
Mode: mode,
HasDefault: boolToInt64(a.HasDefault),
})
if err != nil {
return 0, fmt.Errorf("create proc %q arg %d: %w", p.Name, i+1, err)
}
}
return procOID, nil
}
func returnNullable(p ProcSpec) int64 {
switch {
case p.NeverNull:
return nullableNever
case p.ReturnNullable:
return nullableAlways
}
return nullableDefault
}
type ProcOverload struct {
OID int64
Name string
Kind string
ReturnTypeOID int64
ReturnNullable bool
NeverNull bool
ReturnTemplate string
ArgTypes []int64
}
// FindProcs returns the overloads of name, optionally restricted to the given
// namespaces.
func (c *Catalog) FindProcs(name string, namespaceOIDs []int64) ([]ProcOverload, error) {
ctx := context.Background()
lname := strings.ToLower(name)
var out []ProcOverload
if len(namespaceOIDs) == 0 {
rows, err := c.q.FindProcsAnyNamespace(ctx, lname)
if err != nil {
return nil, fmt.Errorf("find procs %q: %w", name, err)
}
out = make([]ProcOverload, 0, len(rows))
for _, r := range rows {
out = append(out, ProcOverload{
OID: r.Oid,
Name: r.Name,
Kind: r.Kind,
ReturnTypeOID: r.ReturnTypeOid,
ReturnNullable: r.ReturnNullable == nullableAlways,
NeverNull: r.ReturnNullable == nullableNever,
ReturnTemplate: r.ReturnTemplate,
})
}
} else {
nss := make([]sql.NullInt64, len(namespaceOIDs))
for i, ns := range namespaceOIDs {
nss[i] = sql.NullInt64{Int64: ns, Valid: true}
}
rows, err := c.q.FindProcsInNamespaces(ctx, catalogdb.FindProcsInNamespacesParams{
Name: lname,
NamespaceOids: nss,
})
if err != nil {
return nil, fmt.Errorf("find procs %q: %w", name, err)
}
out = make([]ProcOverload, 0, len(rows))
for _, r := range rows {
out = append(out, ProcOverload{
OID: r.Oid,
Name: r.Name,
Kind: r.Kind,
ReturnTypeOID: r.ReturnTypeOid,
ReturnNullable: r.ReturnNullable == nullableAlways,
NeverNull: r.ReturnNullable == nullableNever,
ReturnTemplate: r.ReturnTemplate,
})
}
}
for i := range out {
argTypes, err := c.procArgTypes(out[i].OID)
if err != nil {
return nil, err
}
out[i].ArgTypes = argTypes
}
return out, nil
}
func (c *Catalog) procArgTypes(procOID int64) ([]int64, error) {
return c.q.ProcArgTypes(context.Background(), procOID)
}

361
internal/core/rewrite.go Normal file
View File

@@ -0,0 +1,361 @@
package core
import (
"context"
"fmt"
"strconv"
"strings"
"sync"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
// A dialect describes, as data, what it does to a type before storing it.
// The seed loads three kinds of it from dialect.json: rewrites, which turn
// one expression into another (float(24) into real, a bare decimal into
// decimal(18, 0)); identifier words and positions, which say where a bare
// word is a word rather than a type (the max of nvarchar(max), the
// function of SimpleAggregateFunction(sum, UInt64)); and an affinity rule,
// which says what a family the schema declares and the seed does not list
// stands on. None of it is code, so an engine adds a dialect by writing
// files.
// FlagIdents holds the words that are identifiers wherever they stand as a
// type argument, comma-separated.
const FlagIdents = "types.idents"
// FlagIdentArgs holds the argument positions that are identifiers in a
// family, as "family:1,2;family:1".
const FlagIdentArgs = "types.ident_args"
// typeRewrite is one rewrite, parsed: a pattern whose $n arguments bind
// whatever stands there, a template the bindings are substituted into, and
// a condition on a binding.
type typeRewrite struct {
pattern *TypeExpr
template *TypeExpr
cond rewriteCond
}
// rewriteCond bounds an integer binding: "$1 <= 24".
type rewriteCond struct {
binding string
op string
value int64
}
// rules is what the catalog knows of its dialect's rewriting, read from the
// tables once and kept. The seed invalidates it as it adds to them.
type rules struct {
mu sync.Mutex
loaded bool
rewrites []typeRewrite
idents map[string]bool
identArgs map[string][]int
}
func (c *Catalog) invalidateRules() {
c.rules.mu.Lock()
c.rules.loaded = false
c.rules.mu.Unlock()
}
// loadRules reads the dialect's rewrites and identifier settings.
func (c *Catalog) loadRules() (*rules, error) {
r := &c.rules
r.mu.Lock()
defer r.mu.Unlock()
if r.loaded {
return r, nil
}
r.rewrites, r.idents, r.identArgs = nil, map[string]bool{}, map[string][]int{}
if c.dialectOID == 0 {
r.loaded = true
return r, nil
}
rows, err := c.q.ListTypeRewrites(context.Background(), c.dialectOID)
if err != nil {
return nil, fmt.Errorf("type rewrites: %w", err)
}
for _, row := range rows {
rw := typeRewrite{pattern: ParseTypeExpr(row.Pattern), template: ParseTypeExpr(row.Template)}
if row.Cond != "" {
cond, err := parseRewriteCond(row.Cond)
if err != nil {
return nil, fmt.Errorf("type rewrite %q: %w", row.Pattern, err)
}
rw.cond = cond
}
r.rewrites = append(r.rewrites, rw)
}
if idents, _ := c.DialectFlag(c.dialectOID, FlagIdents); idents != "" {
for _, w := range strings.Split(idents, ",") {
r.idents[strings.ToLower(strings.TrimSpace(w))] = true
}
}
if identArgs, _ := c.DialectFlag(c.dialectOID, FlagIdentArgs); identArgs != "" {
for _, entry := range strings.Split(identArgs, ";") {
family, positions, ok := strings.Cut(entry, ":")
if !ok {
continue
}
for _, p := range strings.Split(positions, ",") {
if n, err := strconv.Atoi(strings.TrimSpace(p)); err == nil {
family = strings.ToLower(strings.TrimSpace(family))
r.identArgs[family] = append(r.identArgs[family], n)
}
}
}
}
r.loaded = true
return r, nil
}
// parseRewriteCond reads "$1 <= 24".
func parseRewriteCond(s string) (rewriteCond, error) {
fields := strings.Fields(s)
if len(fields) != 3 || !strings.HasPrefix(fields[0], "$") {
return rewriteCond{}, fmt.Errorf("condition %q: want \"$n op value\"", s)
}
switch fields[1] {
case "<", "<=", "=", ">=", ">", "!=":
default:
return rewriteCond{}, fmt.Errorf("condition %q: unknown operator", s)
}
v, err := strconv.ParseInt(fields[2], 10, 64)
if err != nil {
return rewriteCond{}, fmt.Errorf("condition %q: %w", s, err)
}
return rewriteCond{binding: fields[0], op: fields[1], value: v}, nil
}
func (cond rewriteCond) holds(bindings map[string]TypeArg) bool {
if cond.binding == "" {
return true
}
a, ok := bindings[cond.binding]
if !ok || a.Int == nil {
return false
}
switch cond.op {
case "<":
return *a.Int < cond.value
case "<=":
return *a.Int <= cond.value
case "=":
return *a.Int == cond.value
case ">=":
return *a.Int >= cond.value
case ">":
return *a.Int > cond.value
case "!=":
return *a.Int != cond.value
}
return false
}
// AddTypeRewrite records a rewrite of the catalog's dialect, applied after
// the ones recorded before it.
func (c *Catalog) AddTypeRewrite(ord int, pattern, template, cond string) error {
err := c.q.CreateTypeRewrite(context.Background(), catalogdb.CreateTypeRewriteParams{
DialectOid: c.dialectOID,
Ord: int64(ord),
Pattern: pattern,
Template: template,
Cond: cond,
})
if err != nil {
return fmt.Errorf("type rewrite %q: %w", pattern, err)
}
c.invalidateRules()
return nil
}
// AddTypeAffinity records the next step of the dialect's affinity rule: a
// family whose name contains one of words, upper-cased, stands on typeOID.
// No words is the default that ends the rule.
func (c *Catalog) AddTypeAffinity(ord int, words []string, typeOID int64) error {
err := c.q.CreateTypeAffinity(context.Background(), catalogdb.CreateTypeAffinityParams{
DialectOid: c.dialectOID,
Ord: int64(ord),
Words: strings.ToUpper(strings.Join(words, ",")),
TypeOid: typeOID,
})
if err != nil {
return fmt.Errorf("type affinity %d: %w", ord, err)
}
return nil
}
// userTypeBase is the type an unseeded family stands on by the dialect's
// affinity rule, or 0 when the dialect has none or none of it matches.
func (c *Catalog) userTypeBase(name string) (int64, error) {
if c.dialectOID == 0 {
return 0, nil
}
rows, err := c.q.ListTypeAffinities(context.Background(), c.dialectOID)
if err != nil {
return 0, fmt.Errorf("type affinities: %w", err)
}
upper := strings.ToUpper(name)
for _, row := range rows {
if row.Words == "" {
return row.TypeOid, nil
}
for _, w := range strings.Split(row.Words, ",") {
if w != "" && strings.Contains(upper, w) {
return row.TypeOid, nil
}
}
}
return 0, nil
}
// canonicalize applies the dialect's identifier settings and rewrites to
// an expression: the first rewrite whose pattern matches is applied, and
// its result is not rewritten again. A rewrite names a family as the
// dialect spells it, so an expression spelled with an alias — dec(10) for
// a rule on decimal — is tried again with the alias resolved.
func (c *Catalog) canonicalize(t *TypeExpr) (*TypeExpr, error) {
r, err := c.loadRules()
if err != nil {
return nil, err
}
t = r.identify(t)
if len(r.rewrites) == 0 {
return t, nil
}
if out, ok := r.rewrite(t); ok {
return out, nil
}
if canonical, ok := c.canonicalFamilyName(t.Name); ok && !strings.EqualFold(canonical, t.Name) {
resolved := t.Clone()
resolved.Name = canonical
if out, ok := r.rewrite(resolved); ok {
return out, nil
}
}
return t, nil
}
// rewrite applies the first rewrite whose pattern matches the expression.
func (r *rules) rewrite(t *TypeExpr) (*TypeExpr, bool) {
name := strings.ToLower(t.Name)
for _, rw := range r.rewrites {
if strings.ToLower(rw.pattern.Name) != name || len(rw.pattern.Args) != len(t.Args) {
continue
}
bindings, ok := matchArgs(rw.pattern.Args, t.Args)
if !ok || !rw.cond.holds(bindings) {
continue
}
out := substitute(rw.template, bindings)
out.Nullable = t.Nullable
return out, true
}
return nil, false
}
// canonicalFamilyName is the name the catalog spells a family by, with
// aliases resolved, or false when the name is not a family it holds.
func (c *Catalog) canonicalFamilyName(name string) (string, bool) {
oid, err := c.familyOIDByQualifiedName(strings.ToLower(strings.TrimSpace(name)))
if err != nil {
return "", false
}
if oid, err = c.canonicalOID(oid); err != nil {
return "", false
}
info, err := c.LookupType(oid)
if err != nil {
return "", false
}
return c.qualifiedName(info), true
}
// identify turns the bare words the dialect calls identifiers into
// identifier arguments.
func (r *rules) identify(t *TypeExpr) *TypeExpr {
if len(t.Args) == 0 || (len(r.idents) == 0 && len(r.identArgs) == 0) {
return t
}
positions := r.identArgs[strings.ToLower(t.Name)]
var out *TypeExpr
for i, a := range t.Args {
if a.Type == nil || len(a.Type.Args) != 0 {
continue
}
word := strings.ToLower(a.Type.Name)
if !r.idents[word] && !containsInt(positions, i+1) {
continue
}
if out == nil {
out = t.Clone()
}
out.Args[i] = TypeArg{Label: a.Label, Ident: &word}
}
if out == nil {
return t
}
return out
}
func containsInt(list []int, n int) bool {
for _, v := range list {
if v == n {
return true
}
}
return false
}
// matchArgs matches a pattern's arguments against an expression's, binding
// each $n to what stands in its place and requiring a literal to be equal.
func matchArgs(pattern, args []TypeArg) (map[string]TypeArg, bool) {
bindings := map[string]TypeArg{}
for i, p := range pattern {
a := args[i]
switch {
case p.Type != nil && strings.HasPrefix(p.Type.Name, "$"):
bindings[p.Type.Name] = a
case p.Type != nil:
if a.Type == nil || !strings.EqualFold(a.Type.Name, p.Type.Name) {
return nil, false
}
case p.Int != nil:
if a.Int == nil || *a.Int != *p.Int {
return nil, false
}
case p.String != nil:
if a.String == nil || *a.String != *p.String {
return nil, false
}
case p.Ident != nil:
if a.Ident == nil || *a.Ident != *p.Ident {
return nil, false
}
default:
return nil, false
}
}
return bindings, true
}
// substitute fills a template's $n arguments from the bindings.
func substitute(template *TypeExpr, bindings map[string]TypeArg) *TypeExpr {
out := template.Clone()
for i, a := range out.Args {
if a.Type != nil && strings.HasPrefix(a.Type.Name, "$") {
if bound, ok := bindings[a.Type.Name]; ok {
label := a.Label
out.Args[i] = bound
if label != "" {
out.Args[i].Label = label
}
}
} else if a.Type != nil {
out.Args[i].Type = substitute(a.Type, bindings)
}
}
return out
}

View File

@@ -0,0 +1,570 @@
package schema
import (
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/core/analyzer"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
func Apply(cat *core.Catalog, n ast.Node) error {
switch v := n.(type) {
case nil:
return nil
case *ast.RawStmt:
return Apply(cat, v.Stmt)
case *ast.List:
for _, it := range v.Items {
if err := Apply(cat, it); err != nil {
return err
}
}
return nil
case *ast.CreateTableStmt:
return applyCreateTable(cat, v)
case *ast.DropTableStmt:
return applyDropTable(cat, v)
case *ast.CreateEnumStmt:
return applyCreateEnum(cat, v)
case *ast.CreateDomainStmt:
return applyCreateDomain(cat, v)
case *ast.CompositeTypeStmt:
return applyCompositeType(cat, v)
case *ast.CreateRangeStmt:
return applyCreateRange(cat, v)
case *ast.CreateExtensionStmt:
if v.Extname == nil {
return nil
}
return cat.LoadExtension(*v.Extname)
case *ast.CreateFunctionStmt:
return applyCreateFunction(cat, v)
case *ast.AlterTableStmt:
return applyAlterTable(cat, v)
case *ast.RenameColumnStmt:
return applyRenameColumn(cat, v)
case *ast.RenameTableStmt:
return applyRenameTable(cat, v)
case *ast.ViewStmt:
return applyView(cat, v.View, v.Aliases, v.Query, v.Replace)
case *ast.CreateTableAsStmt:
if v.Into == nil {
return nil
}
return applyView(cat, v.Into.Rel, v.Into.ColNames, v.Query, false)
}
return nil
}
// applyView records a view, or a table created from a query, as a relation
// whose columns are the ones its query selects.
func applyView(cat *core.Catalog, rel *ast.RangeVar, aliases *ast.List, query ast.Node, replace bool) error {
if rel == nil || rel.Relname == nil {
return fmt.Errorf("create view with nil name")
}
name := *rel.Relname
schema := ""
if rel.Schemaname != nil {
schema = *rel.Schemaname
}
nsOID, err := resolveOrCreateNamespace(cat, schema)
if err != nil {
return err
}
if existing, err := cat.ClassOID(nsOID, name); err == nil {
if !replace {
return fmt.Errorf("relation %q already exists", name)
}
if err := cat.DropClass(existing); err != nil {
return err
}
}
sel, ok := query.(*ast.SelectStmt)
if !ok {
return fmt.Errorf("view %q: unsupported query %T", name, query)
}
res, err := analyzer.Prepare(cat, sel)
if err != nil {
return fmt.Errorf("view %q: %w", name, err)
}
classOID, err := cat.CreateClass(nsOID, name, "v")
if err != nil {
return err
}
names := listStrings(aliases)
for i, col := range res.Columns {
colName := col.Name
if i < len(names) {
colName = names[i]
}
typeOID := col.TypeOID
if typeOID == 0 {
typeOID, err = cat.ResolveTypeName("any")
if err != nil {
return err
}
}
if err := cat.CreateAttributeSpec(core.AttributeSpec{
ClassOID: classOID,
Name: colName,
TypeOID: typeOID,
Num: i + 1,
NotNull: col.NotNull,
DeclType: col.DataType,
}); err != nil {
return fmt.Errorf("view %s.%s: %w", name, colName, err)
}
}
return nil
}
func listStrings(l *ast.List) []string {
if l == nil {
return nil
}
out := make([]string, 0, len(l.Items))
for _, item := range l.Items {
if s, ok := item.(*ast.String); ok {
out = append(out, s.Str)
}
}
return out
}
func applyCreateTable(cat *core.Catalog, stmt *ast.CreateTableStmt) error {
if stmt.Name == nil {
return fmt.Errorf("create table with nil name")
}
// A virtual table's module is the dialect's word for the extension it
// needs, the way CREATE EXTENSION is PostgreSQL's.
if stmt.Using != "" {
if err := cat.LoadExtension(stmt.Using); err != nil {
return err
}
}
nsOID, err := resolveOrCreateNamespace(cat, stmt.Name.Schema)
if err != nil {
return err
}
if _, err := cat.ClassOID(nsOID, stmt.Name.Name); err == nil {
if stmt.IfNotExists {
return nil
}
return fmt.Errorf("relation %q already exists", stmt.Name.Name)
}
classOID, err := cat.CreateClass(nsOID, stmt.Name.Name, "r")
if err != nil {
return err
}
for i, col := range stmt.Cols {
if col == nil || col.TypeName == nil {
return fmt.Errorf("column %d on %q: missing type", i+1, stmt.Name.Name)
}
typeOID, err := columnTypeOID(cat, col)
if err != nil {
return fmt.Errorf("column %s.%s: %w", stmt.Name.Name, col.Colname, err)
}
if err := cat.CreateAttributeSpec(core.AttributeSpec{
ClassOID: classOID,
Name: col.Colname,
TypeOID: typeOID,
Num: i + 1,
NotNull: col.IsNotNull || col.PrimaryKey || typeNotNull(cat, typeOID),
IsPrimaryKey: col.PrimaryKey,
DeclType: declType(col.TypeName),
Hidden: col.IsHidden,
}); err != nil {
return fmt.Errorf("attr %s.%s: %w", stmt.Name.Name, col.Colname, err)
}
}
return nil
}
func applyDropTable(cat *core.Catalog, stmt *ast.DropTableStmt) error {
for _, tn := range stmt.Tables {
if tn == nil {
continue
}
nsOID, err := cat.NamespaceOID(nsName(tn.Schema))
if err != nil {
if stmt.IfExists {
continue
}
return err
}
classOID, err := cat.ClassOID(nsOID, tn.Name)
if err != nil {
if stmt.IfExists {
continue
}
return fmt.Errorf("drop table %q: %w", tn.Name, err)
}
if err := cat.DropClass(classOID); err != nil {
return fmt.Errorf("drop table %q: %w", tn.Name, err)
}
}
return nil
}
func applyAlterTable(cat *core.Catalog, stmt *ast.AlterTableStmt) error {
table := stmt.Table
if table == nil && stmt.Relation != nil {
table = rangeVarTableName(stmt.Relation)
}
classOID, err := lookupClass(cat, table)
if err != nil {
if stmt.MissingOk {
return nil
}
return err
}
for _, item := range listItems(stmt.Cmds) {
cmd, ok := item.(*ast.AlterTableCmd)
if !ok {
continue
}
switch cmd.Subtype {
case ast.AT_AddColumn:
if cmd.Def == nil {
continue
}
typeOID, err := columnTypeOID(cat, cmd.Def)
if err != nil {
return err
}
num, err := cat.NextAttributeNum(classOID)
if err != nil {
return err
}
if err := cat.CreateAttributeSpec(core.AttributeSpec{
ClassOID: classOID,
Name: cmd.Def.Colname,
TypeOID: typeOID,
Num: num,
NotNull: cmd.Def.IsNotNull || cmd.Def.PrimaryKey || typeNotNull(cat, typeOID),
IsPrimaryKey: cmd.Def.PrimaryKey,
DeclType: declType(cmd.Def.TypeName),
}); err != nil {
return err
}
case ast.AT_DropColumn:
if cmd.Name == nil {
continue
}
if err := cat.DropAttribute(classOID, *cmd.Name); err != nil {
return err
}
case ast.AT_AlterColumnType:
if cmd.Def == nil {
continue
}
name := cmd.Def.Colname
if cmd.Name != nil && *cmd.Name != "" {
name = *cmd.Name
}
typeOID, err := columnTypeOID(cat, cmd.Def)
if err != nil {
return err
}
if err := cat.SetAttributeType(classOID, name, typeOID, declType(cmd.Def.TypeName)); err != nil {
return err
}
// An engine that reports a column's whole new definition also
// reports whether it still accepts NULL.
if cmd.Def.IsNotNull {
if err := cat.SetAttributeNotNull(classOID, name, true); err != nil {
return err
}
}
case ast.AT_SetNotNull, ast.AT_DropNotNull:
if cmd.Name == nil {
continue
}
if err := cat.SetAttributeNotNull(classOID, *cmd.Name, cmd.Subtype == ast.AT_SetNotNull); err != nil {
return err
}
}
}
return nil
}
func applyRenameColumn(cat *core.Catalog, stmt *ast.RenameColumnStmt) error {
if stmt.Col == nil || stmt.NewName == nil {
return nil
}
classOID, err := lookupClass(cat, stmt.Table)
if err != nil {
if stmt.MissingOk {
return nil
}
return err
}
// Engines report the column either as a field list or as a bare name.
name := stmt.Col.Name
if names := listStrings(stmt.Col.Fields); len(names) > 0 {
name = names[len(names)-1]
}
if name == "" {
return nil
}
return cat.RenameAttribute(classOID, name, *stmt.NewName)
}
func applyRenameTable(cat *core.Catalog, stmt *ast.RenameTableStmt) error {
if stmt.NewName == nil {
return nil
}
classOID, err := lookupClass(cat, stmt.Table)
if err != nil {
if stmt.MissingOk {
return nil
}
return err
}
return cat.RenameClass(classOID, *stmt.NewName)
}
func lookupClass(cat *core.Catalog, table *ast.TableName) (int64, error) {
if table == nil {
return 0, fmt.Errorf("missing table name")
}
nsOID, err := cat.NamespaceOID(nsName(table.Schema))
if err != nil {
return 0, err
}
return cat.ClassOID(nsOID, table.Name)
}
func rangeVarTableName(rv *ast.RangeVar) *ast.TableName {
tn := &ast.TableName{}
if rv.Schemaname != nil {
tn.Schema = *rv.Schemaname
}
if rv.Relname != nil {
tn.Name = *rv.Relname
}
return tn
}
func listItems(l *ast.List) []ast.Node {
if l == nil {
return nil
}
return l.Items
}
// applyCreateEnum records an enum as a type whose arguments are its labels,
// in order, the way pg_enum keeps them.
func applyCreateEnum(cat *core.Catalog, stmt *ast.CreateEnumStmt) error {
if stmt.TypeName == nil {
return fmt.Errorf("create type with nil name")
}
name := declaredTypeName(stmt.TypeName)
if name == "" {
return fmt.Errorf("create type with empty name")
}
if cat.TypeDeclared(name) {
return nil
}
var labels []core.TypeArg
for _, label := range listStrings(stmt.Vals) {
l := label
labels = append(labels, core.TypeArg{String: &l})
}
_, err := cat.CreateTypeWithArgs(core.TypeSpec{Name: name, Typtype: "e", Category: "E"}, labels)
return err
}
// applyCreateDomain records a domain: a type of its own that stands on its
// base, which is what it resolves through, and that may forbid NULL.
func applyCreateDomain(cat *core.Catalog, stmt *ast.CreateDomainStmt) error {
name := strings.ToLower(strings.Join(listStrings(stmt.Domainname), "."))
if name == "" || stmt.TypeName == nil {
return fmt.Errorf("create domain: missing name or type")
}
if cat.TypeDeclared(name) {
return nil
}
baseOID, err := cat.ResolveType(stmt.TypeName)
if err != nil {
return fmt.Errorf("domain %q: %w", name, err)
}
base, err := cat.LookupType(baseOID)
if err != nil {
return err
}
notNull := false
for _, item := range listItems(stmt.Constraints) {
if con, ok := item.(*ast.Constraint); ok && con.Contype == ast.ConstrTypeNotNull {
notNull = true
}
}
_, err = cat.CreateTypeWithArgs(core.TypeSpec{
Name: name,
Typtype: "d",
Category: base.Category,
BaseOID: baseOID,
NotNull: notNull,
}, nil)
return err
}
// applyCompositeType records a composite type as a type whose arguments are
// its fields, labelled by name.
func applyCompositeType(cat *core.Catalog, stmt *ast.CompositeTypeStmt) error {
if stmt.TypeName == nil {
return fmt.Errorf("create type with nil name")
}
name := declaredTypeName(stmt.TypeName)
if name == "" {
return fmt.Errorf("create type with empty name")
}
if cat.TypeDeclared(name) {
return nil
}
var fields []core.TypeArg
for _, item := range listItems(stmt.Coldeflist) {
col, ok := item.(*ast.ColumnDef)
if !ok || col.TypeName == nil {
continue
}
t := core.ColumnTypeExpr(col)
if t == nil {
continue
}
fields = append(fields, core.TypeArg{Label: col.Colname, Type: t})
}
_, err := cat.CreateTypeWithArgs(core.TypeSpec{Name: name, Typtype: "c", Category: "C"}, fields)
return err
}
// applyCreateRange records a range type over its subtype, which is what a
// bound of it has.
func applyCreateRange(cat *core.Catalog, stmt *ast.CreateRangeStmt) error {
name := strings.ToLower(strings.Join(listStrings(stmt.TypeName), "."))
if name == "" {
return fmt.Errorf("create type with empty name")
}
if cat.TypeDeclared(name) {
return nil
}
spec := core.TypeSpec{Name: name, Typtype: "r", Category: "R"}
for _, item := range listItems(stmt.Params) {
def, ok := item.(*ast.DefElem)
if !ok || def.Defname == nil || *def.Defname != "subtype" {
continue
}
tn, ok := def.Arg.(*ast.TypeName)
if !ok {
continue
}
oid, err := cat.ResolveType(tn)
if err != nil {
return fmt.Errorf("range %q: %w", name, err)
}
spec.ElementOID = oid
}
_, err := cat.CreateTypeWithArgs(spec, nil)
return err
}
// declaredTypeName is the name a CREATE TYPE gives, qualified by its schema
// when it names one.
func declaredTypeName(tn *ast.TypeName) string {
t := core.TypeExprOfTypeName(tn)
if t == nil {
return ""
}
if tn.Schema != "" && !strings.Contains(t.Name, ".") {
return strings.ToLower(tn.Schema) + "." + t.Name
}
return t.Name
}
func applyCreateFunction(cat *core.Catalog, stmt *ast.CreateFunctionStmt) error {
// A procedure returns nothing, so there is no result for a query to
// select and nothing worth recording.
if stmt.Func == nil || stmt.Func.Name == "" || stmt.ReturnType == nil {
return nil
}
returnOID, err := cat.ResolveType(stmt.ReturnType)
if err != nil {
return fmt.Errorf("function %q: %w", stmt.Func.Name, err)
}
var args []core.ProcArg
if stmt.Params != nil {
for _, item := range stmt.Params.Items {
p, ok := item.(*ast.FuncParam)
if !ok {
continue
}
switch p.Mode {
case ast.FuncParamOut, ast.FuncParamTable:
continue
}
argOID, err := cat.ResolveType(p.Type)
if err != nil {
return fmt.Errorf("function %q: %w", stmt.Func.Name, err)
}
arg := core.ProcArg{TypeOID: argOID, HasDefault: p.DefExpr != nil}
if p.Name != nil {
arg.Name = *p.Name
}
args = append(args, arg)
}
}
_, err = cat.CreateProc(core.ProcSpec{
Name: stmt.Func.Name,
ReturnTypeOID: returnOID,
ReturnSet: stmt.ReturnType != nil && stmt.ReturnType.Setof,
Args: args,
})
return err
}
func nsName(schema string) string {
if schema == "" {
return "public"
}
return schema
}
func resolveOrCreateNamespace(cat *core.Catalog, schema string) (int64, error) {
name := nsName(schema)
if oid, err := cat.NamespaceOID(name); err == nil {
return oid, nil
}
return cat.CreateNamespace(name)
}
// typeNotNull reports whether a column of the type can never be NULL
// because the type itself says so, as a domain declared NOT NULL does.
func typeNotNull(cat *core.Catalog, typeOID int64) bool {
info, err := cat.LookupType(typeOID)
return err == nil && info.NotNull
}
// columnTypeOID interns a column's type and returns its row.
func columnTypeOID(cat *core.Catalog, col *ast.ColumnDef) (int64, error) {
t := core.ColumnTypeExpr(col)
if t == nil {
return 0, fmt.Errorf("missing type name")
}
return cat.ResolveTypeExpr(t)
}
// declType is the type as the schema spelled it: an engine that folds or
// reduces the name for the catalog keeps the full spelling alongside.
func declType(tn *ast.TypeName) string {
if tn == nil {
return ""
}
if tn.Spelling != "" {
return tn.Spelling
}
return tn.Name
}

View File

@@ -0,0 +1,134 @@
package seed
import (
"fmt"
"io/fs"
"path"
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
)
// applyExtension applies the extension directory dir, relative to the
// dialect, to a catalog that has already been seeded. Unlike the dialect's
// own seed, an extension lands in a catalog full of types, so everything it
// names is resolved against what is there before being created.
func applyExtension(cat *core.Catalog, fsys fs.FS, dir string) error {
name := path.Base(dir)
sub, err := fs.Sub(fsys, dir)
if err != nil {
return fmt.Errorf("seed: extension %s: %w", name, err)
}
e := &extension{cat: cat}
if err := stream(sub, TypesFile, e.addType); err != nil {
return fmt.Errorf("extension %s: %w", name, err)
}
if err := stream(sub, FunctionsFile, e.addFunction); err != nil {
return fmt.Errorf("extension %s: %w", name, err)
}
return nil
}
type extension struct {
cat *core.Catalog
}
// addType registers a type the extension defines. The type joins the
// dialect's rules the way a type declared by a schema does: it gains the
// comparison operators, and implicit casts to the types of its category when
// the dialect's seed declared that category mutually castable.
func (e *extension) addType(t Type) error {
for _, name := range append([]string{t.Name}, t.Aliases...) {
if _, err := e.cat.TypeOID(name); err == nil {
// The dialect, or an earlier extension, already ships this type.
continue
}
oid, err := e.cat.CreateUserType(name, t.Category)
if err != nil {
return fmt.Errorf("type %q: %w", name, err)
}
if err := e.categoryCasts(oid, t.Category); err != nil {
return fmt.Errorf("type %q: %w", name, err)
}
}
return nil
}
// categoryCasts makes a new type implicitly castable to and from the types
// already in its category, under the same rule the dialect's seed applied to
// the types it was born with.
func (e *extension) categoryCasts(oid int64, category string) error {
categories, err := e.cat.DialectFlag(e.cat.SeededDialectOID(), core.FlagCastCategories)
if err != nil || categories == "" {
return err
}
if categories != "*" && !strings.Contains(categories, category) {
return nil
}
peers, err := e.cat.TypeOIDsInCategory(category)
if err != nil {
return err
}
for _, peer := range peers {
if peer == oid {
continue
}
for _, cast := range [][2]int64{{oid, peer}, {peer, oid}} {
if err := e.cat.CreateCast(core.CastSpec{
SourceTypeOID: cast[0],
TargetTypeOID: cast[1],
Context: "i",
DialectOID: e.cat.SeededDialectOID(),
}); err != nil {
return err
}
}
}
return nil
}
func (e *extension) addFunction(fn Function) error {
returnOID, err := e.funcType(fn.Returns)
if err != nil {
return fmt.Errorf("function %q: %w", fn.Name, err)
}
args := make([]core.ProcArg, 0, len(fn.Args))
for _, a := range fn.Args {
// An out or table parameter is part of the result, not the call.
switch a.Mode {
case "o", "t":
continue
}
argOID, err := e.funcType(a.Type)
if err != nil {
return fmt.Errorf("function %q: %w", fn.Name, err)
}
args = append(args, core.ProcArg{
Name: a.Name,
TypeOID: argOID,
Mode: a.Mode,
HasDefault: a.HasDefault,
})
}
_, err = e.cat.CreateProc(core.ProcSpec{
Name: fn.Name,
DialectOID: e.cat.SeededDialectOID(),
Kind: fn.Kind,
ReturnTypeOID: returnOID,
ReturnNullable: fn.Nullable,
Args: args,
})
if err != nil {
return fmt.Errorf("function %q: %w", fn.Name, err)
}
return nil
}
// funcType resolves a type a function signature names, registering the ones
// no seed bothered to list the way the dialect's own function list does.
func (e *extension) funcType(name string) (int64, error) {
if name == "" {
return 0, nil
}
return e.cat.ResolvePseudoTypeExpr(core.ParseTypeExpr(name))
}

965
internal/core/seed/seed.go Normal file
View File

@@ -0,0 +1,965 @@
// Package seed builds a core catalog for a SQL dialect from a declarative
// description of its type system: the types it defines, the operators and
// casts between them, and the functions it ships with.
//
// A dialect is a directory which each engine package embeds and hands to
// Dialect:
//
// dialect.json what the dialect is called and the rules it is built by
// types.jsonl the types it defines
// operators.jsonl operator overloads beyond the ones the rules generate
// casts.jsonl casts beyond the ones the rules generate
// functions.jsonl the functions it ships with
// relations.jsonl the system tables and views it ships with
//
// The lists are JSONL — one record per line — and are applied as they are
// read, so a dialect whose function list runs to thousands of entries is never
// held in memory as a whole. Any of the lists may be left out.
//
// A dialect may also hold an extensions/ directory with one directory per
// extension, each a smaller bundle of the same files, applied when a schema
// says CREATE EXTENSION — or, for a dialect whose settings map virtual table
// modules to extensions, CREATE VIRTUAL TABLE ... USING.
package seed
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"path"
"slices"
"strconv"
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
)
// The files a dialect directory is made of.
const (
SettingsFile = "dialect.json"
TypesFile = "types.jsonl"
OperatorsFile = "operators.jsonl"
CastsFile = "casts.jsonl"
FunctionsFile = "functions.jsonl"
RelationsFile = "relations.jsonl"
)
// ExtensionsDir is the directory under a dialect holding one directory per
// extension the dialect knows.
const ExtensionsDir = "extensions"
// Settings is dialect.json: what the dialect is called and the rules that
// generate its operators and casts.
type Settings struct {
// Dialect is the name recorded in the catalog, e.g. "postgresql".
Dialect string `json:"dialect"`
// Const names the type each kind of literal takes on, keyed by the
// core.Const* constants. Kinds left out fall back to PostgreSQL's names.
Const map[string]string `json:"const,omitempty"`
// Bool names the type comparisons return.
Bool string `json:"bool,omitempty"`
// Limit names the type a LIMIT or OFFSET count has, which is what a
// placeholder in one is typed as. Unset, it is the integer literal's.
Limit string `json:"limit,omitempty"`
// Untyped names the type a placeholder takes when nothing in the query
// constrains it. Unset, such a placeholder stays untyped.
Untyped string `json:"untyped,omitempty"`
// PropagateNullable makes a function's result nullable whenever one of
// its arguments is, the way ClickHouse's ordinary functions behave,
// unless the function is seeded as never null.
PropagateNullable bool `json:"propagate_nullable,omitempty"`
// QualifyDuplicateColumns names a result column after its relation when
// an earlier result column from another relation has the same name, as
// ClickHouse names the second id of a join e.id.
QualifyDuplicateColumns bool `json:"qualify_duplicate_columns,omitempty"`
// Comparison operators are registered as (T, T) -> Bool for every type in
// ComparisonCategories.
Comparison []string `json:"comparison,omitempty"`
ComparisonCategories string `json:"comparison_categories,omitempty"`
// Arithmetic operators are registered as (T, T) -> T for every type in
// ArithmeticCategories.
Arithmetic []string `json:"arithmetic,omitempty"`
ArithmeticCategories string `json:"arithmetic_categories,omitempty"`
// CastCategories lists the categories whose types are all implicitly
// castable to one another, so that an operator on two spellings of the
// same kind of value resolves. "*" makes every seeded type implicitly
// castable to every other, for dialects that compare across categories.
CastCategories string `json:"cast_categories,omitempty"`
// Modules names the extension a virtual table module belongs to, for a
// dialect whose schemas say CREATE VIRTUAL TABLE ... USING rather than
// CREATE EXTENSION: SQLite's fts5 module comes with the functions its
// enable_fts5 compile option adds.
Modules map[string]string `json:"modules,omitempty"`
// DefaultSchema names the schema a dialect puts an unqualified object
// in when it is not the catalog's own default: SQL Server's dbo,
// DuckDB's main. A type in it is reported unqualified.
DefaultSchema string `json:"default_schema,omitempty"`
// Rewrites are what the dialect does to a type before storing it, in
// order: the first whose pattern matches applies. A pattern's $1, $2
// bind whatever stands there, the template is what the match becomes,
// and Where bounds a binding, as "$1 <= 24" does.
Rewrites []Rewrite `json:"rewrites,omitempty"`
// Idents are the words that are identifiers wherever they stand as a
// type argument, such as the max of nvarchar(max), and IdentArgs the
// argument positions, counted from one, that are identifiers in a
// family, such as the first of SimpleAggregateFunction(sum, UInt64).
Idents []string `json:"idents,omitempty"`
IdentArgs map[string][]int `json:"ident_args,omitempty"`
// Affinity is the rule a type family the schema declares and the seed
// does not list stands on, in order: the first whose words the name
// contains names the base, and one with no words is the default.
Affinity []Affinity `json:"affinity,omitempty"`
// Alias says what an alias in types.jsonl is. "canonical", the default,
// makes it another spelling of the type, which a column declared with
// it is reported as, the way PostgreSQL reports int as integer. "base"
// makes it a type of its own that stands on the one it aliases, which
// is how SQLite keeps a column's declared spelling while comparing it
// by its affinity.
Alias string `json:"alias,omitempty"`
// fsys is the dialect directory the settings were read from.
fsys fs.FS
}
// Rewrite is one rewrite of a type expression.
type Rewrite struct {
From string `json:"from"`
To string `json:"to"`
Where string `json:"where,omitempty"`
}
// Affinity is one step of the rule an unseeded family stands on.
type Affinity struct {
Contains []string `json:"contains,omitempty"`
Type string `json:"type"`
}
// Type is a type family the dialect defines. Aliases are other spellings of
// it that a schema may use in a column definition; each becomes a row that
// points at the type, as an alias of it or as a type standing on it,
// depending on the dialect's Alias setting.
type Type struct {
Name string `json:"name"`
Category string `json:"category"`
Aliases []string `json:"aliases,omitempty"`
// Base names the family this one stands on, which must be listed
// before it: MySQL's bigint unsigned is a type of its own that
// resolves as a bigint where nothing takes it as itself.
Base string `json:"base,omitempty"`
}
// Operator is a single operator overload.
type Operator struct {
Name string `json:"name"`
Left string `json:"left"`
Right string `json:"right"`
Result string `json:"result"`
}
// Cast is a single cast between two types. Context is 'i'mplicit, 'a'ssignment
// or 'e'xplicit.
type Cast struct {
Source string `json:"source"`
Target string `json:"target"`
Context string `json:"context,omitempty"`
}
// Function is a function the dialect ships with. Kind is 'f'unction,
// 'a'ggregate, 'w'indow or 'p'rocedure.
type Function struct {
Name string `json:"name,omitempty"`
Kind string `json:"kind,omitempty"`
Args []Arg `json:"args,omitempty"`
// Returns names the result type, or "$1", "$2"... for the type of that
// argument, or an expression over the arguments — Decimal(18, $2) —
// for a result that depends on an argument's value.
Returns string `json:"returns"`
Nullable bool `json:"nullable,omitempty"`
// NeverNull marks a result that is never NULL even when an argument
// is, in a dialect that propagates nullability.
NeverNull bool `json:"never_null,omitempty"`
}
// Relation is a table or view the dialect ships with, such as one of
// PostgreSQL's system catalogs. Kind is 'r' for a table or 'v' for a view, and
// defaults to a table.
type Relation struct {
// Catalog is the database the relation belongs to, which PostgreSQL
// reports for its own schemas and which the legacy catalog carries.
Catalog string `json:"catalog,omitempty"`
Schema string `json:"schema"`
Name string `json:"name"`
Kind string `json:"kind,omitempty"`
Columns []Column `json:"columns"`
}
// Column is one of a relation's columns.
type Column struct {
Name string `json:"name"`
Type string `json:"type"`
NotNull bool `json:"not_null,omitempty"`
Array bool `json:"array,omitempty"`
Length int `json:"length,omitempty"`
}
// Arg is one of a function's parameters. Mode is 'i'n, 'o'ut, 'b'oth, 't'able
// or 'v'ariadic, and defaults to in.
type Arg struct {
Name string `json:"name,omitempty"`
Type string `json:"type"`
Mode string `json:"mode,omitempty"`
HasDefault bool `json:"has_default,omitempty"`
}
// Dialect returns the catalog option that seeds the dialect described by the
// JSONL files in dir. A dialect that ships extension data — a directory per
// extension under extensions/ — also has CREATE EXTENSION wired up to it.
func Dialect(fsys fs.FS, dir string) core.Option {
return core.WithSeed(func(cat *core.Catalog) error {
sub, err := fs.Sub(fsys, dir)
if err != nil {
return fmt.Errorf("seed: %s: %w", dir, err)
}
settings, err := loadSettings(sub)
if err != nil {
return err
}
if err := apply(cat, sub, settings); err != nil {
return err
}
cat.SetExtensionLoader(func(name string) error {
dir, ok := settings.extensionDir(name)
if !ok {
// An extension sqlc has no data for adds nothing, the way
// the legacy catalog has always treated one.
return nil
}
return applyExtension(cat, sub, dir)
})
return nil
})
}
// ExtensionDir resolves what a schema named — an extension, or a virtual
// table module the dialect's settings map to one — to the extension's
// directory under dir, reporting whether the dialect has data for it.
func ExtensionDir(fsys fs.FS, dir, name string) (string, bool) {
sub, err := fs.Sub(fsys, dir)
if err != nil {
return "", false
}
settings, err := loadSettings(sub)
if err != nil {
return "", false
}
rel, ok := settings.extensionDir(name)
if !ok {
return "", false
}
return path.Join(dir, rel), true
}
// extensionDir is the directory of the extension a name refers to,
// relative to the dialect, if the dialect ships one.
func (s Settings) extensionDir(name string) (string, bool) {
if ext, ok := s.Modules[strings.ToLower(name)]; ok {
name = ext
}
dir := path.Join(ExtensionsDir, name)
if _, err := fs.Stat(s.fsys, dir); err != nil {
return "", false
}
return dir, true
}
func apply(cat *core.Catalog, fsys fs.FS, settings Settings) error {
var err error
b := &builder{
cat: cat,
settings: settings,
oids: map[string]int64{},
namespaces: map[string]int64{},
seenCasts: map[[2]int64]bool{},
}
if b.dialectOID, err = cat.CreateDialect(settings.Dialect); err != nil {
return err
}
// Types come first: everything else names them.
if err := stream(fsys, TypesFile, b.addType); err != nil {
return err
}
// Every dialect has arrays, whether or not its list names the family,
// and a lookup of one against the cached catalog cannot add it then.
if _, err := b.createType(core.ArrayTypeName, "A", 0); err != nil {
return err
}
if err := b.consts(); err != nil {
return err
}
if err := b.rules(); err != nil {
return err
}
if err := b.categoryOperators(); err != nil {
return err
}
if err := stream(fsys, OperatorsFile, b.addOperator); err != nil {
return err
}
if err := stream(fsys, CastsFile, b.addCast); err != nil {
return err
}
if err := b.categoryCasts(); err != nil {
return err
}
if err := stream(fsys, FunctionsFile, b.addFunction); err != nil {
return err
}
return stream(fsys, RelationsFile, b.addRelation)
}
func loadSettings(fsys fs.FS) (Settings, error) {
f, err := fsys.Open(SettingsFile)
if err != nil {
return Settings{}, fmt.Errorf("seed: %w", err)
}
defer f.Close()
dec := json.NewDecoder(bufio.NewReader(f))
dec.DisallowUnknownFields()
var settings Settings
if err := dec.Decode(&settings); err != nil {
return Settings{}, fmt.Errorf("seed: %s: %w", SettingsFile, err)
}
if settings.Dialect == "" {
return Settings{}, fmt.Errorf("seed: %s: dialect has no name", SettingsFile)
}
settings.fsys = fsys
return settings, nil
}
// stream decodes name a record at a time, handing each to fn as it is read. A
// file that is not there contributes nothing, so a dialect only writes the
// files it needs. An unknown field is an error rather than something to
// ignore, so that a misspelled key is caught where it is written.
func stream[T any](fsys fs.FS, name string, fn func(T) error) error {
f, err := fsys.Open(name)
if errors.Is(err, fs.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("seed: %w", err)
}
defer f.Close()
dec := json.NewDecoder(bufio.NewReader(f))
dec.DisallowUnknownFields()
for record := 1; ; record++ {
var rec T
if err := dec.Decode(&rec); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("seed: %s: record %d: %w", name, record, err)
}
if err := fn(rec); err != nil {
return fmt.Errorf("seed: %s: record %d: %w", name, record, err)
}
}
}
// Functions streams a dialect's functions into the form the engine catalogs
// use. It is how an engine whose standard library lives in functions.jsonl
// hands that library to the rest of sqlc.
func Functions(fsys fs.FS, dir string) ([]*catalog.Function, error) {
sub, err := fs.Sub(fsys, dir)
if err != nil {
return nil, fmt.Errorf("seed: %s: %w", dir, err)
}
var out []*catalog.Function
err = stream(sub, FunctionsFile, func(fn Function) error {
args := make([]*catalog.Argument, 0, len(fn.Args))
for _, a := range fn.Args {
args = append(args, &catalog.Argument{
Name: a.Name,
Type: &ast.TypeName{Name: a.Type},
HasDefault: a.HasDefault,
Mode: argMode(a.Mode),
})
}
out = append(out, &catalog.Function{
Name: fn.Name,
Args: args,
ReturnType: &ast.TypeName{Name: fn.Returns},
ReturnTypeNullable: fn.Nullable,
})
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
// Relations streams the relations a dialect ships with in the named schema
// into the form the engine catalogs use.
func Relations(fsys fs.FS, dir, schema string) ([]*catalog.Table, error) {
sub, err := fs.Sub(fsys, dir)
if err != nil {
return nil, fmt.Errorf("seed: %s: %w", dir, err)
}
var out []*catalog.Table
err = stream(sub, RelationsFile, func(rel Relation) error {
if rel.Schema != schema {
return nil
}
table := &catalog.Table{
Rel: &ast.TableName{Catalog: rel.Catalog, Schema: rel.Schema, Name: rel.Name},
Columns: make([]*catalog.Column, 0, len(rel.Columns)),
}
for _, col := range rel.Columns {
// A column's type may carry arguments, as MySQL's varchar(64)
// does; the legacy catalog holds the family and the length
// apart.
t := core.ParseTypeExpr(col.Type)
column := &catalog.Column{
Name: col.Name,
Type: ast.TypeName{Name: t.Name},
IsNotNull: col.NotNull,
IsArray: col.Array,
}
if col.Array {
column.ArrayDims = 1
}
if col.Length > 0 {
length := col.Length
column.Length = &length
} else if len(t.Args) > 0 && t.Args[0].Int != nil {
length := int(*t.Args[0].Int)
column.Length = &length
}
table.Columns = append(table.Columns, column)
}
out = append(out, table)
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
// argModes maps the mode a record carries to the one the catalog uses. The
// letters are PostgreSQL's, which is where the generated function lists come
// from.
var argModes = map[string]ast.FuncParamMode{
"i": ast.FuncParamIn,
"o": ast.FuncParamOut,
"b": ast.FuncParamInOut,
"v": ast.FuncParamVariadic,
"t": ast.FuncParamTable,
"d": ast.FuncParamDefault,
}
func argMode(mode string) ast.FuncParamMode {
if m, ok := argModes[mode]; ok {
return m
}
return ast.FuncParamIn
}
// ArgMode is the letter a record carries for a catalog argument mode, the
// inverse of the mapping applied when reading one.
func ArgMode(mode ast.FuncParamMode) string {
for letter, m := range argModes {
if m == mode {
return letter
}
}
return "i"
}
type builder struct {
cat *core.Catalog
settings Settings
dialectOID int64
// oids maps a lowercased type spelling to the row a seed record naming
// it means, and categories records the category each family was seeded
// under, in the order they were read.
oids map[string]int64
categories []categorized
// namespaces maps a schema name to its OID, so that a run of relations in
// the same schema resolves it once.
namespaces map[string]int64
// seenCasts records the pairs already registered. The alias rules and the
// category rules overlap, and a cast pair is unique in the catalog.
seenCasts map[[2]int64]bool
}
type categorized struct {
name string
category string
}
func (b *builder) addType(t Type) error {
var baseOID int64
if t.Base != "" {
oid, ok := b.oids[strings.ToLower(t.Base)]
if !ok {
return fmt.Errorf("type %q: base %q is not a type listed before it", t.Name, t.Base)
}
baseOID = oid
}
oid, err := b.createType(t.Name, t.Category, baseOID)
if err != nil {
return fmt.Errorf("type %q: %w", t.Name, err)
}
for _, alias := range t.Aliases {
if err := b.addAlias(alias, oid, t.Category); err != nil {
return fmt.Errorf("type %q: alias %q: %w", t.Name, alias, err)
}
}
return nil
}
// addAlias registers another spelling of a type: a row that points at the
// type as its canonical form, or — for a dialect whose aliases are types of
// their own — as its base.
func (b *builder) addAlias(name string, typeOID int64, category string) error {
key := strings.ToLower(name)
if _, ok := b.oids[key]; ok {
return nil
}
spec := core.TypeSpec{Name: key, Typtype: "b", Category: category, DialectOID: b.dialectOID}
if b.settings.Alias == "base" {
spec.BaseOID = typeOID
} else {
spec.CanonicalOID = typeOID
}
oid, err := b.cat.CreateTypeSpec(spec)
if err != nil {
return err
}
// A record naming the alias means the type it stands for, unless the
// alias is a type of its own.
if b.settings.Alias == "base" {
b.oids[key] = oid
} else {
b.oids[key] = typeOID
}
return nil
}
func (b *builder) createType(name, category string, baseOID int64) (int64, error) {
key := strings.ToLower(name)
if oid, ok := b.oids[key]; ok {
return oid, nil
}
oid, err := b.cat.CreateTypeSpec(core.TypeSpec{
Name: key,
Typtype: "b",
Category: category,
BaseOID: baseOID,
DialectOID: b.dialectOID,
})
if err != nil {
return 0, err
}
b.oids[key] = oid
b.categories = append(b.categories, categorized{name: key, category: category})
return oid, nil
}
// constKinds are the kinds of literal a dialect may name a type for.
var constKinds = []string{core.ConstInteger, core.ConstFloat, core.ConstString, core.ConstBool}
func (b *builder) consts() error {
for kind := range b.settings.Const {
if !slices.Contains(constKinds, kind) {
return fmt.Errorf("seed %s: unknown constant kind %q, want one of %s",
b.settings.Dialect, kind, strings.Join(constKinds, ", "))
}
}
// Written in a fixed order rather than the map's: a catalog built from the
// same dialect twice has to come out byte for byte the same, so that the
// cache stores one copy of it rather than one per run.
for _, kind := range constKinds {
name, ok := b.settings.Const[kind]
if !ok {
continue
}
if _, ok := b.oids[strings.ToLower(name)]; !ok {
return fmt.Errorf("seed %s: constant %s names unknown type %q", b.settings.Dialect, kind, name)
}
if err := b.cat.SetConstType(b.dialectOID, kind, name); err != nil {
return err
}
}
if b.settings.DefaultSchema != "" {
if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagDefaultSchema, strings.ToLower(b.settings.DefaultSchema)); err != nil {
return err
}
}
for key, name := range map[string]string{
core.FlagBoolType: b.settings.Bool,
core.FlagLimitType: b.settings.Limit,
core.FlagUntypedType: b.settings.Untyped,
} {
if name == "" {
continue
}
if _, ok := b.oids[strings.ToLower(name)]; !ok {
return fmt.Errorf("seed %s: %s names unknown type %q", b.settings.Dialect, key, name)
}
if err := b.cat.SetDialectFlag(b.dialectOID, key, name); err != nil {
return err
}
}
if b.settings.PropagateNullable {
if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagPropagateNullable, "true"); err != nil {
return err
}
}
if b.settings.QualifyDuplicateColumns {
if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagQualifyDuplicateColumns, "true"); err != nil {
return err
}
}
// A schema declares types the seed knows nothing about — enums, domains,
// arrays, a SQLite column typed whatever the author felt like. Recording
// the comparison operators lets the catalog give those types the same ones.
if len(b.settings.Comparison) > 0 {
if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagComparisonOperators,
strings.Join(b.settings.Comparison, ",")); err != nil {
return err
}
}
// The cast categories are recorded for the same reason: a type an
// extension adds joins its category's casts.
if b.settings.CastCategories != "" {
return b.cat.SetDialectFlag(b.dialectOID, core.FlagCastCategories, b.settings.CastCategories)
}
return nil
}
// rules records what the dialect does to a type before storing it: its
// rewrites, its identifier words and positions, and its affinity rule.
func (b *builder) rules() error {
s := b.settings
for i, rw := range s.Rewrites {
if rw.From == "" || rw.To == "" {
return fmt.Errorf("seed %s: rewrite %d needs from and to", s.Dialect, i+1)
}
if err := b.cat.AddTypeRewrite(i+1, rw.From, rw.To, rw.Where); err != nil {
return err
}
}
if len(s.Idents) > 0 {
if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagIdents, strings.ToLower(strings.Join(s.Idents, ","))); err != nil {
return err
}
}
if len(s.IdentArgs) > 0 {
families := make([]string, 0, len(s.IdentArgs))
for family := range s.IdentArgs {
families = append(families, family)
}
// In a fixed order, so the catalog comes out the same every time.
slices.Sort(families)
entries := make([]string, 0, len(families))
for _, family := range families {
positions := make([]string, 0, len(s.IdentArgs[family]))
for _, p := range s.IdentArgs[family] {
positions = append(positions, strconv.Itoa(p))
}
entries = append(entries, strings.ToLower(family)+":"+strings.Join(positions, ","))
}
if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagIdentArgs, strings.Join(entries, ";")); err != nil {
return err
}
}
for i, a := range s.Affinity {
oid, ok := b.oids[strings.ToLower(a.Type)]
if !ok {
return fmt.Errorf("seed %s: affinity names unknown type %q", s.Dialect, a.Type)
}
if err := b.cat.AddTypeAffinity(i+1, a.Contains, oid); err != nil {
return err
}
}
return nil
}
func (b *builder) categoryOperators() error {
s := b.settings
boolOID, ok := b.oids[strings.ToLower(s.Bool)]
if len(s.Comparison) > 0 && !ok {
return fmt.Errorf("seed %s: comparison operators need a bool type", s.Dialect)
}
for _, t := range b.categories {
if strings.Contains(s.ComparisonCategories, t.category) {
for _, op := range s.Comparison {
if err := b.operator(op, t.name, t.name, s.Bool, boolOID); err != nil {
return err
}
}
}
if strings.Contains(s.ArithmeticCategories, t.category) {
for _, op := range s.Arithmetic {
if err := b.operator(op, t.name, t.name, t.name, b.oids[t.name]); err != nil {
return err
}
}
}
}
return nil
}
func (b *builder) addOperator(op Operator) error {
return b.operator(op.Name, op.Left, op.Right, op.Result, 0)
}
func (b *builder) operator(name, left, right, result string, resultOID int64) error {
leftOID, ok := b.oids[strings.ToLower(left)]
if !ok {
return fmt.Errorf("operator %q: unknown left type %q", name, left)
}
rightOID, ok := b.oids[strings.ToLower(right)]
if !ok {
return fmt.Errorf("operator %q: unknown right type %q", name, right)
}
if resultOID == 0 {
resultOID, ok = b.oids[strings.ToLower(result)]
if !ok {
return fmt.Errorf("operator %q: unknown result type %q", name, result)
}
}
_, err := b.cat.CreateOperator(core.OperatorSpec{
Name: name,
DialectOID: b.dialectOID,
LeftTypeOID: leftOID,
RightTypeOID: rightOID,
ResultTypeOID: resultOID,
})
return err
}
// categoryCasts applies the rule that makes the types in a category mutually
// castable, once every type has been read.
func (b *builder) categoryCasts() error {
categories := b.settings.CastCategories
if categories == "" {
return nil
}
anyCategory := categories == "*"
for _, src := range b.categories {
if !anyCategory && !strings.Contains(categories, src.category) {
continue
}
for _, tgt := range b.categories {
if src.name == tgt.name {
continue
}
if !anyCategory && src.category != tgt.category {
continue
}
if err := b.addCast(Cast{Source: src.name, Target: tgt.name, Context: "i"}); err != nil {
return err
}
}
}
return nil
}
func (b *builder) addCast(c Cast) error {
srcOID, ok := b.oids[strings.ToLower(c.Source)]
if !ok {
return fmt.Errorf("cast: unknown source type %q", c.Source)
}
tgtOID, ok := b.oids[strings.ToLower(c.Target)]
if !ok {
return fmt.Errorf("cast: unknown target type %q", c.Target)
}
if srcOID == tgtOID || b.seenCasts[[2]int64{srcOID, tgtOID}] {
return nil
}
b.seenCasts[[2]int64{srcOID, tgtOID}] = true
context := c.Context
if context == "" {
context = "i"
}
return b.cat.CreateCast(core.CastSpec{
SourceTypeOID: srcOID,
TargetTypeOID: tgtOID,
Context: context,
DialectOID: b.dialectOID,
})
}
func (b *builder) addFunction(fn Function) error {
returns, template := returnTemplate(fn.Returns)
returnOID, err := b.funcType(returns)
if err != nil {
return fmt.Errorf("function %q: %w", fn.Name, err)
}
args := make([]core.ProcArg, 0, len(fn.Args))
for _, a := range fn.Args {
// An out or table parameter is part of the result, not the call.
switch a.Mode {
case "o", "t":
continue
}
argOID, err := b.funcType(a.Type)
if err != nil {
return fmt.Errorf("function %q: %w", fn.Name, err)
}
args = append(args, core.ProcArg{
Name: a.Name,
TypeOID: argOID,
Mode: a.Mode,
HasDefault: a.HasDefault,
})
}
_, err = b.cat.CreateProc(core.ProcSpec{
Name: fn.Name,
DialectOID: b.dialectOID,
Kind: fn.Kind,
ReturnTypeOID: returnOID,
ReturnNullable: fn.Nullable,
NeverNull: fn.NeverNull,
ReturnTemplate: template,
Args: args,
})
if err != nil {
return fmt.Errorf("function %q: %w", fn.Name, err)
}
return nil
}
// returnTemplate splits a function's Returns into the type the catalog
// records and the template it keeps: a result spelled over the arguments,
// as Decimal(18, $2) is, records its family and keeps the whole spelling
// to fill in at each call. A bare $n, the type of that argument, is a
// pseudo-type of its own rather than a template.
func returnTemplate(returns string) (string, string) {
if !strings.Contains(returns, "$") || strings.HasPrefix(returns, "$") {
return returns, ""
}
t := core.ParseTypeExpr(returns)
return t.Name, returns
}
func (b *builder) addRelation(rel Relation) error {
if rel.Name == "" {
return errors.New("relation has no name")
}
nsOID, err := b.namespace(rel.Schema)
if err != nil {
return err
}
kind := rel.Kind
if kind == "" {
kind = "r"
}
classOID, err := b.cat.CreateClass(nsOID, rel.Name, kind)
if err != nil {
return err
}
for i, col := range rel.Columns {
t := core.ParseTypeExpr(col.Type)
if col.Array {
t = core.Array(t)
}
typeOID, err := b.columnType(t)
if err != nil {
return fmt.Errorf("relation %q column %q: %w", rel.Name, col.Name, err)
}
if err := b.cat.CreateAttributeSpec(core.AttributeSpec{
ClassOID: classOID,
Name: col.Name,
TypeOID: typeOID,
Num: i + 1,
NotNull: col.NotNull,
DeclType: col.Type,
}); err != nil {
return fmt.Errorf("relation %q column %q: %w", rel.Name, col.Name, err)
}
}
return nil
}
// namespace resolves the schema a relation belongs to, creating it the first
// time it is named. A relation with no schema belongs to the default one.
func (b *builder) namespace(schema string) (int64, error) {
if schema == "" {
schema = "public"
}
if oid, ok := b.namespaces[schema]; ok {
return oid, nil
}
oid, err := b.cat.NamespaceOID(schema)
if err != nil {
if oid, err = b.cat.CreateNamespace(schema); err != nil {
return 0, err
}
}
b.namespaces[schema] = oid
return oid, nil
}
// columnType resolves a column's type, which may be an array or carry
// arguments.
func (b *builder) columnType(t *core.TypeExpr) (int64, error) {
key := t.Key()
if oid, ok := b.oids[key]; ok {
return oid, nil
}
oid, err := b.cat.ResolveTypeExpr(t)
if err != nil {
return 0, err
}
b.oids[key] = oid
return oid, nil
}
// funcType resolves the type a function signature names. Signatures reference
// pseudo types ("any", "record") and types no dialect bothers to list, so an
// unknown name is registered rather than rejected.
func (b *builder) funcType(name string) (int64, error) {
if name == "" {
return 0, nil
}
key := strings.ToLower(name)
if oid, ok := b.oids[key]; ok {
return oid, nil
}
oid, err := b.cat.ResolvePseudoTypeExpr(core.ParseTypeExpr(name))
if err != nil {
return 0, err
}
b.oids[key] = oid
return oid, nil
}

16
internal/core/sqlc.json Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "2",
"sql": [
{
"engine": "sqlite",
"schema": "catalogdef/schema.sql",
"queries": "catalogdef/query.sql",
"gen": {
"go": {
"package": "catalogdb",
"out": "catalogdb"
}
}
}
]
}

324
internal/core/typeexpr.go Normal file
View File

@@ -0,0 +1,324 @@
package core
import (
"strconv"
"strings"
)
// TypeExpr is a type written as a call expression: a lowercased name applied
// to arguments that are other types, integers, booleans or strings, each
// with an optional label, and a nullable flag at whatever depth it applies.
// Nothing about a nested type is special-cased, so an array of nullable
// strings is array(string nullable), a map is map(string, uint32) and a
// named tuple is tuple(lat: float64, lon: float64). The catalog resolves the
// names; the expression only records what was declared or inferred.
type TypeExpr struct {
Name string `json:"name"`
Nullable bool `json:"nullable,omitempty"`
Args []TypeArg `json:"args,omitempty"`
}
// TypeArg is one argument of a TypeExpr: exactly one of Type, Int, Bool,
// String or Ident is set. Ident is a bare word that is not a type — the max
// of nvarchar(max), the function of SimpleAggregateFunction(sum, UInt64),
// the fields of interval day to second.
type TypeArg struct {
Label string `json:"label,omitempty"`
Type *TypeExpr `json:"type,omitempty"`
Int *int64 `json:"int,omitempty"`
Bool *bool `json:"bool,omitempty"`
String *string `json:"string,omitempty"`
Ident *string `json:"ident,omitempty"`
}
// ArrayTypeName is the family every dialect's array is an instance of: an
// array of integers is array(integer), whatever the dialect spells it.
const ArrayTypeName = "array"
// Array wraps element in one array dimension.
func Array(element *TypeExpr) *TypeExpr {
return &TypeExpr{Name: ArrayTypeName, Args: []TypeArg{{Type: element}}}
}
// IsArray reports whether the expression is an array.
func (t *TypeExpr) IsArray() bool {
return t != nil && t.Name == ArrayTypeName && len(t.Args) > 0 && t.Args[0].Type != nil
}
// Element is an array's element type, or nil for anything else.
func (t *TypeExpr) Element() *TypeExpr {
if !t.IsArray() {
return nil
}
return t.Args[0].Type
}
// ArrayDims counts the array dimensions wrapped around the expression's
// innermost type, and Innermost is that type: the integer of an array of
// arrays of integers.
func (t *TypeExpr) ArrayDims() int {
dims := 0
for t.IsArray() {
dims++
t = t.Element()
}
return dims
}
func (t *TypeExpr) Innermost() *TypeExpr {
for t.IsArray() {
t = t.Element()
}
return t
}
// Clone copies the expression, arguments and all, so that a caller can set
// nullability on the copy without touching a cached one.
func (t *TypeExpr) Clone() *TypeExpr {
if t == nil {
return nil
}
out := &TypeExpr{Name: t.Name, Nullable: t.Nullable}
if len(t.Args) > 0 {
out.Args = make([]TypeArg, len(t.Args))
for i, a := range t.Args {
out.Args[i] = a
out.Args[i].Type = a.Type.Clone()
}
}
return out
}
// WithNullable returns a copy of the expression with its own nullability set
// as given; the nullability of a nested type is left alone.
func (t *TypeExpr) WithNullable(nullable bool) *TypeExpr {
out := t.Clone()
if out != nil {
out.Nullable = nullable
}
return out
}
// Key is the expression's canonical spelling, which identifies its row in
// the catalog: the expression's own nullability is not part of it, since a
// row is never nullable, while the nullability of a nested type is.
func (t *TypeExpr) Key() string {
if t == nil {
return ""
}
if !t.Nullable {
return t.String()
}
return t.WithNullable(false).String()
}
// ParseTypeExpr reads a type spelled the way every dialect spells one, as a
// name optionally applied to a parenthesised, comma-separated argument list
// whose entries may be labelled (`lat Float64` in a tuple, `'a' = 1` in an
// enum). A Nullable(T) wrapper becomes T with Nullable set, and a trailing
// [] becomes an array of the element.
func ParseTypeExpr(s string) *TypeExpr {
s = strings.TrimSpace(s)
if element, ok := strings.CutSuffix(s, ArraySuffix); ok {
return Array(ParseTypeExpr(element))
}
name, args := splitTypeArgs(s)
name = strings.ToLower(name)
if name == "nullable" && len(args) == 1 {
t := ParseTypeExpr(args[0])
t.Nullable = true
return t
}
t := &TypeExpr{Name: name}
for _, a := range args {
t.Args = append(t.Args, parseTypeArg(a))
}
return t
}
func parseTypeArg(a string) TypeArg {
a = strings.TrimSpace(a)
if strings.HasPrefix(a, "'") {
end := quotedEnd(a)
lit := strings.ReplaceAll(strings.ReplaceAll(a[1:end-1], `\'`, `'`), `''`, `'`)
if rest := strings.TrimSpace(a[end:]); strings.HasPrefix(rest, "=") {
arg := parseTypeArg(rest[1:])
arg.Label = lit
return arg
}
return TypeArg{String: &lit}
}
if n, err := strconv.ParseInt(a, 10, 64); err == nil {
return TypeArg{Int: &n}
}
switch strings.ToLower(a) {
case "true", "false":
b := strings.EqualFold(a, "true")
return TypeArg{Bool: &b}
}
// A label is a word before a colon, as the canonical form writes it:
// `lat: Float64`. A word before a space is a label too, as ClickHouse
// writes `lat Float64`, but only when what follows is a single word,
// since `timestamp with time zone` is a name and not a label.
head := a
if p := strings.IndexByte(a, '('); p >= 0 {
head = a[:p]
}
if i := strings.IndexByte(head, ':'); i > 0 && !strings.ContainsAny(head[:i], " '\"") {
return TypeArg{Label: strings.TrimSpace(a[:i]), Type: ParseTypeExpr(a[i+1:])}
}
if i := strings.IndexByte(head, ' '); i > 0 && !strings.Contains(strings.TrimSpace(head[i+1:]), " ") {
return TypeArg{Label: a[:i], Type: ParseTypeExpr(a[i+1:])}
}
return TypeArg{Type: ParseTypeExpr(a)}
}
// quotedEnd returns the index just past the single-quoted literal starting
// at the beginning of s, honouring backslash escapes and doubled quotes.
func quotedEnd(s string) int {
for i := 1; i < len(s); i++ {
switch {
case s[i] == '\\' && i+1 < len(s):
i++
case s[i] == '\'' && i+1 < len(s) && s[i+1] == '\'':
i++
case s[i] == '\'':
return i + 1
}
}
return len(s)
}
// splitTypeArgs splits `Base(arg, arg)` into its base name and top-level
// arguments, leaving nested parentheses and quoted strings intact. Words
// after the closing parenthesis belong to the name, since MySQL writes
// decimal(10,2) unsigned and PostgreSQL timestamp(3) with time zone.
func splitTypeArgs(t string) (string, []string) {
open := strings.IndexByte(t, '(')
if open < 0 {
return t, nil
}
close := matchingParen(t, open)
if close < 0 {
return t, nil
}
base := strings.TrimSpace(t[:open])
if rest := strings.TrimSpace(t[close+1:]); rest != "" {
base += " " + rest
}
inner := t[open+1 : close]
var (
args []string
depth int
quote byte
start int
)
for i := 0; i < len(inner); i++ {
c := inner[i]
switch {
case quote != 0:
if c == '\\' {
i++
} else if c == quote {
quote = 0
}
case c == '\'' || c == '"' || c == '`':
quote = c
case c == '(':
depth++
case c == ')':
depth--
case c == ',' && depth == 0:
args = append(args, strings.TrimSpace(inner[start:i]))
start = i + 1
}
}
if last := strings.TrimSpace(inner[start:]); last != "" || len(args) > 0 {
args = append(args, last)
}
return base, args
}
// matchingParen finds the parenthesis closing the one at open, skipping
// nested parentheses and quoted strings, or -1 when it is not closed.
func matchingParen(t string, open int) int {
depth := 0
var quote byte
for i := open; i < len(t); i++ {
c := t[i]
switch {
case quote != 0:
if c == '\\' {
i++
} else if c == quote {
quote = 0
}
case c == '\'' || c == '"' || c == '`':
quote = c
case c == '(':
depth++
case c == ')':
depth--
if depth == 0 {
return i
}
}
}
return -1
}
// HasNullable reports whether the expression marks nullability anywhere,
// which tells whether the spelling it came from said so itself.
func (t *TypeExpr) HasNullable() bool {
if t == nil {
return false
}
if t.Nullable {
return true
}
for _, a := range t.Args {
if a.Type.HasNullable() {
return true
}
}
return false
}
// String renders the expression in its canonical text form, with a trailing
// "nullable" marking a nullable type.
func (t *TypeExpr) String() string {
if t == nil {
return ""
}
var b strings.Builder
b.WriteString(t.Name)
if len(t.Args) > 0 {
b.WriteByte('(')
for i, a := range t.Args {
if i > 0 {
b.WriteString(", ")
}
if a.Label != "" {
b.WriteString(a.Label)
b.WriteString(": ")
}
switch {
case a.Type != nil:
b.WriteString(a.Type.String())
case a.Int != nil:
b.WriteString(strconv.FormatInt(*a.Int, 10))
case a.Bool != nil:
b.WriteString(strconv.FormatBool(*a.Bool))
case a.String != nil:
b.WriteString("'" + strings.ReplaceAll(*a.String, "'", `\'`) + "'")
case a.Ident != nil:
b.WriteString(*a.Ident)
}
}
b.WriteByte(')')
}
if t.Nullable {
b.WriteString(" nullable")
}
return b.String()
}

150
internal/core/typename.go Normal file
View File

@@ -0,0 +1,150 @@
package core
import (
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
// TypeExprOfTypeName reads the type an AST node names into an expression.
// An engine that renders the whole type as a call expression — DuckDB's
// struct(a: integer, b: varchar) — hands it over in Canonical; one that
// folds it into the spelling the formatter prints back — ClickHouse's
// Array(Nullable(String)), SQLite's VARYING CHARACTER(10) — in Spelling.
// Otherwise the name comes from Name or the qualifying parts of Names, the
// type modifiers become integer or string arguments, and each array bound
// wraps the result in an array.
func TypeExprOfTypeName(tn *ast.TypeName) *TypeExpr {
if tn == nil {
return nil
}
if tn.Canonical != "" {
return ParseTypeExpr(tn.Canonical)
}
if tn.Spelling != "" {
return ParseTypeExpr(tn.Spelling)
}
name := strings.TrimSpace(tn.Name)
if name == "" && tn.Names != nil {
parts := make([]string, 0, len(tn.Names.Items))
for _, item := range tn.Names.Items {
s, ok := item.(*ast.String)
if !ok || s.Str == "pg_catalog" {
continue
}
parts = append(parts, s.Str)
}
name = strings.Join(parts, ".")
}
name = strings.ToLower(name)
if name == "" {
return nil
}
// An engine that reports the schema apart from the name qualifies it
// the way a dotted name does, so the type resolves in its namespace.
if schema := strings.ToLower(tn.Schema); schema != "" && schema != "pg_catalog" && !strings.Contains(name, ".") {
name = schema + "." + name
}
// A name an engine spelled with its own arguments or array suffix reads
// the same way a spelling does.
t := ParseTypeExpr(name)
for _, item := range listItems(tn.Typmods) {
if arg, ok := typmodArg(item); ok {
t.Args = append(t.Args, arg)
}
}
for range listItems(tn.ArrayBounds) {
t = Array(t)
}
return t
}
// ColumnTypeExpr reads a column definition's type. Engines report an array
// column either on the type name or on the column itself.
func ColumnTypeExpr(col *ast.ColumnDef) *TypeExpr {
if col == nil {
return nil
}
t := TypeExprOfTypeName(col.TypeName)
if t == nil {
return nil
}
// MySQL reports an unsigned column on the definition, and the
// members of an enum or set apart from the type's name.
if col.IsUnsigned && !strings.Contains(t.Name, " unsigned") {
t.Name += " unsigned"
}
if vals := listItems(col.Vals); len(vals) > 0 && len(t.Args) == 0 {
for _, item := range vals {
if s, ok := item.(*ast.String); ok {
v := s.Str
t.Args = append(t.Args, TypeArg{String: &v})
}
}
}
if col.TypeName.Canonical != "" || col.TypeName.Spelling != "" || listItems(col.TypeName.ArrayBounds) != nil {
return t
}
dims := col.ArrayDims
if dims == 0 && col.IsArray {
dims = 1
}
for i := 0; i < dims; i++ {
t = Array(t)
}
return t
}
// typmodArg reads one type modifier as an argument: an integer, a quoted
// string, or a bare word, which is an identifier such as the max of
// nvarchar(max) or the day to second of an interval. A constant node holds
// a literal; a bare String node holds a word.
func typmodArg(n ast.Node) (TypeArg, bool) {
switch v := n.(type) {
case *ast.A_Const:
switch val := v.Val.(type) {
case *ast.String:
s := val.Str
return TypeArg{String: &s}, true
default:
return typmodArg(v.Val)
}
case *ast.Integer:
i := v.Ival
return TypeArg{Int: &i}, true
case *ast.String:
s := strings.ToLower(v.Str)
return TypeArg{Ident: &s}, true
case *ast.ColumnRef:
parts := make([]string, 0, len(listItems(v.Fields)))
for _, item := range listItems(v.Fields) {
if s, ok := item.(*ast.String); ok {
parts = append(parts, s.Str)
}
}
if len(parts) == 0 {
return TypeArg{}, false
}
ident := strings.ToLower(strings.Join(parts, "."))
return TypeArg{Ident: &ident}, true
}
return TypeArg{}, false
}
func listItems(l *ast.List) []ast.Node {
if l == nil {
return nil
}
return l.Items
}
// ResolveType interns the type an AST node names, registering it when the
// dialect's seed did not: a schema is free to declare types of its own.
func (c *Catalog) ResolveType(tn *ast.TypeName) (int64, error) {
t := TypeExprOfTypeName(tn)
if t == nil {
return 0, fmt.Errorf("missing type name")
}
return c.ResolveTypeExpr(t)
}

825
internal/core/types.go Normal file
View File

@@ -0,0 +1,825 @@
package core
import (
"context"
"database/sql"
"errors"
"fmt"
"slices"
"strings"
"sync"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
// TypeSpec describes a type row. Name is the family's name; Expr is the
// canonical spelling of the whole expression and defaults to the name, which
// is what a family's is.
type TypeSpec struct {
Name string
Expr string
Typtype string
Category string
Preferred bool
NamespaceOID int64
DialectOID int64
FamilyOID int64
ElementOID int64
BaseOID int64
CanonicalOID int64
NotNull bool
}
// TypeInfo is a type row as the catalog holds it.
type TypeInfo struct {
OID int64
NamespaceOID int64
Name string
Expr string
Category string
Typtype string
Preferred bool
FamilyOID int64
ElementOID int64
BaseOID int64
CanonicalOID int64
NotNull bool
}
// IsFamily reports whether the row is a family rather than an instance.
func (t TypeInfo) IsFamily() bool { return t.FamilyOID == 0 }
// typeCache remembers what the catalog holds about a type. A row never
// changes once written, and a restored catalog is read-only, so a cached
// answer is good for the life of the catalog. Analysis runs concurrently on
// a restored catalog, so the cache is locked.
type typeCache struct {
mu sync.RWMutex
infos map[int64]TypeInfo
exprs map[int64]*TypeExpr
namespaces map[int64]string
namespaceOIDs map[string]int64
}
func (c *typeCache) namespace(oid int64) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
name, ok := c.namespaces[oid]
return name, ok
}
func (c *typeCache) namespaceOID(name string) (int64, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
oid, ok := c.namespaceOIDs[name]
return oid, ok
}
func (c *typeCache) putNamespace(oid int64, name string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.namespaces == nil {
c.namespaces = map[int64]string{}
c.namespaceOIDs = map[string]int64{}
}
c.namespaces[oid] = name
c.namespaceOIDs[name] = oid
}
func (c *typeCache) info(oid int64) (TypeInfo, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
info, ok := c.infos[oid]
return info, ok
}
func (c *typeCache) expr(oid int64) (*TypeExpr, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
e, ok := c.exprs[oid]
return e, ok
}
func (c *typeCache) put(info TypeInfo, expr *TypeExpr) {
c.mu.Lock()
defer c.mu.Unlock()
if c.infos == nil {
c.infos = map[int64]TypeInfo{}
c.exprs = map[int64]*TypeExpr{}
}
c.infos[info.OID] = info
if expr != nil {
c.exprs[info.OID] = expr
}
}
func (c *Catalog) CreateType(name string) (int64, error) {
return c.CreateTypeSpec(TypeSpec{Name: name, Typtype: "b"})
}
// CreateTypeSpec inserts a type row. It is the raw insert: nothing is
// canonicalized and no arguments are written, so it is what the seed and
// ResolveTypeExpr build on rather than what a caller with an expression
// wants.
func (c *Catalog) CreateTypeSpec(t TypeSpec) (int64, error) {
if t.Typtype == "" {
t.Typtype = "b"
}
if t.NamespaceOID == 0 {
oid, err := c.NamespaceOID("public")
if err != nil {
return 0, fmt.Errorf("create type %q: default namespace: %w", t.Name, err)
}
t.NamespaceOID = oid
}
name := strings.ToLower(t.Name)
expr := t.Expr
if expr == "" {
expr = name
}
oid, err := c.q.CreateType(context.Background(), catalogdb.CreateTypeParams{
Name: name,
Expr: expr,
Typtype: t.Typtype,
Category: nullString(t.Category),
Preferred: boolToInt64(t.Preferred),
NamespaceOid: t.NamespaceOID,
DialectOid: nullInt64(t.DialectOID),
FamilyOid: nullInt64(t.FamilyOID),
ElementOid: nullInt64(t.ElementOID),
BaseOid: nullInt64(t.BaseOID),
CanonicalOid: nullInt64(t.CanonicalOID),
NotNull: boolToInt64(t.NotNull),
})
if err != nil {
return 0, fmt.Errorf("create type %q: %w", expr, err)
}
return oid, nil
}
// ArraySuffix is the suffix a schema appends to an element type's spelling
// to name an array of it, which ParseTypeExpr reads as one array dimension.
const ArraySuffix = "[]"
// CreateUserType registers a type family a schema declared rather than the
// dialect, such as an enum or a name the dialect's seed does not list. The
// type gains the dialect's comparison operators, so a column of it can be
// compared.
func (c *Catalog) CreateUserType(name, category string) (int64, error) {
typtype := "b"
if category == "E" {
typtype = "e"
}
nsOID, bare, err := c.declaredTypeNamespace(strings.ToLower(name))
if err != nil {
return 0, fmt.Errorf("create type %q: %w", name, err)
}
spec := TypeSpec{
Name: bare,
NamespaceOID: nsOID,
Typtype: typtype,
Category: category,
DialectOID: c.dialectOID,
}
// A dialect may say what an unknown spelling stands on, as SQLite's
// affinity rule does; the type then resolves through that base and
// needs no operators of its own.
if category == "U" {
baseOID, err := c.userTypeBase(bare)
if err != nil {
return 0, err
}
if baseOID != 0 {
base, err := c.LookupType(baseOID)
if err != nil {
return 0, err
}
spec.BaseOID = baseOID
spec.Category = base.Category
return c.CreateTypeSpec(spec)
}
}
oid, err := c.CreateTypeSpec(spec)
if err != nil {
return 0, err
}
if err := c.createComparisons(oid); err != nil {
return 0, err
}
return oid, nil
}
// createComparisons gives a type the dialect's comparison operators, which the
// seed registered for the types it knew about up front.
func (c *Catalog) createComparisons(typeOID int64) error {
if c.dialectOID == 0 {
return nil
}
ops, _ := c.DialectFlag(c.dialectOID, FlagComparisonOperators)
boolName, _ := c.DialectFlag(c.dialectOID, "const."+ConstBool)
if ops == "" || boolName == "" {
return nil
}
boolOID, err := c.TypeOID(boolName)
if err != nil {
return nil
}
for _, op := range strings.Split(ops, ",") {
if op == "" {
continue
}
if _, err := c.CreateOperator(OperatorSpec{
Name: op,
DialectOID: c.dialectOID,
LeftTypeOID: typeOID,
RightTypeOID: typeOID,
ResultTypeOID: boolOID,
}); err != nil {
return err
}
}
return nil
}
// TypeOIDsInCategory returns the type families the catalog's dialect has in
// the named category, in the order they were created.
func (c *Catalog) TypeOIDsInCategory(category string) ([]int64, error) {
oids, err := c.q.TypeOIDsInCategory(context.Background(), catalogdb.TypeOIDsInCategoryParams{
DialectOid: nullInt64(c.dialectOID),
Category: nullString(category),
})
if err != nil {
return nil, fmt.Errorf("types in category %q: %w", category, err)
}
return oids, nil
}
// TypeOID returns the family a name refers to: an alias spelling resolves to
// the type it names.
func (c *Catalog) TypeOID(name string) (int64, error) {
oid, err := c.familyOIDByName(strings.ToLower(name))
if err != nil {
return 0, fmt.Errorf("type %q: %w", name, err)
}
return c.canonicalOID(oid)
}
// familyOIDByName finds the family row a bare name refers to, alias rows
// included, in the default namespaces: the catalog's own, PostgreSQL's
// system catalog and the dialect's default schema, in that order of
// preference. A type in any other namespace is reached by qualifying it,
// as PostgreSQL reaches one off the search path.
func (c *Catalog) familyOIDByName(name string) (int64, error) {
nsOIDs, err := c.defaultNamespaceOIDs()
if err != nil {
return 0, err
}
rows, err := c.q.TypeOIDsByNameInNamespaces(context.Background(), catalogdb.TypeOIDsByNameInNamespacesParams{
Name: name,
NamespaceOids: nsOIDs,
})
if err != nil {
return 0, err
}
for _, ns := range nsOIDs {
for _, row := range rows {
if row.NamespaceOid == ns {
return row.Oid, nil
}
}
}
return 0, sql.ErrNoRows
}
// defaultNamespaceOIDs lists the namespaces a bare type name is looked up
// in, in order of preference, skipping any the catalog does not have yet.
func (c *Catalog) defaultNamespaceOIDs() ([]int64, error) {
names := []string{"pg_catalog", "public"}
if c.dialectOID != 0 {
if name, _ := c.DialectFlag(c.dialectOID, FlagDefaultSchema); name != "" {
names = append(names, name)
}
}
out := make([]int64, 0, len(names))
for _, name := range names {
oid, err := c.namespaceOIDByName(name)
if err != nil {
continue
}
out = append(out, oid)
}
return out, nil
}
// namespaceOIDByName is NamespaceOID with the answer remembered, since a
// type lookup asks for the same few namespaces every time. A namespace
// created after the answer was cached is found on the next miss.
func (c *Catalog) namespaceOIDByName(name string) (int64, error) {
if oid, ok := c.types.namespaceOID(name); ok {
return oid, nil
}
oid, err := c.NamespaceOID(name)
if err != nil {
return 0, err
}
c.types.putNamespace(oid, name)
return oid, nil
}
// familyOIDByQualifiedName is familyOIDByName for a name that may carry its
// namespace, as myschema.mood does: a qualified name is looked up in that
// namespace alone.
func (c *Catalog) familyOIDByQualifiedName(name string) (int64, error) {
ns, bare := splitQualifiedName(name)
if ns == "" {
return c.familyOIDByName(bare)
}
nsOID, err := c.namespaceOIDByName(ns)
if err != nil {
return 0, err
}
return c.q.TypeOIDByNameInNamespace(context.Background(), catalogdb.TypeOIDByNameInNamespaceParams{
NamespaceOid: nsOID,
Name: bare,
})
}
// TypeDeclared reports whether a schema's CREATE TYPE would redeclare a
// type: one of that name in the namespace the name qualifies, or in the
// default namespaces for a bare name.
func (c *Catalog) TypeDeclared(name string) bool {
_, err := c.familyOIDByQualifiedName(strings.ToLower(name))
return err == nil
}
// splitQualifiedName splits "myschema.mood" into its namespace and name. A
// name with no dot has no namespace.
func splitQualifiedName(name string) (ns, bare string) {
if i := strings.LastIndexByte(name, '.'); i > 0 {
return name[:i], name[i+1:]
}
return "", name
}
// declaredTypeNamespace is the namespace a declared type's row goes in: the
// one its name qualifies, created if the schema has not, or the dialect's
// default schema — dbo, main — so that a bare CREATE TYPE and a qualified
// reference to it name one row.
func (c *Catalog) declaredTypeNamespace(name string) (int64, string, error) {
ns, bare := splitQualifiedName(name)
if ns == "" {
if c.dialectOID != 0 {
ns, _ = c.DialectFlag(c.dialectOID, FlagDefaultSchema)
}
if ns == "" {
return 0, bare, nil
}
}
oid, err := c.NamespaceOID(ns)
if err != nil {
if oid, err = c.CreateNamespace(ns); err != nil {
return 0, "", err
}
}
return oid, bare, nil
}
// CreateTypeWithArgs registers a declared type that has arguments of its own
// — a composite's fields, an enum's labels — as a family row carrying them.
// The arguments' types are interned first.
func (c *Catalog) CreateTypeWithArgs(spec TypeSpec, args []TypeArg) (int64, error) {
nsOID, bare, err := c.declaredTypeNamespace(strings.ToLower(spec.Name))
if err != nil {
return 0, fmt.Errorf("create type %q: %w", spec.Name, err)
}
spec.Name = bare
if nsOID != 0 {
spec.NamespaceOID = nsOID
}
if spec.DialectOID == 0 {
spec.DialectOID = c.dialectOID
}
argOIDs := make([]int64, len(args))
for i, a := range args {
if a.Type == nil {
continue
}
oid, _, err := c.internType(a.Type, func(name string) (int64, error) {
return c.CreateUserType(name, "U")
}, true)
if err != nil {
return 0, fmt.Errorf("create type %q: %w", spec.Name, err)
}
argOIDs[i] = oid
}
oid, err := c.CreateTypeSpec(spec)
if err != nil {
return 0, err
}
if err := c.createComparisons(oid); err != nil {
return 0, err
}
return oid, c.insertTypeArgs(oid, spec.Expr, args, argOIDs)
}
// insertTypeArgs writes a type's argument rows.
func (c *Catalog) insertTypeArgs(oid int64, key string, args []TypeArg, argOIDs []int64) error {
ctx := context.Background()
for i, a := range args {
p := catalogdb.CreateTypeArgParams{TypeOid: oid, Ord: int64(i + 1), Label: a.Label}
switch {
case a.Type != nil:
p.ArgTypeOid = nullInt64(argOIDs[i])
p.Nullable = boolToInt64(a.Type.Nullable)
case a.Int != nil:
p.IntValue = sql.NullInt64{Int64: *a.Int, Valid: true}
case a.Bool != nil:
p.BoolValue = sql.NullInt64{Int64: boolToInt64(*a.Bool), Valid: true}
case a.String != nil:
p.StringValue = sql.NullString{String: *a.String, Valid: true}
case a.Ident != nil:
p.Ident = sql.NullString{String: *a.Ident, Valid: true}
}
if err := c.q.CreateTypeArg(ctx, p); err != nil {
return fmt.Errorf("type %q: argument %d: %w", key, i+1, err)
}
}
return nil
}
// canonicalOID follows an alias row to the row it stands for.
func (c *Catalog) canonicalOID(oid int64) (int64, error) {
for i := 0; i < 16; i++ {
info, err := c.LookupType(oid)
if err != nil {
return 0, err
}
if info.CanonicalOID == 0 {
return oid, nil
}
oid = info.CanonicalOID
}
return 0, fmt.Errorf("type oid %d: alias chain does not end", oid)
}
// ResolutionOID is the row an operator, function or cast over the type is
// looked up on when none is registered on the type itself: an instance's
// family, an alias's canonical type, a domain's or wrapper's base. It
// returns 0 when there is nothing further to fall back to.
func (c *Catalog) ResolutionOID(oid int64) int64 {
info, err := c.LookupType(oid)
if err != nil {
return 0
}
switch {
case info.CanonicalOID != 0:
return info.CanonicalOID
case info.FamilyOID != 0:
return info.FamilyOID
case info.BaseOID != 0:
return info.BaseOID
}
return 0
}
// ResolutionChain lists the type and every row resolution falls back to, in
// order, ending with the family everything about the type is registered on.
func (c *Catalog) ResolutionChain(oid int64) []int64 {
chain := []int64{oid}
for i := 0; i < 16 && oid != 0; i++ {
oid = c.ResolutionOID(oid)
if oid != 0 {
chain = append(chain, oid)
}
}
return chain
}
// TypeName returns the family name of a type row.
func (c *Catalog) TypeName(oid int64) (string, error) {
info, err := c.LookupType(oid)
if err != nil {
return "", err
}
return info.Name, nil
}
// LookupType returns what the catalog holds about a type row.
func (c *Catalog) LookupType(oid int64) (TypeInfo, error) {
if info, ok := c.types.info(oid); ok {
return info, nil
}
row, err := c.q.LookupType(context.Background(), oid)
if err != nil {
return TypeInfo{}, fmt.Errorf("lookup type oid %d: %w", oid, err)
}
info := TypeInfo{
OID: row.Oid,
NamespaceOID: row.NamespaceOid,
Name: row.Name,
Expr: row.Expr,
Category: row.Category.String,
Typtype: row.Typtype,
Preferred: row.Preferred != 0,
FamilyOID: orZero(row.FamilyOid),
ElementOID: orZero(row.ElementOid),
BaseOID: orZero(row.BaseOid),
CanonicalOID: orZero(row.CanonicalOid),
NotNull: row.NotNull != 0,
}
c.types.put(info, nil)
return info, nil
}
// namespaceName is the name of a namespace row, remembered once read.
func (c *Catalog) namespaceName(oid int64) (string, error) {
if name, ok := c.types.namespace(oid); ok {
return name, nil
}
namespaces, err := c.Namespaces()
if err != nil {
return "", err
}
for _, ns := range namespaces {
c.types.putNamespace(ns.OID, ns.Name)
}
name, _ := c.types.namespace(oid)
return name, nil
}
// TypeExprOf is the expression a type row stands for, read back from its
// arguments: the family's name for a family, the family applied to its
// arguments for an instance. The result is the caller's to change.
func (c *Catalog) TypeExprOf(oid int64) (*TypeExpr, error) {
if e, ok := c.types.expr(oid); ok {
return e.Clone(), nil
}
info, err := c.LookupType(oid)
if err != nil {
return nil, err
}
expr := &TypeExpr{Name: c.qualifiedName(info)}
if !info.IsFamily() {
rows, err := c.q.TypeArgs(context.Background(), oid)
if err != nil {
return nil, fmt.Errorf("type oid %d: arguments: %w", oid, err)
}
for _, r := range rows {
arg := TypeArg{Label: r.Label}
switch {
case r.ArgTypeOid.Valid:
t, err := c.TypeExprOf(r.ArgTypeOid.Int64)
if err != nil {
return nil, err
}
t.Nullable = r.Nullable != 0
arg.Type = t
case r.IntValue.Valid:
v := r.IntValue.Int64
arg.Int = &v
case r.BoolValue.Valid:
v := r.BoolValue.Int64 != 0
arg.Bool = &v
case r.StringValue.Valid:
v := r.StringValue.String
arg.String = &v
case r.Ident.Valid:
v := r.Ident.String
arg.Ident = &v
}
expr.Args = append(expr.Args, arg)
}
}
c.types.put(info, expr)
return expr.Clone(), nil
}
// qualifiedName is the name a type row is known by in an expression: its
// name, qualified with its namespace when that is not one of the dialect's
// defaults, as format_type prints a type off the search path. An
// instance's key is built from these, so the array of one schema's mood is
// a row apart from the array of another's.
func (c *Catalog) qualifiedName(info TypeInfo) string {
if ns, err := c.namespaceName(info.NamespaceOID); err == nil && ns != "" && !slices.Contains(c.DefaultNamespaces(), ns) {
return ns + "." + info.Name
}
return info.Name
}
// ResolveTypeExpr interns the type an expression names and returns its row:
// the family for a bare name, the instance for a family applied to
// arguments, each argument type interned first. Names are canonicalized, so
// integer and int4 intern to one row. A family the dialect did not seed is
// one the schema declared, and is registered as a user type.
func (c *Catalog) ResolveTypeExpr(t *TypeExpr) (int64, error) {
oid, _, err := c.internType(t, func(name string) (int64, error) {
return c.CreateUserType(name, "U")
}, true)
return oid, err
}
// ResolvePseudoTypeExpr is ResolveTypeExpr for the type a function signature
// names. Signatures reference pseudo-types ("any", "record") and types no
// dialect bothers to list, so an unknown family is registered as an opaque
// type rather than rejected, and gets no operators of its own.
func (c *Catalog) ResolvePseudoTypeExpr(t *TypeExpr) (int64, error) {
oid, _, err := c.internType(t, func(name string) (int64, error) {
return c.CreateTypeSpec(TypeSpec{Name: name, Category: "U", DialectOID: c.dialectOID})
}, true)
return oid, err
}
// ResolveTypeName is ResolveTypeExpr for a type spelled as a string.
func (c *Catalog) ResolveTypeName(name string) (int64, error) {
return c.ResolveTypeExpr(ParseTypeExpr(name))
}
var errUnknownType = errors.New("unknown type")
// TypeLookup is what LookupTypeExpr found for an expression.
type TypeLookup struct {
// OID is the instance row when the catalog holds one, otherwise the
// family row.
OID int64
// FamilyOID is the family, which is OID for a family or an instance the
// catalog does not hold.
FamilyOID int64
// Expr is the expression canonicalized: the family and every argument
// type spelled as the catalog spells them, whether or not the instance
// is a row.
Expr *TypeExpr
}
// LookupTypeExpr finds the row an expression names without writing, and
// reports false when the family is not one the catalog holds.
func (c *Catalog) LookupTypeExpr(t *TypeExpr) (TypeLookup, bool) {
refuse := func(string) (int64, error) { return 0, errUnknownType }
oid, canonical, err := c.internType(t, refuse, false)
if errors.Is(err, errUnknownType) && canonical != nil {
// The family is known and the instance is not a row.
familyOID, _, err := c.internType(&TypeExpr{Name: canonical.Name}, refuse, false)
if err != nil {
return TypeLookup{}, false
}
return TypeLookup{OID: familyOID, FamilyOID: familyOID, Expr: canonical}, true
}
if err != nil {
return TypeLookup{}, false
}
info, err := c.LookupType(oid)
if err != nil {
return TypeLookup{}, false
}
familyOID := oid
if !info.IsFamily() {
familyOID = info.FamilyOID
}
return TypeLookup{OID: oid, FamilyOID: familyOID, Expr: canonical}, true
}
// internType resolves an expression to its row, creating the instance row
// when there is none and write allows it, and calling newFamily for a
// family name the catalog does not hold, which may refuse. Alongside the
// row it returns the expression canonicalized; when the instance is not a
// row and writing is not allowed, the canonical expression still comes
// back with errUnknownType.
func (c *Catalog) internType(t *TypeExpr, newFamily func(name string) (int64, error), write bool) (int64, *TypeExpr, error) {
if t == nil || strings.TrimSpace(t.Name) == "" {
return 0, nil, fmt.Errorf("missing type name")
}
t, err := c.canonicalize(t)
if err != nil {
return 0, nil, err
}
name := strings.ToLower(strings.TrimSpace(t.Name))
familyOID, err := c.familyOIDByQualifiedName(name)
if err != nil {
if name == ArrayTypeName && write {
// Every dialect has arrays, whether or not its seed lists the
// family; one that does not gets it as an array type rather
// than a user type.
familyOID, err = c.CreateTypeSpec(TypeSpec{Name: name, Category: "A", DialectOID: c.dialectOID})
} else {
familyOID, err = newFamily(name)
}
if err != nil {
return 0, nil, fmt.Errorf("type %q: %w", name, err)
}
}
if familyOID, err = c.canonicalOID(familyOID); err != nil {
return 0, nil, err
}
family, err := c.LookupType(familyOID)
if err != nil {
return 0, nil, err
}
if len(t.Args) == 0 {
return familyOID, &TypeExpr{Name: c.qualifiedName(family)}, nil
}
// The instance's canonical spelling is the family applied to its
// arguments as the catalog spells them, so each argument type is
// resolved first and read back. An argument whose own instance is not
// a row, in a lookup that may not write, still has a canonical
// spelling, which the whole expression's is built from.
canonical := &TypeExpr{Name: c.qualifiedName(family), Args: make([]TypeArg, len(t.Args))}
argOIDs := make([]int64, len(t.Args))
unknown := false
for i, a := range t.Args {
canonical.Args[i] = a
if a.Type == nil {
continue
}
oid, argExpr, err := c.internType(a.Type, newFamily, write)
if err != nil {
if !errors.Is(err, errUnknownType) || argExpr == nil {
return 0, nil, err
}
unknown = true
}
argOIDs[i] = oid
argExpr.Nullable = a.Type.Nullable
canonical.Args[i].Type = argExpr
}
if unknown {
return 0, canonical, errUnknownType
}
key := canonical.Key()
ctx := context.Background()
if oid, err := c.q.TypeOIDByExprInNamespace(ctx, catalogdb.TypeOIDByExprInNamespaceParams{
NamespaceOid: family.NamespaceOID,
Expr: key,
}); err == nil {
return oid, canonical, nil
} else if !errors.Is(err, sql.ErrNoRows) {
return 0, nil, fmt.Errorf("type %q: %w", key, err)
}
// A lookup that may not write stops here, canonical expression in hand.
if !write {
return 0, canonical, errUnknownType
}
spec := TypeSpec{
Name: family.Name,
Expr: key,
Typtype: family.Typtype,
Category: family.Category,
NamespaceOID: family.NamespaceOID,
DialectOID: c.dialectOID,
FamilyOID: familyOID,
}
if family.Name == ArrayTypeName && argOIDs[0] != 0 {
spec.ElementOID = argOIDs[0]
}
oid, err := c.CreateTypeSpec(spec)
if err != nil {
return 0, nil, err
}
if err := c.insertTypeArgs(oid, key, canonical.Args, argOIDs); err != nil {
return 0, nil, err
}
return oid, canonical, nil
}
func nullableOID(oid int64) any {
if oid == 0 {
return nil
}
return oid
}
func nullableString(s string) any {
if s == "" {
return nil
}
return s
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
func nullInt64(oid int64) sql.NullInt64 {
return sql.NullInt64{Int64: oid, Valid: oid != 0}
}
func nullString(s string) sql.NullString {
return sql.NullString{String: s, Valid: s != ""}
}
func boolToInt64(b bool) int64 {
if b {
return 1
}
return 0
}
func orZero(n sql.NullInt64) int64 {
if n.Valid {
return n.Int64
}
return 0
}

769
internal/core/types.md Normal file
View File

@@ -0,0 +1,769 @@
# Types in the analysis core
How the core catalog, the analyzer and `sqlc analyze` represent a type today,
where that falls short of each engine's type system, how each engine's own
catalog represents one, and the design that closes the gap: every type the
schema or the dialect declares is a row holding its full expression,
canonicalized the way the engine would and denormalized so that a bare name
like `integer` and a structured expression like `numeric(10, 2)` both
resolve to one.
## What a type is today
The catalog (`catalogdef/schema.sql`) has one row per *name* in `sql_type`:
a lowercased string, a category letter, a `typtype` that is only ever `b` or
`e`, and an `element_oid` that is written for arrays but never read. A
dialect's `types.jsonl` seeds one row per type and one more per alias, then
joins every spelling of a type to every other with implicit casts. Arrays are
rows named after their element with `[]` appended, created on first use.
Anything else — a type argument, a struct field, an enum label, a domain's
base, a range's subtype — has nowhere to go: `sql_attribute` keeps the
column's verbatim spelling in `decl_type` (only SQLite and ClickHouse set it)
and has `type_length` and `type_scale` columns that no DDL path fills.
The analyzer (`analyzer/expr.go`) types an expression as `exprType`: a type
OID, or a bare name when the catalog has no row, plus nullability. A type is
therefore a row, and a type that is not a row degrades to whatever row its
spelling's first word finds.
The output (`TypeExpr` in `typeexpr.go`, printed by `sqlc analyze`) already
has the right shape: a name applied to labelled arguments that are types,
integers, booleans or strings, with `nullable` at any depth. It is complete
only where a source column's `decl_type` spelling exists to be parsed; every
other column and every parameter is rebuilt from the flat row name.
## What each engine loses
The table is what `sqlc analyze` reports today for a schema exercising each
engine's type system, against what the engine itself says the type is.
| Engine | Declared | Reported | The engine says |
|---|---|---|---|
| PostgreSQL | `numeric(10,2)`, `varchar(255)`, `timestamp(3)`, `bit(8)` | `numeric`, `varchar`, `timestamp`, `bit` | typmods are part of the type |
| PostgreSQL | `int[][]` | `array(int4)` | two dimensions |
| PostgreSQL | `CREATE DOMAIN posint AS integer` | `posint`, category U | an integer with a constraint |
| PostgreSQL | `CREATE TYPE point2 AS (x float8, y float8)` | `point2`, category U | two named fields |
| PostgreSQL | `CREATE TYPE mood AS ENUM (...)` | `mood` | the labels |
| PostgreSQL | `CREATE TYPE floatrange AS RANGE (subtype = float8)` | `floatrange`, category U | a range over float8 |
| PostgreSQL | `myschema.mood` | `mood` for a column, `myschema.mood` for a cast: two rows, both in `public` | one type in a namespace |
| PostgreSQL | `interval day to second` | `interval` | fields are a typmod |
| MySQL | `BIGINT UNSIGNED`, `INT UNSIGNED` | `bigint`, `int` | a different value range; codegen picks `int64` over `uint64` |
| MySQL | `TINYINT(1)` | `tinyint` | the display width is how drivers and codegen spot a boolean |
| MySQL | `DECIMAL(10,2) UNSIGNED`, `DATETIME(6)`, `VARCHAR(255)` | `decimal`, `datetime`, `varchar` | precision, fractional seconds, length |
| MySQL | `ENUM('a','b')`, `SET('x','y')` | `enum`, `set` | the members |
| MySQL | `CAST(? AS CHAR(10))` | `var_string` | `char`; the parser's internal name leaks |
| SQLite | `FOO BAR(3)`, `VARCHAR(255)` | `foo bar(3)`, `varchar(255)` | correct spelling, but each is a row of category U that compares with nothing, and the affinity SQLite gives it (NUMERIC, TEXT) is not modelled |
| ClickHouse | every column type | complete | complete, from the spelling |
| ClickHouse | `CAST(x AS Nullable(String))` | `nullable` | `Nullable(String)` |
| ClickHouse | `CAST(x AS Array(UInt8))` | `array` | `Array(UInt8)` |
| ClickHouse | `toDecimal64(x, 4)`, `toDateTime64(x, 3)` | `decimal64`, `datetime64` | `Decimal(18, 4)`, `DateTime64(3)`: the result depends on an argument's value |
| ClickHouse | `SimpleAggregateFunction(sum, UInt64)` | `simpleaggregatefunction(sum, uint64)` | `sum` is a function name, which the expression reads as a type |
| ClickHouse | `n Nested(a UInt8, b String)` | one column `n` | two columns `n.a Array(UInt8)`, `n.b Array(String)` |
| DuckDB | `STRUCT(a INTEGER, b VARCHAR)`, `MAP(VARCHAR, INTEGER)`, `UNION(num INTEGER, str VARCHAR)` | `struct`, `map`, `union` | the fields |
| DuckDB | `INTEGER[]`, `INTEGER[3]`, `INTEGER[][]` | `array(integer)` for all three | LIST, fixed-size ARRAY, nested LIST |
| DuckDB | `DECIMAL(18,3)`, `VARCHAR(10)`, `ENUM('a','b')` | `decimal`, `varchar`, `enum` | arguments and members |
| GoogleSQL | `ARRAY<INT64>` | a row *named* `array<int64>` | an array of int64 |
| GoogleSQL | `STRUCT<a INT64, b STRING>`, `ARRAY<STRUCT<x INT64>>` | `struct`, `array<struct>` | the fields |
| GoogleSQL | `STRING(10)`, `NUMERIC(10,2)`, `[1, 2]`, `STRUCT(1 AS x)` | `string`, `numeric`, untyped, untyped | parameters; a constructed array and struct |
| SQL Server | `NVARCHAR(MAX)`, `VARBINARY(MAX)` | `nvarchar`, `varbinary` | MAX decides the Go type |
| SQL Server | `DECIMAL(10,2)`, `DATETIME2(3)`, `FLOAT(24)`, `VECTOR(3)` | `decimal`, `datetime2`, `float`, `vector` | arguments; `FLOAT(24)` is `real` |
| SQL Server | `CREATE TYPE dbo.PhoneNumber FROM varchar(20) NOT NULL` | `phonenumber`, category U | a `varchar(20)` that is never null, in schema `dbo` |
Three of these are regressions against the legacy compiler rather than gaps
shared with it: the plugin protocol's `Column` carries `unsigned`, `length`
and `array_dims`, codegen reads all three (`golang/mysql_type.go` turns
`tinyint` with length 1 into `bool` and `unsigned` into `uint64`;
`golang/go_type.go` nests one slice per dimension), and the core bridge in
`compiler/parse_core.go` sets none of them from what the core reports.
Two are broken outside column declarations only: ClickHouse's columns are
whole because the engine hands the core a spelling and the core keeps it on
the attribute. The same spelling in a cast, a function result or a typed
placeholder has no attribute to live on, so it degrades to its first word.
That is the tell: the expression belongs to the type, not to the column.
## What each engine's own catalog says
Each engine was asked, on a live system where one could be had, what it
records about a type, how it spells a column's type back, what it says a
query result's type is, and how it describes a function. PostgreSQL 16,
MySQL 8, SQLite 3.53, ClickHouse 25.8 and DuckDB 1.5 answered directly; SQL
Server and GoogleSQL are from their documented catalogs and the parsers
sqlc uses for them.
**PostgreSQL** has one `pg_type` row per family, and a second row per array
type (`_int4`, with `typelem` pointing at the element). Arguments are not
part of the type: they are an opaque `int32` typmod on the use site —
`pg_attribute.atttypmod`, a domain's `typtypmod`, a function argument's
type has none — whose encoding is the type's own business and which
`format_type(oid, typmod)` decodes back into a spelling. Dimensions are
likewise on the attribute (`attndims`) and informational: `int[3]` and
`int[][]` are both the type `integer[]`. Declared types are rows with a
`typtype`: `e` with labels and their order in `pg_enum`, `c` with fields as
the attributes of a hidden relation (`typrelid`), `d` with `typbasetype`,
`typtypmod` and `typnotnull`, `r` and `m` with the subtype in `pg_range`;
pseudo-types (`anyarray`, `record`) are rows of `typtype` `p` and category
`P`. What it reports is canonical and not what was written: `int` comes back
as `integer`, `varchar(255)` as `character varying(255)`, `timestamp(3)` as
`timestamp(3) without time zone`, `int[3]` as `integer[]`, and a domain
column as the domain's name. A view keeps its columns' typmods; a prepared
statement's result types (`pg_prepared_statements.result_types`, the
protocol's RowDescription) carry the typmod only for a column read straight
from a table and `-1` for an expression, so `numeric(10,2) + 1` is
`numeric`. `pg_proc` has full signatures over families and pseudo-types,
`pg_operator` likewise, and `pg_cast` the contexts.
**MySQL** has no catalog of types, functions or operators. What it has is
`information_schema.COLUMNS`, which describes a column twice: `DATA_TYPE` is
the family (`bigint`, `decimal`, `enum`) and `COLUMN_TYPE` is the whole
canonical spelling — `bigint unsigned`, `tinyint(1)`, `decimal(10,2)
unsigned`, `enum('a','b')`, `bigint(20) unsigned zerofill` — beside the
arguments decoded into `NUMERIC_PRECISION`, `NUMERIC_SCALE`,
`CHARACTER_MAXIMUM_LENGTH`, `DATETIME_PRECISION`, `CHARACTER_SET_NAME` and
`COLLATION_NAME`. The spelling is canonical: `BOOLEAN` becomes `tinyint(1)`,
`INTEGER` becomes `int`, `VARCHAR(10) CHARACTER SET binary` becomes
`varbinary(10)`. A view's columns show that the binder computes a full type
for every expression: `decimal(10,2) + 1` is `decimal(11,2)`, `decimal(10,2)
* decimal(10,2)` is `decimal(20,4)`, `int unsigned + 1` is `bigint
unsigned`, `CONCAT(varchar(255), 'x')` is `varchar(256)`, `SUM(int unsigned)`
is `decimal(32,0)`, `AVG(int)` is `decimal(14,4)`, `->>` is `longtext`. User
routines are described the same way in `ROUTINES` and `PARAMETERS`
(`DTD_IDENTIFIER` is `decimal(5,2)`, `bigint unsigned`); built-in functions
are not described at all. A result set on the wire is coarser than the
catalog: a storage type code (`TINY`, `LONG`, `NEWDECIMAL`, `VAR_STRING`),
a length in bytes, a decimals count and flags (`UNSIGNED`, `NOT_NULL`,
`ENUM`, `SET`, `BINARY`), so a `varchar(255)` column and `CAST(x AS
CHAR(10))` are both `VAR_STRING`, an enum is `STRING` with the `ENUM` flag,
and a `tinyint(1)` is `TINY` with length 1. That code is where the parser's
`var_string` comes from.
**SQLite** has no catalog of types either. `pragma_table_xinfo` returns a
column's declared type verbatim — `VARCHAR(255)`, `FOO BAR(3)`, `my type`,
or nothing — and the type system is affinity: a rule over the spelling's
substrings (INT anywhere is INTEGER; CHAR, CLOB or TEXT is TEXT; BLOB or no
type is BLOB; REAL, FLOA or DOUB is REAL; anything else is NUMERIC) that
decides how a stored value is coerced, so `FOO BAR(3)` holding `'12'`
stores the integer 12. Arguments are decoration: `CAST(x AS DECIMAL(5,2))`
applies NUMERIC affinity and nothing else. A `STRICT` table admits only
`INT`, `INTEGER`, `REAL`, `TEXT`, `BLOB` and `ANY`, and rejects
`VARCHAR(10)`. An expression has no type; a value has a storage class
(`typeof()` is `integer`, `real`, `text`, `blob` or `null`), and a result
column's `sqlite3_column_decltype` is the declared spelling for a column
read from a table and nothing for anything else. `pragma_function_list`
names functions and their arity and nothing more.
**ClickHouse** models a type as a call expression, which is why the output
shape was chosen. `system.data_type_families` lists 66 families and 73
aliases (`INT` is an alias of `Int32`); a column's type in `system.columns`
is the whole expression, canonicalized: `Decimal32(4)` is stored as
`Decimal(9, 4)`, `Enum('a', 'b')` as `Enum8('a' = 1, 'b' = 2)`,
`Variant(String, Int64)` with its members sorted, and `Nested(a UInt8, b
String)` as two columns `n.a Array(UInt8)` and `n.b Array(String)`.
`Nullable` and `LowCardinality` are spelled as wrappers and reported as part
of the type, at any depth. Every expression has a full type computed by the
binder, from argument types with promotion — `Int8 + UInt8` is `Int16`,
`Int32 + UInt64` is `Int64`, `Int32 / Int32` is `Float64`, `Decimal(9, 4) *
Decimal(38, 10)` is `Decimal(38, 14)` — from argument values —
`toDecimal64(x, 4)` is `Decimal(18, 4)`, `toDateTime64(x, 3)` is
`DateTime64(3)` — and from literals by value: `1` is `UInt8`, `-1` is
`Int8`, `[1, NULL]` is `Array(Nullable(UInt8))`. Wrappers propagate:
`concat(lc, 'x')` is `LowCardinality(String)`, `ns = 'x'` is
`Nullable(UInt8)`, `NULL` is `Nullable(Nothing)`. A typed placeholder
`{p:Decimal(10, 2)}` is exactly its declared type. `system.functions` has a
name, an aggregate flag, an alias and a description, and no signature.
**DuckDB** has `duckdb_types()`, one row per spelling with a `logical_type`
naming the family (`int`, `int4` and `integer` are three rows over
`INTEGER`), a category (`NUMERIC`, `STRING`, `DATETIME`, `BOOLEAN`,
`COMPOSITE`, or none for `bit` and `enum`), and `labels` for an enum, which
a `CREATE TYPE ... AS ENUM` adds to as a non-internal row in the user's
schema. `duckdb_columns().data_type` is the whole canonical expression:
`INTEGER[]`, `INTEGER[3]`, `STRUCT(a INTEGER, b VARCHAR)`, `MAP(VARCHAR,
INTEGER)`, `UNION(num INTEGER, str VARCHAR)`, `DECIMAL(18,3)`, with the
precision and scale also decoded into their own columns. Canonicalization
goes further than anywhere else: `TEXT` is `VARCHAR`, `VARCHAR(10)` is
`VARCHAR` (the length is dropped), `NUMERIC` is `DECIMAL(18,3)`, `VARINT` is
`BIGNUM`, `FLOAT4` is `FLOAT`, `JSON` keeps its name over `VARCHAR`'s id,
and a column of the named enum `mood` is reported as `ENUM('sad', 'ok')`
with the name gone. Expressions have full binder-computed types:
`DECIMAL(18,3) * DECIMAL(18,3)` is `DECIMAL(18,6)`, `1.5` is
`DECIMAL(2,1)`, `SUM(INTEGER)` is `HUGEINT`, `SUM(DECIMAL(18,3))` is
`DECIMAL(38,3)`, `INTEGER / 2` is `DOUBLE`, `[1, NULL]` is `INTEGER[]` with
no inner nullability, and `NULL` is a type of its own. `duckdb_functions()`
has signatures over families and generics — `parameter_types` are
`DECIMAL`, `INTEGER[]`, `ANY`, `T`, `K`, `V` and `return_type` a family —
so the arguments of a result are the binder's, not the catalog's. A
prepared statement's parameter types are `UNKNOWN` until bound.
**SQL Server** describes a column in `sys.columns` as a family plus decoded
arguments: `system_type_id` and `user_type_id`, `max_length` in bytes with
`-1` for `MAX`, `precision`, `scale`, `collation_name`, `is_nullable`,
`is_identity`. `sys.types` has one row per system type — `decimal` and
`numeric` are two — and one per alias type from `CREATE TYPE ... FROM`, with
`is_user_defined`, its own `max_length`, `precision`, `scale` and
`is_nullable`, and its base's `system_type_id`; `sysname` is such a row over
`nvarchar(128)`, and CLR types like `geography` and `hierarchyid` are
assembly types sharing one `system_type_id`. Canonicalization fills in what
was left out and folds one family into another: `varchar` alone is
`varchar(1)` in a declaration and `varchar(30)` in a `CAST`, `decimal` is
`decimal(18,0)`, `datetime2` is `datetime2(7)`, `FLOAT(24)` is stored as
`real` and `FLOAT(25)` and up as `float`. `sys.all_parameters` describes a
function's parameters the same way as a column. The T-SQL parser sqlc uses
(`teesql`) reports a type as a name with a parameter list in which `MAX` is
a literal node of its own.
**GoogleSQL** has no catalog sqlc can read offline; its two hosts describe a
column as a whole string. BigQuery's `INFORMATION_SCHEMA.COLUMNS.DATA_TYPE`
is `ARRAY<STRUCT<a INT64, b STRING>>`, `NUMERIC(10, 2)` or `STRING(10)`,
and `COLUMN_FIELD_PATHS` lists every nested struct field with a path and a
`DATA_TYPE` of its own. Spanner's `SPANNER_TYPE` is `STRING(MAX)`,
`ARRAY<INT64>`, `PROTO<pkg.Message>` or `ENUM<pkg.Enum>`, and a column
cannot be a struct. The parser sqlc uses (`zetajones`) has a node per type
form — `SimpleType` with a `TypeParameterList`, `ArrayType`, `StructType`
with named fields, `RangeType`, `MapType`, `FunctionType` — and, as in T-SQL,
`MAX` is a literal node in the parameter list. Arrays do not nest.
What the review settles:
| | Unit of identity | Where arguments live | Column spelling reported | Result types | Function signatures |
|---|---|---|---|---|---|
| PostgreSQL | family row, plus one row per array type | opaque typmod and dims on the use site | canonical, decoded by `format_type` | family, typmod only for a table column | full, over families and pseudo-types |
| MySQL | none: a spelling | decoded columns beside the spelling | canonical spelling | full, computed by the binder | none for built-ins |
| SQLite | none: the declared text | none: decoration | verbatim | storage class of the value | arity only |
| ClickHouse | family, with aliases | in the spelling | canonical expression | full, computed by the binder | none |
| DuckDB | family, with aliases | in the spelling, plus decoded columns | canonical expression, some arguments dropped | full, computed by the binder | families and generics |
| SQL Server | family row and alias-type row | decoded columns on the use site | canonical, defaults filled in | family and decoded arguments | families and decoded arguments |
| GoogleSQL | none offline: a spelling | in the spelling | canonical spelling | full | none offline |
Two conclusions follow. First, no engine keeps arguments inside its type
table: PostgreSQL and SQL Server put them on the use site and everything
else puts them in the spelling. A row per expression is nonetheless the
right shape for sqlc, because sqlc's job is to *report* the expression, and
every engine that decodes its use-site arguments decodes them into exactly
the call expression `TypeExpr` already is; PostgreSQL's typmod is a
per-type encoding that only PostgreSQL can read, so sqlc would store the
decoded form either way. Second, every engine with a catalog canonicalizes
on the way in and reports the canonical form, never the declared spelling,
and the same is true of what `goldeneye` checks the analyze cases against.
The design below changes on that point.
## The design
### One row per type expression
`sql_type` keeps one row per distinct type expression. A row is either a
**family** — a name the dialect or the schema declares, such as `numeric`,
`array`, `struct`, `mood` — or an **instance**, a family applied to
arguments, such as `numeric(10, 2)`, `array(int4)` or
`struct(a: int4, b: text)`. The row's `expr` is the expression's canonical
string, which is its interning key; the row's `name` is the family's name,
so the index that turns `integer` into a row keeps working for instances,
and an instance points at its family.
```sql
CREATE TABLE sql_type (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
namespace_oid INTEGER NOT NULL REFERENCES sql_namespace(oid),
dialect_oid INTEGER REFERENCES sql_dialect(oid),
name TEXT NOT NULL, -- the family name: 'numeric', 'array', 'mood'
expr TEXT NOT NULL, -- the whole expression, canonical: 'numeric(10, 2)'; equals name for a family
typtype TEXT NOT NULL DEFAULT 'b', -- b base, c composite, d domain, e enum, r range, p pseudo
category TEXT,
preferred INTEGER NOT NULL DEFAULT 0,
family_oid INTEGER REFERENCES sql_type(oid), -- NULL on a family; the family on an instance
element_oid INTEGER REFERENCES sql_type(oid), -- what the type holds: an array's element, a map's value, a range's subtype
base_oid INTEGER REFERENCES sql_type(oid), -- what the type stands on: a domain's or alias type's base, a wrapper's inner type, a SQLite spelling's affinity
canonical_oid INTEGER REFERENCES sql_type(oid), -- the row the engine reports this one as: integer -> int4, float(24) -> real, mood -> enum('sad', 'ok') in DuckDB
not_null INTEGER NOT NULL DEFAULT 0, -- a domain or alias type declared NOT NULL
UNIQUE (namespace_oid, expr)
);
CREATE INDEX idx_sql_type_name ON sql_type(name);
-- sql_type_arg: the arguments of an instance, or the fields, labels or
-- members of a declared composite, enum or set, in order. Exactly one of
-- arg_type_oid, int_value, bool_value, string_value and ident is set.
CREATE TABLE sql_type_arg (
type_oid INTEGER NOT NULL REFERENCES sql_type(oid),
ord INTEGER NOT NULL,
label TEXT NOT NULL DEFAULT '', -- a struct field, tuple element or enum label
arg_type_oid INTEGER REFERENCES sql_type(oid),
nullable INTEGER NOT NULL DEFAULT 0, -- the argument type is nullable here: Array(Nullable(String))
int_value INTEGER,
bool_value INTEGER,
string_value TEXT,
ident TEXT, -- a bare word that is not a type: max, sum, day to second
PRIMARY KEY (type_oid, ord)
);
```
`size` goes: nothing reads it. `type_length` and `type_scale` leave
`sql_attribute`: they were one engine's two arguments, and now every
engine's arguments are rows. `decl_type` stays, since the verbatim spelling
is what the formatter prints back and what SQLite reports.
The four pointer columns are the denormalization. Each is derivable by
walking `sql_type_arg`, and each is what resolution asks for in one
statement:
- `family_oid` is what operator and function lookup fall back to when there
is no overload on the instance: `numeric(10, 2) + numeric(5, 1)` finds no
operator on either instance and resolves on `numeric`, which is exactly
what `pg_operator` and `duckdb_functions()` hold.
- `element_oid` is what a subscript, `ANY($1)`, `unnest` and a star over a
map yield, and what `IsArray` was; it is PostgreSQL's `typelem` and
`pg_range.rngsubtype`.
- `base_oid` is what a domain, an alias type, `LowCardinality(T)` or a
SQLite spelling resolves through: `posint = 1` finds no operator on
`posint` and resolves on `int4`; `FOO BAR(3) = 1` resolves on `numeric`.
It is `typbasetype`, SQL Server's `system_type_id` behind a
`user_type_id`, and SQLite's affinity rule.
- `canonical_oid` is the row the engine would report this one as. An alias
spelling points at its family (`integer` at `int4`); a declared type
points at what the engine reports instead of its name when it does so
(DuckDB's `mood` at `enum('sad', 'ok')`); an instance points at the
instance canonicalization rewrote it to when the rewrite changes the
family (SQL Server's `float(24)` at `real`). It replaces the n² implicit
casts between spellings of one type.
A declared type is a family row with arguments of its own. `CREATE TYPE
point2 AS (x float8, y float8)` is a row named `point2`, `typtype` `c`, with
two labelled type arguments, the way PostgreSQL keeps them as the attributes
of `typrelid`; `CREATE TYPE mood AS ENUM ('sad', 'ok')` is a row with
`typtype` `e` and two string arguments in order, which is `pg_enum` and
DuckDB's `labels`; ClickHouse's `Enum8('active' = 1, 'deleted' = 2)` is an
instance of `enum8` with two labelled integer arguments and `base_oid` at
`int8`; `CREATE DOMAIN posint AS integer` and SQL Server's `CREATE TYPE
PhoneNumber FROM varchar(20) NOT NULL` are rows with `typtype` `d`,
`base_oid` at the base instance and `not_null` set. The same table holds
what the schema names and what it constructs anonymously.
### Canonicalize on the way in; report the canonical form
Every engine with a catalog rewrites a declared type before storing it and
reports the rewritten form, and `goldeneye` checks the analyze cases against
what the engine reports. So interning canonicalizes, in three steps that
are dialect data or dialect code:
1. **Aliases**, which `types.jsonl` already lists: `int` and `integer`
resolve to the canonical family. This also settles what the canonical
*name* is: the one the engine reports, which for PostgreSQL is
`format_type`'s (`integer`, `bigint`, `character varying`) rather than
`pg_type`'s (`int4`, `int8`, `varchar`), so PostgreSQL's `types.jsonl`
flips which spelling is the name and which the alias. Codegen accepts
both spellings already.
`format_type` is a string and the output is an expression, so the
spelling is read into one, and its grammar is slightly wider than
`name(args)`: the typmod may sit inside a multi-word name
(`timestamp(3) without time zone`, `time(4) with time zone`), after
trailing words (`interval day to second(3)`), or before an array suffix
(`numeric(10,2)[]`, `character varying(255)[]`), and one name is quoted
(`"char"`). The family is the words with the parenthesis lifted out, so
`timestamp(3) without time zone` is `{name: "timestamp without time
zone", args: [3]}`, `interval day to second(3)` is `interval` with an
identifier argument and an integer one, and `numeric(10,2)[]` is
`array(numeric(10, 2))`. Two spellings are the same family under
different typmods, `bpchar` and `character(5)`, and canonicalize to
`character`. The PostgreSQL goldens change with this: `bigserial`
becomes `bigint` (a serial is a default, not a type PostgreSQL reports),
`int4` becomes `integer`, `varchar` becomes `character varying`.
2. **Argument defaults and drops**, which are data: SQL Server's `varchar`
is `varchar(1)` and `decimal` is `decimal(18, 0)`; DuckDB's `numeric` is
`decimal(18, 3)` and `varchar(10)` is `varchar`; PostgreSQL's `int[3]` is
`array(int4)`. A family in `types.jsonl` may say `"defaults": [18, 0]`
or `"args": 0`.
3. **Rewrites that change the family by argument**, which are data too: an
ordered list of pattern, template and bound in `dialect.json` —
`Decimal32($1)` to `Decimal(9, $1)`, `float($1)` to `real` where `$1 <=
24`, `boolean` to `tinyint(1)`, `sysname` to `nvarchar(128)` — that the
seed loads into a table and the catalog applies before interning. What
only a parser can do stays in the engine's converter, which every engine
has anyway: ClickHouse numbers an Enum's members and sorts a Variant's
when it spells the type, and MySQL's parser folds `varchar(n) character
set binary` into `varbinary(n)`.
The reported type is the canonical row's expression. The declared spelling
is kept on the attribute in `decl_type`, for the formatter and for SQLite,
whose canonical form is the spelling itself. This reverses the earlier
choice of reporting a column as it was declared: the engines do not, and
the check that keeps the analyzer honest compares against the engines.
### Nullability is not a type
A row is never nullable. Outer nullability stays where it is: on the
attribute, on the analyzer's `exprType`, on the reported column. Inner
nullability is a flag on the argument position, so `Array(Nullable(String))`
is an instance of `array` whose one argument is `string` with `nullable`
set, and `Nullable(String)` in a cast is `string` with the expression's own
nullability set. This is what `TypeExpr` already says — `nullable` at
whatever depth it applies, never a wrapper — and it keeps `string` and
`string nullable` from being two types that need their own operators. The
review confirms only ClickHouse spells inner nullability at all: DuckDB's
`[1, NULL]` is `INTEGER[]` and PostgreSQL has no such thing.
### Array dimensions
PostgreSQL's type for `int[][]` is `integer[]`, with the dimensions on the
attribute and unenforced; DuckDB and ClickHouse nest, `INTEGER[][]` and
`Array(Array(UInt8))`, and DuckDB distinguishes the fixed-size
`INTEGER[3]`. The expression nests one `array` per declared dimension in
every dialect, with a fixed size as a second argument (`array(integer, 3)`),
because codegen renders one slice per dimension and a two-dimensional
PostgreSQL column has to come out `[][]int32` as it does on the legacy path.
PostgreSQL's canonicalization therefore keeps the dimensions its own
catalog drops, and a future PostgreSQL check in `goldeneye` reads `attndims`
to reproduce them. Subscripting follows the dialect: PostgreSQL yields the
innermost element however many subscripts are applied.
### The catalog interns at schema time; the analyzer looks up at query time
The cached catalog is opened read-only (`catalog.go` opens it with
`mode=ro&immutable=1`), and `exprType` carries a bare name precisely so that
analysis never has to write. That constraint stands, and it decides which
expressions become rows:
- **Interned**: what the dialect seeds and what the schema declares. Every
column type, every declared type's fields and base, every function
signature's argument and return type becomes a row on load, through one
entry point, `ResolveTypeExpr(*TypeExpr) (oid, error)`, which
canonicalizes, then walks the expression bottom-up, interning each
argument type first. `ResolveType` and `ResolveTypeName` become callers
of it.
- **Looked up**: what a query writes. A cast, a constructed array or struct,
a typed placeholder and a function result are resolved by
`LookupTypeExpr(*TypeExpr) (oid, familyOID, bool)`, which canonicalizes,
finds the instance row when the schema happened to declare the same
expression and otherwise the family row, and never writes.
`exprType` becomes the pair:
```go
type exprType struct {
typeOID int64 // the instance row when the catalog has one, else the family row, else 0
expr *core.TypeExpr // the whole expression, whenever anything is known about it
nullable bool
...
}
```
Resolution uses `typeOID` and its `family_oid`, `base_oid` and
`canonical_oid` chain; reporting uses `expr`. The `typeName` fallback and
the `[]` suffix convention go away: an array is `array` applied to its
element, in the catalog as in the output, and `TypeNameString` is replaced
by a function that reads an `ast.TypeName` into a `TypeExpr`, folding
`Typmods` into integer arguments, `ArrayBounds` into one `array` per
dimension, `Names` into a namespace and a name, and `Spelling` through
`ParseTypeExpr`.
### Result types: the family from the catalog, the arguments from the dialect
MySQL, ClickHouse and DuckDB compute a full type for every expression in the
binder — `decimal(10,2) + 1` is `decimal(11,2)`, `Int8 + UInt8` is `Int16`,
`SUM(INTEGER)` is `HUGEINT` — and none of them keeps those rules in a
catalog; PostgreSQL's catalog says `numeric + numeric` is `numeric` and its
results drop the typmod. The catalog can only ever answer at the family
level, and that is the baseline every dialect gets: an operator or function
result is the family the overload names, with the arguments of an `$n`
result carried over from the argument it stands for. A result that depends
on an argument's value is a template in the seed — `"returns": "Decimal(18,
$2)"` for `toDecimal64(x, s)` — that the analyzer fills in from the call's
literals. ClickHouse needs it, since its check compares whole expressions;
MySQL's check reads the wire type, which is the family with its flags, so
the baseline passes it. Arithmetic promotion — `Int8 + UInt8` is `Int16` —
is not expressed yet.
### What each engine hands the core
The contract with an engine is that its `ast.TypeName` reads into a
`TypeExpr` that says everything the engine's own catalog would. Where an
engine already folds its type into a spelling, `ParseTypeExpr` reads it;
where it does not, the converter has a small change to make.
| Engine | Form | Expression |
|---|---|---|
| PostgreSQL | `numeric(10,2)`, `varchar(255)`, `timestamp(3) with time zone` | `numeric(10, 2)`, `character varying(255)`, `timestamp with time zone(3)`: typmods become integer arguments on the canonical family, named as `format_type` names it |
| PostgreSQL | `int[]`, `int[][]`, `int[3]` | `array(integer)`, `array(array(integer))`, `array(integer)`: one per array bound, the bound itself dropped as PostgreSQL drops it |
| PostgreSQL | `interval day to second` | `interval('day to second')`: the fields are one identifier argument, as `format_type` prints them |
| PostgreSQL | domain, composite, enum, range | declared rows, as above; `CreateDomainStmt`, `CompositeTypeStmt` and `CreateRangeStmt` gain `schema.Apply` cases |
| PostgreSQL | `myschema.mood` | a row in namespace `myschema`; `Names` resolves to a namespace rather than a dotted name |
| MySQL | `BIGINT UNSIGNED`, `DECIMAL(10,2) UNSIGNED` | `bigint unsigned`, `decimal unsigned(10, 2)`: unsigned is a family of its own, as `COLUMN_TYPE` and the wire flag report it, rather than the alias of the signed type `types.jsonl` lists today; the converter puts it in the name instead of on `ColumnDef.IsUnsigned`, which the core ignores |
| MySQL | `TINYINT(1)`, `DATETIME(6)`, `VARCHAR(255)`, `BOOLEAN` | `tinyint(1)`, `datetime(6)`, `varchar(255)`, `tinyint(1)`: the converter's `Typmods` are read, and `boolean` canonicalizes as MySQL does |
| MySQL | `ENUM('a','b')`, `SET('x','y')` | `enum('a', 'b')`, `set('x', 'y')`: the converter renders `Vals` into the spelling |
| MySQL | `CAST(x AS CHAR(10))` | `char(10)`: the cast converter names the SQL type, not the wire code `var_string` |
| MySQL | `VARCHAR(10) CHARACTER SET binary` | `varbinary(10)`, as the parser folds it; collation is not part of the type |
| SQLite | any spelling | the spelling as an instance, `varchar(255)`, `foo bar(3)`, verbatim as `pragma_table_xinfo` reports it, with `base_oid` set by the affinity rule applied when the dialect resolves an unknown name; `types.jsonl`'s alias lists become the rule |
| SQLite | `STRICT` tables, `ANY` | the family rows; a strict table's column names one of them or fails, as SQLite does |
| SQLite | an expression | its storage class, `integer`, `real`, `text` or `blob`, which is what `typeof()` and the check report |
| ClickHouse | every parametric type | the spelling, read as today, now also for casts, `{p:T}` placeholders and results |
| ClickHouse | `Nullable(T)`, `LowCardinality(T)` | `T` with `nullable`; `lowcardinality(T)` with `base_oid` at `T` |
| ClickHouse | `Decimal32(4)`, `Enum('a', 'b')`, `Variant(String, Int64)`, `INT` | `decimal(9, 4)` by a rewrite, `enum8(a: 1, b: 2)` and `variant(int64, string)` by the converter, `int32` by an alias |
| ClickHouse | `SimpleAggregateFunction(sum, UInt64)` | `simpleaggregatefunction(sum, uint64)` with `sum` an identifier argument |
| ClickHouse | `toDecimal64(x, s)` | `decimal(18, s)`, by the seed's return template |
| ClickHouse | `Nested(a UInt8, b String)` | a relation-shape rule, not a type: the column becomes `n.a array(uint8)` and `n.b array(string)` on load, as `system.columns` has them |
| DuckDB | `STRUCT(a INTEGER, b VARCHAR)`, `MAP(K, V)`, `UNION(...)` | `struct(a: integer, b: varchar)`, `map(varchar, integer)`, `union(num: integer, str: varchar)`: the converter renders the darkwing type expression it already has instead of keeping its name |
| DuckDB | `INTEGER[]`, `INTEGER[3]` | `array(integer)` and `array(integer, 3)`: a list is the cross-dialect array, a fixed size is its second argument |
| DuckDB | `TEXT`, `VARCHAR(10)`, `NUMERIC`, `mood` | `varchar`, `varchar`, `decimal(18, 3)`, `enum('sad', 'ok')`, by aliases, argument rules and `canonical_oid` on the declared enum |
| GoogleSQL | `ARRAY<INT64>`, `STRUCT<a INT64, b STRING>`, `RANGE<DATE>` | `array(int64)`, `struct(a: int64, b: string)`, `range(date)`: the converter renders the zetajones type node in call form, or `ParseTypeExpr` accepts `<...>` |
| GoogleSQL | `STRING(10)`, `STRING(MAX)`, `NUMERIC(10,2)`, `[1, 2]`, `STRUCT(1 AS x)` | the typmods are read, with `max` an identifier argument; the array and struct constructors are typed from their elements |
| SQL Server | `NVARCHAR(MAX)`, `VARCHAR`, `DECIMAL` | `nvarchar(max)` with `max` an identifier argument; `varchar(1)` and `decimal(18, 0)` by the defaults `sys.types` applies |
| SQL Server | `FLOAT(24)` | `real`, by a rewrite |
| SQL Server | `dbo.PhoneNumber`, `sysname` | a row in namespace `dbo`, `typtype` `d`, `base_oid` at `varchar(20)`, `not_null` set; `sysname` seeded the same way over `nvarchar(128)` |
The `ident` argument is the one addition to `TypeExpr` and to
`goldeneye/analysis.TypeExpr`, which mirrors it. Without it `max`, `sum` and
`day to second` read as types, and today they do: `SimpleAggregateFunction`
reports `sum` as a type. Both parsers that have `MAX` model it as a literal
node, and the converters hand it over as an identifier; `ParseTypeExpr`
writes a bare word that is not a known family as an identifier only when a
dialect says so, since a struct field's type is also a bare word.
### What the analyzer reports
`Column` and `Parameter` in `analysis.go` keep `Type` as the expression and
`TypeOID` as the row, instance or family. `DataType` and `IsArray` stay as
the flat view for the legacy compiler bridge, and that bridge derives what
codegen reads from the expression rather than dropping it: `ArrayDims` is
the depth of `array` nesting, `Length` is the first integer argument,
`Unsigned` is a family name ending in ` unsigned`. `sqlc analyze` prints the
expression as it does today, with `ident` as a fifth argument kind.
`TypeNameString`, `ArraySuffix`, `CreateArrayType`, `TypeLength` and
`TypeScale` are the API that goes; `ResolveTypeExpr`, `LookupTypeExpr` and
`TypeExprOf(oid)` — the expression a row stands for, read back from
`sql_type_arg` — are the API that replaces it.
### What the seed files gain
`types.jsonl` is unchanged for a family beyond the optional argument
defaults; an alias becomes a row with `canonical_oid` instead of a mesh of
casts. A function's argument and return types may be expressions, and the
return type may reference an argument's value as well as its type. The
category rules in `dialect.json` apply to families; an instance inherits
its family's category, which is how `numeric(10, 2)` joins the numeric
casts without being seeded. `dialect.json` also carries the dialect's
rewrites, its identifier words and positions, and its affinity rule, so
that nothing about a dialect is code.
`goldeneye` checks the analyze cases against what each database reports, and
its answer shape is the same `TypeExpr`. ClickHouse and DuckDB report whole
expressions already. MySQL's driver reports the family, unsigned and
nullability but not precision or length, and `ColumnType.DecimalSize` and
`ColumnType.Length` can add them, with the length divided by the charset's
bytes per character since the wire reports bytes. SQLite reports a declared
spelling for a table column and a storage class for an expression, which
are an instance row and a family row respectively. A PostgreSQL check reads
`format_type(atttypid, atttypmod)` and `attndims`. Every check keeps passing
on the way, since a family with no arguments prints as it does now.
## A worked example: an array of arrays of integers
```sql
CREATE TABLE grids (
id bigint PRIMARY KEY,
cells int[][] NOT NULL
);
-- name: GetGrid :one
SELECT id, cells, cells[1][2] AS cell FROM grids WHERE cells = $1;
```
The PostgreSQL parser hands over `pg_catalog.int4` with two array bounds,
which reads into `array(array(int4))`; canonicalization makes it
`array(array(integer))`. Interning walks it bottom-up, so the inner array
gets its row before the outer one. With illustrative OIDs, `sql_type`
holds the seeded families and the two rows the schema added:
| oid | name | expr | typtype | category | family_oid | element_oid | canonical_oid |
|---|---|---|---|---|---|---|---|
| 23 | integer | integer | b | N | | | |
| 24 | int4 | int4 | b | N | | | 23 |
| 20 | bigint | bigint | b | N | | | |
| 100 | array | array | b | A | | | |
| 1001 | array | array(integer) | b | A | 100 | 23 | |
| 1002 | array | array(array(integer)) | b | A | 100 | 1001 | |
Row 1001 is what PostgreSQL calls `_int4`; row 1002 is the one PostgreSQL
does not have, since its own type collapses the dimensions onto `attndims`.
An instance takes its family's namespace. `sql_type_arg` has one row per
argument position:
| type_oid | ord | label | arg_type_oid | nullable |
|---|---|---|---|---|
| 1001 | 1 | | 23 | 0 |
| 1002 | 1 | | 1001 | 0 |
and `sql_attribute` points `grids.cells` at row 1002, `not_null` set, with
`int[][]` in `decl_type`.
Analysis resolves `cells` to that attribute, so its `exprType` is
`{typeOID: 1002, expr: array(array(integer)), nullable: false}`, and the
parameter compared with it takes the same type. `cells[1][2]` follows the
dialect's subscript rule — PostgreSQL yields the innermost element however
many subscripts are applied — which is a walk down `element_oid` from 1002
to 1001 to 23, nullable because a subscript can miss. `sqlc analyze` prints:
```json
[
{
"name": "GetGrid",
"cmd": ":one",
"columns": [
{ "name": "id", "type": { "name": "bigint" }, "table": "grids" },
{
"name": "cells",
"type": {
"name": "array",
"args": [
{ "type": { "name": "array", "args": [ { "type": { "name": "integer" } } ] } }
]
},
"table": "grids"
},
{ "name": "cell", "type": { "name": "integer", "nullable": true } }
],
"params": [
{
"number": 1,
"column": {
"name": "cells",
"type": {
"name": "array",
"args": [
{ "type": { "name": "array", "args": [ { "type": { "name": "integer" } } ] } }
]
},
"table": "grids"
}
}
]
}
]
```
Its string form is `array(array(integer))`; today the same column prints
`array(int4)`, one level, with `type_oid` pointing at a row named `int4[]`.
The legacy bridge derives `DataType` `integer` and `ArrayDims` 2 from the
nesting, so Go codegen renders `[][]int32` as the legacy path does.
DuckDB's `INTEGER[][]` and ClickHouse's `Array(Array(Int32))` produce the
same rows and output, with `int32` in ClickHouse's case.
## As implemented
The design above is implemented, engine by engine, with these departures
and details settled on the way:
- What the note called hooks is data, so that an engine adds a dialect by
writing files and never by registering Go code. `dialect.json` carries
`rewrites`, an ordered list of pattern, template and optional bound —
`float($1)` to `real` where `$1 <= 24`, `decimal` to `decimal(18, 0)`,
`Decimal32($1)` to `Decimal(9, $1)`, `sysname` to `nvarchar(128)` — which
the seed loads into `sql_type_rewrite` and the catalog applies before
interning, first match winning, tried once with the name as spelled and
once with its alias resolved so that `dec` meets a rule on `decimal`;
`idents` and `ident_args`, the words and
argument positions that are identifiers rather than types, kept as dialect
flags; and `affinity`, SQLite's ordered rule for a family the seed does
not list, loaded into `sql_type_affinity` and asked when a schema
declares one. A result that depends on an argument's value is a template
in `functions.jsonl` — `"returns": "Decimal(18, $2)"` — kept on
`sql_proc.return_template` and filled in from the call's literals. What
is genuinely about parsing stays in the engine's converter: ClickHouse
numbers an Enum's members and sorts a Variant's in the canonical
rendering it hands the core, while the spelling the formatter prints stays
the author's.
- A bare type name resolves in the default namespaces only — the catalog's
own, `pg_catalog` and the dialect's default schema — and `CREATE TYPE`
deduplicates within the namespace it names, so `foo.mood` and `mood` are
two types and a bare `mood` never binds to `foo.mood`. A bare `CREATE
TYPE` lands in the dialect's default schema when it has one, so SQL
Server's `PhoneNumber` and `dbo.PhoneNumber` are one row. A type outside
the default namespaces is spelled with its namespace wherever the catalog
spells it, including inside an instance's key, so `array(foo.mood)` and
`array(mood)` are two rows.
- A lookup that may not write — the analyzer's, against the cached catalog
— still canonicalizes an expression whose instance is not a row, however
deep the missing instance sits, so `$1::varchar(10)[]` reports
`array(character varying(10))` against the `array` family. The seed
gives every dialect the `array` family for that reason, whether or not
its `types.jsonl` lists it.
- A family in `types.jsonl` may name a `base` listed before it, which is
how MySQL's unsigned families stand on their signed ones: `bigint
unsigned` is a type of its own, and resolves as a `bigint` where nothing
takes it as itself.
- SQLite's `dialect.json` says `"alias": "base"`, which makes each alias in
its `types.jsonl` a type of its own standing on the type it aliases,
rather than another spelling of it.
- An engine hands the core one of three things: a canonical rendering
(`TypeName.Canonical`, a call expression with fields labelled `a: integer`,
which DuckDB, GoogleSQL and ClickHouse write and the formatter never
prints), the author's spelling (`TypeName.Spelling`, which the formatter
prints back and SQLite hands over as its type), or a name with `Typmods`
and `ArrayBounds`, where an integer constant is an integer argument, a
bare `ast.String` is an identifier and a quoted constant a string.
`ParseTypeExpr` reads a label before a colon, a label before a space only
when a single word follows, and words after a closing parenthesis as part
of the name, as in `decimal(10,2) unsigned`. `ColumnDef.IsUnsigned` and `ColumnDef.Vals` add MySQL's unsigned
and enum members. `ParamRef.Name` carries the name a `{name:Type}`
placeholder gives itself.
- A cast is NULL when its operand is, or when its type says so, as
`Nullable(String)` does; a cast of a placeholder types the placeholder
and takes its name and source from what it is compared with. A cast to
`interval day to second` decodes the field mask the parser reports the
way a column definition does.
- ClickHouse's `LowCardinality(Nullable(T))`, the only order it accepts,
is a nullable column, and the converter and `goldeneye` both read it as
`lowcardinality(T)` with the nullability on the outside, where the
column's nullability lives.
- MySQL types `CAST(x AS CHAR(10))` as `varchar(10)` and `CAST(x AS
BINARY(8))` as `varbinary(8)`, which is what its metadata and a view over
the cast both report, rather than the `char(10)` the table above
proposed. `goldeneye` reads a table column's type from `COLUMN_TYPE`, in
the relations seed and in the analyze check, and compares an expression
by family alone, since the wire carries no arguments.
- SQLite reports a cast to a spelling that is not a storage class, such as
`DECIMAL(5,2)`, as that spelling, while the value's storage class is what
a run would show; the check's cases keep to storage classes.
- DuckDB's `JSON` is grouped under `varchar` by `duckdb_types()`'s logical
type, so a JSON column reports `varchar`; a named enum reports its name,
not its labels, since the canonicalizer cannot see the catalog. A
GoogleSQL array or struct constructor in a select list is still untyped.
- PostgreSQL's `relations.jsonl` spells an array column as its element with
the array flag, which `goldeneye` now writes from `typelem` for the
`_`-prefixed array types alone; `int2vector` and `oidvector` share the
array category but stay types of their own. MySQL's keeps the case of an
enum's members, which are values.
## Order of work
1. The tables and the interning entry point: `sql_type.expr`, `family_oid`,
`element_oid`, `base_oid`, `canonical_oid`, `not_null`, `sql_type_arg`,
`ResolveTypeExpr`, `LookupTypeExpr`, `TypeExprOf`, with the alias step
of canonicalization. Arrays become instances of `array`; the `[]`
convention goes. Every existing golden holds, since a bare name prints
the same.
2. The analyzer: `exprType` carries the expression; casts, constructors,
placeholders and function results report it; resolution falls back
through the pointer chain. This is where ClickHouse's casts and
parameters come right.
3. The engines, one at a time, each with an `analyze_types/<engine>` case
alongside ClickHouse's and each with its rewrites: PostgreSQL
typmods, dimensions, declared types and `format_type` names; MySQL
unsigned, typmods, members and `boolean`, plus the `var_string` leak;
DuckDB's nested types and dropped arguments; GoogleSQL's angle brackets;
SQL Server's `max`, defaults and alias types; SQLite's affinity rule.
4. The legacy bridge: `parse_core.go` derives `Unsigned`, `Length` and
`ArrayDims` from the expression, and the `experiment_coreanalyzer` cases
grow MySQL unsigned and boolean columns and a PostgreSQL two-dimensional
array, so the core path generates what the legacy path does.
5. Return templates for value-dependent results, ClickHouse first; then
MySQL's precision arithmetic once its check reads precision from the
wire.
## Open questions
- How far a return template goes. ClickHouse's arithmetic promotion and
`toDecimal64(x, 4)` are finite rules; `arrayMap(f, arr)` returns an array
of the lambda's result, which needs the lambda typed first.
- Whether DuckDB's dropped `VARCHAR(10)` length and reported anonymous enum
should be canonicalized away as DuckDB does, or kept because a user
declared them. The check decides for the former.