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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user