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
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:
82
internal/core/analysis.go
Normal file
82
internal/core/analysis.go
Normal 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"`
|
||||
}
|
||||
389
internal/core/analyzer/analyzer.go
Normal file
389
internal/core/analyzer/analyzer.go
Normal 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
|
||||
}
|
||||
110
internal/core/analyzer/bench_test.go
Normal file
110
internal/core/analyzer/bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
211
internal/core/analyzer/dml.go
Normal file
211
internal/core/analyzer/dml.go
Normal 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,
|
||||
}
|
||||
}
|
||||
1038
internal/core/analyzer/expr.go
Normal file
1038
internal/core/analyzer/expr.go
Normal file
File diff suppressed because it is too large
Load Diff
169
internal/core/analyzer/projection.go
Normal file
169
internal/core/analyzer/projection.go
Normal 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)
|
||||
}
|
||||
243
internal/core/analyzer/scope.go
Normal file
243
internal/core/analyzer/scope.go
Normal 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
287
internal/core/attribute.go
Normal 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
161
internal/core/cache.go
Normal 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
69
internal/core/cast.go
Normal 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
189
internal/core/catalog.go
Normal 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)
|
||||
}
|
||||
31
internal/core/catalogdb/db.go
Normal file
31
internal/core/catalogdb/db.go
Normal 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,
|
||||
}
|
||||
}
|
||||
142
internal/core/catalogdb/models.go
Normal file
142
internal/core/catalogdb/models.go
Normal 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
|
||||
}
|
||||
1445
internal/core/catalogdb/query.sql.go
Normal file
1445
internal/core/catalogdb/query.sql.go
Normal file
File diff suppressed because it is too large
Load Diff
6
internal/core/catalogdef/embed.go
Normal file
6
internal/core/catalogdef/embed.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package catalogdef
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed schema.sql
|
||||
var Schema string
|
||||
284
internal/core/catalogdef/query.sql
Normal file
284
internal/core/catalogdef/query.sql
Normal 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 = ?;
|
||||
227
internal/core/catalogdef/schema.sql
Normal file
227
internal/core/catalogdef/schema.sql
Normal 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
78
internal/core/class.go
Normal 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
|
||||
}
|
||||
21
internal/core/constraint.go
Normal file
21
internal/core/constraint.go
Normal 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
171
internal/core/db.go
Normal 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
210
internal/core/dialect.go
Normal 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
|
||||
}
|
||||
39
internal/core/namespace.go
Normal file
39
internal/core/namespace.go
Normal 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
68
internal/core/operator.go
Normal 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
173
internal/core/proc.go
Normal 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
361
internal/core/rewrite.go
Normal 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
|
||||
}
|
||||
570
internal/core/schema/schema.go
Normal file
570
internal/core/schema/schema.go
Normal 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
|
||||
}
|
||||
134
internal/core/seed/extension.go
Normal file
134
internal/core/seed/extension.go
Normal 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
965
internal/core/seed/seed.go
Normal 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
16
internal/core/sqlc.json
Normal 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
324
internal/core/typeexpr.go
Normal 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
150
internal/core/typename.go
Normal 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
825
internal/core/types.go
Normal 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
769
internal/core/types.md
Normal 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.
|
||||
Reference in New Issue
Block a user