Files
upstream-mirror 0f4df23290
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
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

966 lines
30 KiB
Go

// 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
}