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

270 lines
7.3 KiB
Go

package sqlite
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/analysis"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// The analyze cases are checked against the analysis shell, which is asked
// three things about each query. What the library reports about the
// prepared statement, through `.stats stmt`: each result column's name and,
// for one read straight from a table, its declared type and which table
// column it is, with the column's NOT NULL looked up in the catalog. What
// the query returns, over the case's fixture and again over no rows, for
// the columns the library has no answer for: an expression's type is the
// storage class of its value, since SQLite types values rather than
// expressions, and any column is nullable when a NULL comes back — an
// aggregate over no rows, the far side of an outer join. And the bytecode
// the statement compiles to, which is where the parameters are found,
// since the library reports nothing about them but their number.
// placeholder is one parameter of a query as sqlc numbers them: each ? in
// turn, and each sqlc.arg name once, at its first appearance. SQLite
// numbers ?NNN the same way, so the query is rewritten with those.
type placeholder struct {
Number int
Name string
}
func bind(sql string) (string, []placeholder) {
var phs []placeholder
numbers := map[string]int{}
out := endtoend.Rewrite(sql, func(name, _ string) string {
n, ok := numbers[name]
if name == "" || !ok {
n = len(phs) + 1
phs = append(phs, placeholder{Number: n, Name: name})
if name != "" {
numbers[name] = n
}
}
return fmt.Sprintf("?%d", n)
})
return out, phs
}
// Analyze runs a case's queries through the analysis shell and returns what
// SQLite reports in the JSON shape sqlc analyze prints.
func Analyze(ctx context.Context, dir string, c endtoend.Case) ([]byte, error) {
if err := checkOptions(ctx, dir, analysisShell); err != nil {
return nil, err
}
binary := analysisShell.binary(dir)
schema, err := os.ReadFile(c.Schema)
if err != nil {
return nil, err
}
var fixture []byte
if c.Fixture != "" {
if fixture, err = os.ReadFile(c.Fixture); err != nil {
return nil, err
}
}
queries, err := c.Queries()
if err != nil {
return nil, err
}
out := make([]analysis.Query, 0, len(queries))
for _, q := range queries {
aq, err := analyzeQuery(ctx, binary, string(schema), string(fixture), q)
if err != nil {
return nil, fmt.Errorf("%s: %w", q.Name, err)
}
out = append(out, aq)
}
return analysis.Encode(out)
}
// Check compares what SQLite reports for a case with the output the case
// committed, returning a diff when they differ.
func Check(ctx context.Context, dir string, c endtoend.Case) (string, error) {
got, err := Analyze(ctx, dir, c)
if err != nil {
return "", err
}
return c.Compare(got)
}
func analyzeQuery(ctx context.Context, binary, schema, fixture string, q endtoend.Query) (analysis.Query, error) {
sql, phs := bind(q.SQL)
// What the library says: the catalog, the bytecode, and the statement
// prepared, with nothing bound to its parameters.
s := newScript()
s.sql(schema)
s.sql(fixture)
s.add(".mode json")
s.add(".explain off")
for _, cq := range catalogQueries {
s.section(cq.section)
s.sql(cq.sql)
}
s.section("explain")
s.sql("EXPLAIN " + sql)
s.section("query")
s.add(".stats stmt")
line := s.sql(sql)
s.section("end")
out, err := run(ctx, binary, s)
if err != nil {
return analysis.Query{}, err
}
if errs := out.errorsBefore(line); len(errs) > 0 {
return analysis.Query{}, errors.New(strings.Join(errs, "\n"))
}
query := out.sections["query"]
if query == nil || !query.prepared {
msg := strings.TrimSpace(out.stderr)
if msg == "" {
msg = "the statement was not prepared"
}
return analysis.Query{}, errors.New(msg)
}
cat, err := readCatalog(out)
if err != nil {
return analysis.Query{}, err
}
var prog []instr
if explain := out.sections["explain"]; explain != nil && len(explain.blocks) > 0 {
if err := explain.decode(0, &prog); err != nil {
return analysis.Query{}, fmt.Errorf("reading the bytecode: %w", err)
}
}
var names []string
for _, m := range query.columns {
names = append(names, m.Name)
}
t := newTracer(cat, names)
t.run(prog)
params := make([]analysis.Column, len(phs))
for i, ph := range phs {
params[i] = t.param(ph.Number)
}
// What the statement returns: over the fixture, with each parameter
// bound to a value of the column it stands in for, so that the rows
// come through the WHERE clause, and over no rows.
s = newScript()
s.sql(schema)
s.sql(fixture)
s.add(".parameter init")
for i, ph := range phs {
if v := sample(params[i]); v != "" {
s.sql(fmt.Sprintf("REPLACE INTO temp.sqlite_parameters(key, value) VALUES ('?%d', %s)", ph.Number, v))
}
}
s.add(".mode quote")
s.section("query")
s.sql(sql)
s.section("end")
bound, err := run(ctx, binary, s)
if err != nil {
return analysis.Query{}, err
}
s = newScript()
s.sql(schema)
s.add(".mode quote")
s.section("query")
s.sql(sql)
s.section("end")
empty, err := run(ctx, binary, s)
if err != nil {
return analysis.Query{}, err
}
var rows []string
for _, o := range []*output{bound, empty} {
if q := o.sections["query"]; q != nil {
rows = append(rows, q.rows...)
}
}
aq := analysis.Query{
Name: q.Name,
Cmd: q.Cmd,
Columns: []analysis.Column{},
Params: []analysis.Param{},
}
for i, m := range query.columns {
aq.Columns = append(aq.Columns, describeColumn(cat, m, classes(rows, i)))
}
for i, ph := range phs {
ac := params[i]
if ph.Name != "" {
ac.Name = ph.Name
}
aq.Params = append(aq.Params, analysis.Param{Number: ph.Number, Column: ac})
}
return aq, nil
}
// sample is an expression for a value to bind to a parameter: one of its
// column's values in the fixture, or a value of its type when all that is
// known is the type. Empty when nothing is known.
func sample(ac analysis.Column) string {
if ac.Table != "" && ac.Name != "" {
col, tbl := quoteIdent(ac.Name), quoteIdent(ac.Table)
return fmt.Sprintf("(SELECT %s FROM %s WHERE %s IS NOT NULL LIMIT 1)", col, tbl, col)
}
if ac.Type == nil {
return ""
}
switch ac.Type.Name {
case "integer", "numeric":
return "1"
case "real":
return "1.0"
case "text":
return "'a'"
case "blob":
return "x'00'"
}
return ""
}
func quoteIdent(s string) string {
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
}
// classes lists the storage classes of a column's values across rows.
func classes(rows []string, i int) []string {
var out []string
for _, row := range rows {
if c := cells(row); i < len(c) {
out = append(out, storageClass(c[i]))
}
}
return out
}
// describeColumn describes a result column: as the table column it is read
// from when it is one, otherwise by the storage class of its values, and
// nullable when any of its values was NULL.
func describeColumn(cat *catalog, m columnMeta, classes []string) analysis.Column {
ac := analysis.Column{Name: m.Name}
if col := cat.lookup(m.Table, m.Origin); col != nil {
d := col.describe()
ac.Type, ac.Table = d.Type, d.Table
} else {
for _, class := range classes {
if class != "null" {
ac.Type = &analysis.TypeExpr{Name: class}
break
}
}
}
if ac.Type != nil {
for _, class := range classes {
if class == "null" {
ac.Type.Nullable = true
}
}
}
return ac
}