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

304 lines
8.6 KiB
Go

// Package dialect describes the files a dialect seed is made of — the JSONL
// records under internal/engine/<engine>/dialect that give an engine its
// type system and standard library — and provides what a generator needs to
// write them and a check needs to compare them with what is committed.
//
// The record types mirror the ones in internal/core/seed, which reads the
// files. They are repeated here rather than imported so that this module
// never shares code with the analysis it checks: the files are the contract,
// and a change to their shape has to be made on both sides.
package dialect
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
)
// 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, each a smaller bundle of the same files.
const ExtensionsDir = "extensions"
// Type is a type the dialect defines. Aliases are spellings of the same type
// that a schema may use in a column definition.
type Type struct {
Name string `json:"name"`
Category string `json:"category"`
Aliases []string `json:"aliases,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"`
Kind string `json:"kind,omitempty"`
Args []Arg `json:"args,omitempty"`
Returns string `json:"returns"`
Nullable bool `json:"nullable,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"`
}
// 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 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"`
}
// Files is what a generator produces: the content of each file it writes,
// keyed by slash-separated path relative to the dialect directory, such as
// "functions.jsonl" or "extensions/hstore/types.jsonl". A generator owns
// only the files it produces; the rest of a dialect, dialect.json above all,
// is written by hand and is neither generated nor checked.
type Files map[string][]byte
// JSONL encodes records one per line, the way the committed files are
// written: encoding/json's compact form, with a newline after each record.
func JSONL[T any](records []T) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
for _, record := range records {
if err := enc.Encode(record); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// ReadTypes reads the hand-written types.jsonl of a dialect directory, for
// a generator that writes a function once per spelling of a type.
func ReadTypes(dir string) ([]Type, error) {
blob, err := os.ReadFile(filepath.Join(dir, TypesFile))
if err != nil {
return nil, err
}
var types []Type
dec := json.NewDecoder(bytes.NewReader(blob))
for dec.More() {
var t Type
if err := dec.Decode(&t); err != nil {
return nil, fmt.Errorf("%s: %w", filepath.Join(dir, TypesFile), err)
}
types = append(types, t)
}
return types, nil
}
// Dir returns the dialect directory of an engine,
// internal/engine/<engine>/dialect, found relative to this source file so
// the working directory does not matter.
func Dir(engine string) (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", errors.New("cannot locate the goldeneye source directory")
}
dir := filepath.Join(filepath.Dir(file), "..", "..", "engine", engine, "dialect")
if _, err := os.Stat(dir); err != nil {
return "", err
}
return filepath.Clean(dir), nil
}
// Names lists the files in name order, so that a run is reported and
// written the same way every time.
func (f Files) Names() []string {
names := make([]string, 0, len(f))
for name := range f {
names = append(names, name)
}
sort.Strings(names)
return names
}
// Write writes the files into dir, creating extension directories as
// needed. Files the generator did not produce are left alone.
func Write(dir string, files Files) error {
for _, name := range files.Names() {
path := filepath.Join(dir, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
if err := os.WriteFile(path, files[name], 0o644); err != nil {
return err
}
}
return nil
}
// Check compares the files with the ones committed in dir and returns a
// report of every file that differs, or "" when each matches byte for byte.
func Check(dir string, files Files) (string, error) {
var report strings.Builder
for _, name := range files.Names() {
want, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(name)))
if errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(&report, "%s: not committed\n", name)
continue
}
if err != nil {
return "", err
}
if bytes.Equal(want, files[name]) {
continue
}
fmt.Fprintf(&report, "%s (-committed +database)\n%s", name, Diff(string(want), string(files[name])))
}
return report.String(), nil
}
// Diff is a line diff of two texts, marking lines only in want with "-" and
// lines only in got with "+", showing two lines of context around each
// change and eliding the rest. The committed files run to thousands of
// lines and usually differ in a handful, so the common head and tail are
// stripped before the longest common subsequence of the middle is found.
func Diff(want, got string) string {
a := lines(want)
b := lines(got)
head := 0
for head < len(a) && head < len(b) && a[head] == b[head] {
head++
}
tail := 0
for tail < len(a)-head && tail < len(b)-head && a[len(a)-1-tail] == b[len(b)-1-tail] {
tail++
}
var edits []edit
for _, line := range a[:head] {
edits = append(edits, edit{' ', line})
}
edits = append(edits, lcsEdits(a[head:len(a)-tail], b[head:len(b)-tail])...)
for _, line := range a[len(a)-tail:] {
edits = append(edits, edit{' ', line})
}
const context = 2
show := make([]bool, len(edits))
for i, e := range edits {
if e.kind == ' ' {
continue
}
for j := max(0, i-context); j <= min(len(edits)-1, i+context); j++ {
show[j] = true
}
}
var out strings.Builder
elided := false
for i, e := range edits {
if !show[i] {
elided = true
continue
}
if elided && out.Len() > 0 {
out.WriteString("...\n")
}
elided = false
fmt.Fprintf(&out, "%c %s\n", e.kind, e.line)
}
return out.String()
}
type edit struct {
kind byte // ' ', '-' or '+'
line string
}
func lines(s string) []string {
if s == "" {
return nil
}
return strings.Split(strings.TrimSuffix(s, "\n"), "\n")
}
// lcsEdits is the edit script between a and b by longest common subsequence.
func lcsEdits(a, b []string) []edit {
lcs := make([][]int, len(a)+1)
for i := range lcs {
lcs[i] = make([]int, len(b)+1)
}
for i := len(a) - 1; i >= 0; i-- {
for j := len(b) - 1; j >= 0; j-- {
if a[i] == b[j] {
lcs[i][j] = lcs[i+1][j+1] + 1
} else {
lcs[i][j] = max(lcs[i+1][j], lcs[i][j+1])
}
}
}
var edits []edit
i, j := 0, 0
for i < len(a) && j < len(b) {
switch {
case a[i] == b[j]:
edits = append(edits, edit{' ', a[i]})
i++
j++
case lcs[i+1][j] >= lcs[i][j+1]:
edits = append(edits, edit{'-', a[i]})
i++
default:
edits = append(edits, edit{'+', b[j]})
j++
}
}
for ; i < len(a); i++ {
edits = append(edits, edit{'-', a[i]})
}
for ; j < len(b); j++ {
edits = append(edits, edit{'+', b[j]})
}
return edits
}