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

445 lines
14 KiB
Go

// Package sqlite generates the SQLite dialect seed under
// internal/engine/sqlite/dialect from sqlite3 shells built from the
// amalgamation sqlite.org publishes, run against in-memory databases that
// need no server, and from the amalgamation itself.
//
// SQLite describes its functions as far as their names, their kinds and the
// number of arguments each takes — pragma_function_list — and no further:
// it types values rather than columns or functions, so nothing in the
// database says what a function returns or what it expects. The source
// does, in its way. Every function is registered with the C functions that
// implement it, and those set their result through sqlite3_result_* and
// read their arguments through sqlite3_value_*, which is as close as SQLite
// comes to declaring a signature. So functions.jsonl is built from both:
// the shell says which functions exist, how many arguments each overload
// takes and whether it aggregates, and the amalgamation says what each
// returns and what its arguments hold. A function the shell reports that
// the source does not register, or whose implementation sets no result,
// fails generation rather than being guessed at. What neither can say —
// which scalar functions return NULL for arguments that are not — is a
// short list in signatures.go; for aggregates it is found by running each
// over no rows.
//
// Which functions a SQLite has is decided when it is compiled, so the
// dialect treats compile options the way the PostgreSQL dialect treats
// contrib extensions. functions.jsonl is what a build with the default
// options has, and each further option gets a directory under extensions/
// holding the functions a build with that option adds — found the way
// CREATE EXTENSION's additions are, by comparing the catalog with and
// without. SQLite has no catalog of types or operators, so types.jsonl and
// operators.jsonl are hand-written.
//
// The shells are built once per pinned version by Install.
package sqlite
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"os/exec"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// Engine is the name of the engine directory the dialect lives under.
const Engine = "sqlite"
// functionList is every function the connection knows, one row per
// overload: name, whether the library builds it in or the shell or an
// extension registered it, 's', 'a' or 'w' for how it is called, and how
// many arguments it takes. Fixed arities come before the variable one so
// that an exact match is found first by anything reading the file in
// order.
const functionList = `
SELECT name, builtin, type, narg
FROM pragma_function_list
ORDER BY name, narg < 0, narg`
type functionRow struct {
Name string `json:"name"`
Builtin int `json:"builtin"`
Type string `json:"type"`
NArg int `json:"narg"`
}
// key tells one overload from another.
func (r functionRow) key() string {
return r.Name + "/" + strconv.Itoa(r.NArg)
}
// Version reports the release the default shell is.
func Version(ctx context.Context, dir string) (string, error) {
out, err := exec.CommandContext(ctx, builds()[0].binary(dir), "--version").Output()
if err != nil {
return "", fmt.Errorf("sqlite3 --version: %w", err)
}
return "SQLite " + strings.TrimSpace(string(out)), nil
}
// query runs a SQL statement against an in-memory database and decodes the
// shell's JSON output into rows.
func query(ctx context.Context, binary, sql string, rows any) error {
cmd := exec.CommandContext(ctx, binary, "-json", "-bail", ":memory:", sql)
out, err := cmd.Output()
if err != nil {
var exit *exec.ExitError
if errors.As(err, &exit) {
return fmt.Errorf("sqlite3: %s: %s", err, bytes.TrimSpace(exit.Stderr))
}
return fmt.Errorf("sqlite3: %w", err)
}
// An empty result set prints nothing rather than [].
if len(bytes.TrimSpace(out)) == 0 {
return nil
}
return json.Unmarshal(out, rows)
}
// minArgs decodes a negative argument count from pragma_function_list,
// which prints the count SQLite keeps for the function as it is: -1 for any
// number of arguments, and the two values reserved for built-ins, -3 for
// one or more and -4 for two or more — see matchQuality in the SQLite
// source. No function is counted as -2.
func minArgs(narg int) int {
if narg < -2 {
return -2 - narg
}
return 0
}
// functionKind maps the type the shell reports onto the seed's letters.
// The shell says 's' for a scalar function, 'a' for an aggregate and 'w'
// for one with a window implementation — but every one of SQLite's
// aggregates has one, so 'w' covers avg and count as well as row_number.
// The two are told apart by calling the function without an OVER clause,
// which only a window function refuses.
func functionKind(ctx context.Context, binary string, row functionRow) (string, error) {
switch row.Type {
case "s":
return "", nil
case "a":
return "a", nil
case "w":
window, err := isWindowFunction(ctx, binary, row)
if err != nil {
return "", err
}
if window {
return "w", nil
}
return "a", nil
}
return "", fmt.Errorf("sqlite: function %s has unknown type %q", row.Name, row.Type)
}
// isWindowFunction reports whether the shell refuses to call a function
// without an OVER clause. Any other complaint — an aggregate objecting to
// the NULLs it is handed — means the call was accepted as an aggregate.
func isWindowFunction(ctx context.Context, binary string, row functionRow) (bool, error) {
n := row.NArg
if n < 0 {
n = minArgs(n)
}
args := strings.TrimSuffix(strings.Repeat("NULL, ", n), ", ")
cmd := exec.CommandContext(ctx, binary, ":memory:", fmt.Sprintf("SELECT %s(%s)", row.Name, args))
var stderr bytes.Buffer
cmd.Stdout = io.Discard
cmd.Stderr = &stderr
err := cmd.Run()
if err == nil {
return false, nil
}
if strings.Contains(stderr.String(), "misuse of window function") {
return true, nil
}
var exit *exec.ExitError
if errors.As(err, &exit) {
return false, nil
}
return false, fmt.Errorf("sqlite3: %w", err)
}
// shell is one built sqlite3 and the functions it reports.
type shell struct {
build build
binary string
rows []functionRow
}
// readShell lists a build's functions, after checking that the shell was
// built with the options the build says — a cached shell from before the
// option lists changed would otherwise describe the wrong dialect.
func readShell(ctx context.Context, dir string, b build) (*shell, error) {
s := &shell{build: b, binary: b.binary(dir)}
if err := checkOptions(ctx, dir, b); err != nil {
return nil, err
}
if err := query(ctx, s.binary, functionList, &s.rows); err != nil {
return nil, err
}
return s, nil
}
// checkOptions asks a build's shell whether it was compiled with every
// option any build turns on, and fails when the answers do not match the
// build's own list.
func checkOptions(ctx context.Context, dir string, b build) error {
var used []struct {
Option string `json:"option"`
Used int `json:"used"`
}
known := map[string]bool{}
for _, b := range shells() {
for _, opt := range b.flags() {
known[opt] = true
}
}
var clauses []string
for _, opt := range slices.Sorted(maps.Keys(known)) {
clauses = append(clauses, fmt.Sprintf("SELECT '%s' AS option, sqlite_compileoption_used('%s') AS used", opt, opt))
}
if err := query(ctx, b.binary(dir), strings.Join(clauses, " UNION ALL "), &used); err != nil {
return err
}
for _, u := range used {
want := 0
if slices.Contains(b.flags(), u.Option) {
want = 1
}
if u.Used != want {
return fmt.Errorf("sqlite: the %s shell was not built with the options it should have been (%s is %d): remove %s and run `go run ./cmd/goldeneye install sqlite` again", b.name, u.Option, u.Used, filepath.Join(dir, b.name))
}
}
return nil
}
// generator accumulates the functions of every build, reading their
// signatures from the amalgamation, and remembers which names were
// reported so that the lists in signatures.go can be checked against the
// shells at the end.
type generator struct {
ctx context.Context
src *source
reported map[string]bool
// numeric holds every spelling types.jsonl gives integer and real, in
// its order, for the functions written once per spelling.
numeric map[string][]string
}
// overload is one row of a shell's list with what was found out about it.
type overload struct {
row functionRow
kind string
sig signature
}
// functions turns rows into records, one per overload. Every row has to be
// a function the amalgamation registers unless omitted. In the default
// build only what the library builds in counts: the rest is what the shell
// registers on top — edit, sha3, the extensions it bundles — which is not
// the dialect's business, and which a comparison with the default build has
// already removed from an option's rows.
func (g *generator) functions(s *shell, base bool) ([]dialect.Function, error) {
var overloads []overload
seen := map[string]bool{}
var missing []string
for _, row := range s.rows {
g.reported[row.Name] = true
if omitted[row.Name] || seen[row.key()] || base && row.Builtin == 0 {
continue
}
seen[row.key()] = true
sig, err := g.src.signature(row.Name)
if err != nil {
if !seen[row.Name] {
seen[row.Name] = true
missing = append(missing, err.Error())
}
continue
}
kind, err := functionKind(g.ctx, s.binary, row)
if err != nil {
return nil, err
}
overloads = append(overloads, overload{row: row, kind: kind, sig: sig})
}
if len(missing) > 0 {
return nil, fmt.Errorf("sqlite: %s build: %s", s.build.name, strings.Join(missing, "; "))
}
empty, err := overNoRows(g.ctx, s.binary, overloads)
if err != nil {
return nil, err
}
funcs := make([]dialect.Function, 0, len(overloads))
for _, o := range overloads {
isNullable := nullable[o.row.Name]
if o.kind == "a" {
isNullable = empty[o.row.key()] == "null"
}
fn := dialect.Function{
Name: o.row.Name,
Kind: o.kind,
Args: o.sig.args(o.row.NArg),
Returns: o.sig.Returns,
Nullable: isNullable,
}
funcs = append(funcs, fn)
// A function that returns an integer for an integer and a real for
// a real is written once more per spelling of each, the way
// PostgreSQL's catalog has a sum per numeric type. The overload over
// any comes first: the analysis core picks the overload whose
// parameter is the argument's type, but the legacy compiler takes
// the first of the right arity, and keeps what it had.
if o.sig.Numeric && len(fn.Args) > 0 {
for _, typ := range []string{"integer", "real"} {
for _, spelling := range g.numeric[typ] {
typed := fn
typed.Args = append([]dialect.Arg{{Type: spelling}}, fn.Args[1:]...)
typed.Returns = typ
funcs = append(funcs, typed)
}
}
}
}
return funcs, nil
}
// overNoRows runs every aggregate over no rows and reports the type of
// what each returns, "null" for the ones — avg, max, group_concat — that
// return NULL when there is nothing to aggregate, and not count or total.
// The probes go through one shell process, each statement labelled with
// its overload so that the answers can be told apart.
func overNoRows(ctx context.Context, binary string, overloads []overload) (map[string]string, error) {
var script strings.Builder
for _, o := range overloads {
if o.kind != "a" {
continue
}
n := o.row.NArg
if n < 0 {
n = minArgs(n)
}
args := strings.TrimSuffix(strings.Repeat("x, ", n), ", ")
fmt.Fprintf(&script, "SELECT '%s', typeof(%s(%s)) FROM (SELECT NULL AS x) WHERE 0;\n", o.row.key(), o.row.Name, args)
}
results := map[string]string{}
if script.Len() == 0 {
return results, nil
}
cmd := exec.CommandContext(ctx, binary, "-list", ":memory:", script.String())
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
var exit *exec.ExitError
if !errors.As(err, &exit) {
return nil, fmt.Errorf("sqlite3: %w", err)
}
}
for _, line := range strings.Split(strings.TrimSpace(stdout.String()), "\n") {
key, typ, ok := strings.Cut(line, "|")
if ok {
results[key] = typ
}
}
for _, o := range overloads {
if o.kind == "a" {
if _, ok := results[o.row.key()]; !ok {
return nil, fmt.Errorf("sqlite: cannot run %s over no rows: %s", o.row.Name, strings.TrimSpace(stderr.String()))
}
}
}
return results, nil
}
// added returns the rows of an option's shell that the default shell does
// not have: what the option adds.
func added(opt, base *shell) *shell {
have := map[string]bool{}
for _, row := range base.rows {
have[row.key()] = true
}
diff := &shell{build: opt.build, binary: opt.binary}
for _, row := range opt.rows {
if !have[row.key()] {
diff.rows = append(diff.rows, row)
}
}
return diff
}
// Generate reads the dialect from the shells under dir and the
// amalgamation they were built from.
func Generate(ctx context.Context, dir string) (dialect.Files, error) {
src, err := readSource(filepath.Join(dir, "src", "sqlite3.c"))
if err != nil {
return nil, err
}
g := &generator{ctx: ctx, src: src, reported: map[string]bool{}, numeric: map[string][]string{}}
dialectDir, err := dialect.Dir(Engine)
if err != nil {
return nil, err
}
types, err := dialect.ReadTypes(dialectDir)
if err != nil {
return nil, err
}
for _, t := range types {
if t.Name == "integer" || t.Name == "real" {
g.numeric[t.Name] = append([]string{t.Name}, t.Aliases...)
}
}
all := builds()
base, err := readShell(ctx, dir, all[0])
if err != nil {
return nil, err
}
funcs, err := g.functions(base, true)
if err != nil {
return nil, err
}
files := dialect.Files{}
if files[dialect.FunctionsFile], err = dialect.JSONL(funcs); err != nil {
return nil, err
}
for _, b := range all[1:] {
s, err := readShell(ctx, dir, b)
if err != nil {
return nil, err
}
funcs, err := g.functions(added(s, base), false)
if err != nil {
return nil, err
}
if len(funcs) == 0 {
return nil, fmt.Errorf("sqlite: %s adds no functions over the default build; drop it from the extensions in install.go", strings.Join(b.options, " "))
}
if files[path.Join(dialect.ExtensionsDir, b.name, dialect.FunctionsFile)], err = dialect.JSONL(funcs); err != nil {
return nil, err
}
}
var stale []string
for _, list := range []map[string]bool{omitted, nullable, realResults} {
for name := range list {
if !g.reported[name] {
stale = append(stale, name)
}
}
}
if len(stale) > 0 {
sort.Strings(stale)
return nil, fmt.Errorf("sqlite: signatures.go lists function(s) no shell reports: %s", strings.Join(stale, ", "))
}
return files, nil
}