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

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View File

@@ -0,0 +1,725 @@
package mssql
import (
"context"
"database/sql"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/analysis"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// The analyze cases are checked against a live server, which is asked
// three things about each query, none of which runs it. What a driver
// would see: sys.dm_exec_describe_first_result_set describes each result
// column — its name, its type spelled the way a declaration spells it,
// whether it can be NULL, and which table column it is read from. What
// each parameter would be: sp_describe_undeclared_parameters says what
// type the server would give each parameter the query leaves undeclared.
// And what each parameter stands in for: the estimated showplan, compiled
// with the parameters declared as those types, says which column each is
// compared with or assigned to, and a parameter with such a partner is
// described as that column, from the catalog of the case's database.
//
// Two things the describing function keeps to itself: a json or vector
// column is described by the nvarchar(max) it is sent to a driver as, so
// a column read from a table is typed from the catalog instead, and a
// type SQL Server calls timestamp is written as the rowversion the
// dialect names it.
// placeholder is one parameter of a query as sqlc numbers them: each ?
// in turn, and each @name or sqlc.arg name once, at its first appearance.
// Each appearance becomes a variable of its own, since the server
// describes an undeclared parameter only when it is used once.
type placeholder struct {
Number int
Name string
Vars []string // the variables its appearances became, without the @
}
var identRe = regexp.MustCompile(`^@([A-Za-z_][A-Za-z0-9_]*)`)
// bind rewrites the query so that every parameter is a variable used once,
// and lists the parameters in sqlc's order. The query's own @name
// references are kept, since that is how sqlc's SQL Server queries name
// their parameters; a sqlc.arg becomes one, and each ? in turn becomes
// one named after its position.
func bind(query string) (string, []placeholder) {
positional := 0
sql := endtoend.Rewrite(query, func(name, _ string) string {
if name == "" {
positional++
name = "p" + strconv.Itoa(positional)
}
return "@" + name
})
var (
phs []placeholder
numbers = map[string]int{}
out strings.Builder
)
i := 0
for i < len(sql) {
c := sql[i]
switch {
case c == '\'' || c == '"' || c == '[':
end := skipQuoted(sql, i)
out.WriteString(sql[i:end])
i = end
case strings.HasPrefix(sql[i:], "--"):
end := strings.IndexByte(sql[i:], '\n')
if end < 0 {
end = len(sql)
} else {
end += i
}
out.WriteString(sql[i:end])
i = end
case strings.HasPrefix(sql[i:], "/*"):
end := strings.Index(sql[i:], "*/")
if end < 0 {
end = len(sql)
} else {
end += i + 2
}
out.WriteString(sql[i:end])
i = end
case c == '@' && (i == 0 || !isWordByte(sql[i-1])) && identRe.MatchString(sql[i:]):
m := identRe.FindStringSubmatch(sql[i:])
name := m[1]
n, ok := numbers[name]
if !ok {
n = len(phs) + 1
numbers[name] = n
phs = append(phs, placeholder{Number: n, Name: name})
}
ph := &phs[n-1]
v := name
if k := len(ph.Vars); k > 0 {
v = fmt.Sprintf("%s__%d", name, k+1)
}
ph.Vars = append(ph.Vars, v)
out.WriteString("@" + v)
i += len(m[0])
default:
out.WriteByte(c)
i++
}
}
return out.String(), phs
}
func isWordByte(c byte) bool {
return c == '_' || c == '@' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
}
// skipQuoted returns the index just past the quoted token starting at i: a
// string, a double-quoted identifier or a bracketed one, whose closing
// delimiter is escaped by doubling it.
func skipQuoted(s string, i int) int {
open := s[i]
close := open
if open == '[' {
close = ']'
}
j := i + 1
for j < len(s) {
switch {
case s[j] == close && j+1 < len(s) && s[j+1] == close:
j += 2
case s[j] == close:
return j + 1
default:
j++
}
}
return len(s)
}
// splitStatements splits a script into the statements it is made of, on
// the semicolons outside strings, brackets, comments and BEGIN ... END
// blocks, and on the GO lines a T-SQL script separates batches with. A
// CREATE TYPE has to be its own batch before a table can use the type, so
// a schema is loaded one statement at a time; a trigger or procedure
// body keeps its semicolons, since a BEGIN, or a CASE, opens a block that
// its END closes.
func splitStatements(src string) []string {
var stmts []string
flush := func(s string) {
if s = strings.TrimSpace(s); s != "" && !strings.EqualFold(s, "go") {
stmts = append(stmts, s)
}
}
start := 0
i := 0
depth := 0
if isGoLine(src, 0) {
for i < len(src) && src[i] != '\n' {
i++
}
start = i
}
for i < len(src) {
c := src[i]
switch {
case c == '\'' || c == '"' || c == '[':
i = skipQuoted(src, i)
case strings.HasPrefix(src[i:], "--"):
end := strings.IndexByte(src[i:], '\n')
if end < 0 {
i = len(src)
} else {
i += end
}
case strings.HasPrefix(src[i:], "/*"):
end := strings.Index(src[i:], "*/")
if end < 0 {
i = len(src)
} else {
i += end + 2
}
case isWordByte(c):
end := i
for end < len(src) && isWordByte(src[end]) {
end++
}
switch word := strings.ToLower(src[i:end]); word {
case "begin":
// BEGIN TRAN[SACTION] and BEGIN DISTRIBUTED are statements,
// not blocks.
if !nextWordIn(src, end, "tran", "transaction", "distributed") {
depth++
}
case "case":
depth++
case "end":
if depth > 0 {
depth--
}
}
i = end
case c == ';' && depth == 0:
flush(src[start:i])
start = i + 1
i++
case c == '\n' && isGoLine(src, i+1):
flush(src[start:i])
i++
for i < len(src) && src[i] != '\n' {
i++
}
start = i
depth = 0
default:
i++
}
}
flush(src[start:])
return stmts
}
// nextWordIn reports whether the word after position i, past spaces and
// comments, is one of the words.
func nextWordIn(src string, i int, words ...string) bool {
for i < len(src) {
switch {
case src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r':
i++
case strings.HasPrefix(src[i:], "--"):
end := strings.IndexByte(src[i:], '\n')
if end < 0 {
return false
}
i += end
case strings.HasPrefix(src[i:], "/*"):
end := strings.Index(src[i:], "*/")
if end < 0 {
return false
}
i += end + 2
default:
end := i
for end < len(src) && isWordByte(src[end]) {
end++
}
for _, w := range words {
if strings.EqualFold(src[i:end], w) {
return true
}
}
return false
}
}
return false
}
// isGoLine reports whether the line starting at i is a GO batch separator.
func isGoLine(src string, i int) bool {
end := strings.IndexByte(src[i:], '\n')
if end < 0 {
end = len(src)
} else {
end += i
}
return strings.EqualFold(strings.TrimSpace(src[i:end]), "go")
}
// column is what the catalog says about a column of the case's database.
type column struct {
name string
typ *analysis.TypeExpr
nullable bool
}
// relation is a table or view of the case's database, by schema and name,
// in lower case.
type relation struct {
schema, name string
}
// analyzer holds the session a case is described in and the catalog of
// the case's database.
type analyzer struct {
conn *sql.Conn
catalog map[relation][]column
// userTypes are the alias types the schema created, by name in lower
// case, which the server reports beside the system type they stand on
// and the dialect reports by their own name.
userTypes map[string]bool
// canonical names the type the dialect reports a spelling as, for each
// alias types.jsonl lists: SQL Server spells numeric and timestamp in
// its catalog, which the dialect names decimal and rowversion.
canonical map[string]string
}
var dbNameRe = regexp.MustCompile(`[^A-Za-z0-9_]+`)
// Analyze loads a case's schema and fixture into a database of their own
// on the server, describes its queries there and returns what SQL Server
// reports in the JSON shape sqlc analyze prints.
func Analyze(ctx context.Context, dsn string, c endtoend.Case) ([]byte, error) {
conn, err := open(ctx, dsn)
if err != nil {
return nil, err
}
defer conn.Close()
if err := checkVersion(ctx, conn); err != nil {
return nil, err
}
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
}
db := quote("goldeneye_" + dbNameRe.ReplaceAllString(c.Name, "_"))
for _, stmt := range []string{
"USE master",
"DROP DATABASE IF EXISTS " + db,
"CREATE DATABASE " + db,
"USE " + db,
} {
if _, err := conn.ExecContext(ctx, stmt); err != nil {
return nil, err
}
}
defer conn.ExecContext(context.WithoutCancel(ctx), "USE master; DROP DATABASE IF EXISTS "+db)
for _, stmt := range splitStatements(string(schema)) {
if _, err := conn.ExecContext(ctx, stmt); err != nil {
return nil, fmt.Errorf("loading %s: %w", c.Schema, err)
}
}
for _, stmt := range splitStatements(string(fixture)) {
if _, err := conn.ExecContext(ctx, stmt); err != nil {
return nil, fmt.Errorf("loading %s: %w", c.Fixture, err)
}
}
a := &analyzer{conn: conn}
if a.canonical, err = readAliases(); err != nil {
return nil, err
}
if err := a.readCatalog(ctx); err != nil {
return nil, err
}
out := make([]analysis.Query, 0, len(queries))
for _, q := range queries {
aq, err := a.analyzeQuery(ctx, q)
if err != nil {
return nil, fmt.Errorf("%s: %w", q.Name, err)
}
out = append(out, aq)
}
return analysis.Encode(out)
}
// readAliases reads the aliases the hand-written types.jsonl gives each
// type, keyed by alias.
func readAliases() (map[string]string, error) {
dir, err := dialect.Dir(Engine)
if err != nil {
return nil, err
}
types, err := dialect.ReadTypes(dir)
if err != nil {
return nil, err
}
canonical := map[string]string{}
for _, t := range types {
for _, alias := range t.Aliases {
canonical[strings.ToLower(alias)] = strings.ToLower(t.Name)
}
}
return canonical, nil
}
// Check compares what SQL Server reports for a case with the output the
// case committed, returning a diff when they differ.
func Check(ctx context.Context, dsn string, c endtoend.Case) (string, error) {
got, err := Analyze(ctx, dsn, c)
if err != nil {
return "", err
}
return c.Compare(got)
}
// tablesQuery lists the tables and views of the current database, and the
// columns of each: the type as the schema declared it, whether that is a
// type the schema created, and the dimensions of a vector, which is all
// the describing function does not say.
const tablesQuery = `
SELECT s.name, o.name, c.name, t.name, t.is_user_defined, c.vector_dimensions
FROM sys.columns c
JOIN sys.objects o ON o.object_id = c.object_id
JOIN sys.schemas s ON s.schema_id = o.schema_id
JOIN sys.types t ON t.user_type_id = c.user_type_id
WHERE o.type IN ('U', 'V')
ORDER BY s.name, o.name, c.column_id`
// readCatalog reads the tables the schema created. The server spells each
// column's type by describing a SELECT * from the table, and the catalog
// says which columns that spelling is not the declared type of.
func (a *analyzer) readCatalog(ctx context.Context) error {
a.catalog = map[relation][]column{}
a.userTypes = map[string]bool{}
rows, err := a.conn.QueryContext(ctx, tablesQuery)
if err != nil {
return err
}
type declared struct {
name string
typeName string
userDefined bool
dimensions sql.NullInt64
}
decls := map[relation][]declared{}
var order []relation
for rows.Next() {
var (
schema, table, col string
d declared
)
if err := rows.Scan(&schema, &table, &col, &d.typeName, &d.userDefined, &d.dimensions); err != nil {
rows.Close()
return err
}
d.name = col
rel := relation{strings.ToLower(schema), strings.ToLower(table)}
if _, ok := decls[rel]; !ok {
order = append(order, rel)
}
decls[rel] = append(decls[rel], d)
if d.userDefined {
a.userTypes[strings.ToLower(d.typeName)] = true
}
}
rows.Close()
if err := rows.Err(); err != nil {
return err
}
for _, rel := range order {
described, err := a.describe(ctx, "SELECT * FROM "+quote(rel.schema)+"."+quote(rel.name))
if err != nil {
return fmt.Errorf("%s.%s: %w", rel.schema, rel.name, err)
}
byName := map[string]declared{}
for _, d := range decls[rel] {
byName[strings.ToLower(d.name)] = d
}
var cols []column
for _, dc := range described {
d, ok := byName[strings.ToLower(dc.name)]
if !ok {
return fmt.Errorf("%s.%s: the server describes a column %q the catalog does not list", rel.schema, rel.name, dc.name)
}
t := dc.typ
switch {
case d.userDefined:
t = &analysis.TypeExpr{Name: strings.ToLower(d.typeName)}
case strings.EqualFold(d.typeName, "json"):
t = &analysis.TypeExpr{Name: "json"}
case strings.EqualFold(d.typeName, "vector") && d.dimensions.Valid:
n := d.dimensions.Int64
t = &analysis.TypeExpr{Name: "vector", Args: []analysis.TypeArg{{Int: &n}}}
}
cols = append(cols, column{name: strings.ToLower(dc.name), typ: t, nullable: dc.nullable})
}
a.catalog[rel] = cols
}
return nil
}
// described is one result column as sys.dm_exec_describe_first_result_set
// reports it.
type described struct {
name string
typ *analysis.TypeExpr
nullable bool
source *columnRef // the table column it is read from, or nil
}
const describeColumnsQuery = `
SELECT column_ordinal, name, system_type_name, user_type_name, is_nullable,
source_schema, source_table, source_column, is_computed_column, error_message
FROM sys.dm_exec_describe_first_result_set(@p1, NULL, 1)
WHERE is_hidden = 0
ORDER BY column_ordinal`
// describe asks the server what the first result set of a statement
// looks like. A statement that returns no rows describes as no columns; one
// the server cannot compile describes as its error.
func (a *analyzer) describe(ctx context.Context, query string) ([]described, error) {
rows, err := a.conn.QueryContext(ctx, describeColumnsQuery, query)
if err != nil {
return nil, err
}
defer rows.Close()
var out []described
for rows.Next() {
var (
ordinal int
name, sysType, userType sql.NullString
nullable sql.NullBool
srcSchema, srcTable, srcColumn sql.NullString
computed sql.NullBool
errMsg sql.NullString
)
if err := rows.Scan(&ordinal, &name, &sysType, &userType, &nullable, &srcSchema, &srcTable, &srcColumn, &computed, &errMsg); err != nil {
return nil, err
}
if errMsg.Valid {
return nil, fmt.Errorf("cannot be described: %s", errMsg.String)
}
d := described{
name: name.String,
typ: a.typeOf(sysType.String, userType),
nullable: nullable.Bool,
}
// A computed column names the column it is computed from, as the
// one an update through the result set would write; a column is
// read from a table only when it is that column.
if srcTable.Valid && !computed.Bool {
d.source = &columnRef{schema: srcSchema.String, table: srcTable.String, column: srcColumn.String}
}
out = append(out, d)
}
return out, rows.Err()
}
// typeOf reads a type the server spelled, which is the system type unless
// the server also names a type the schema created, which the dialect
// reports by that name and no argument.
func (a *analyzer) typeOf(systemType string, userType sql.NullString) *analysis.TypeExpr {
if userType.Valid && a.userTypes[strings.ToLower(userType.String)] {
return &analysis.TypeExpr{Name: strings.ToLower(userType.String)}
}
return parseType(systemType, a.canonical)
}
// parseType reads a type spelled the way a declaration spells it —
// nvarchar(max), decimal(10,2), datetime2(7) — into an expression, named
// the way the dialect names it when the spelling is one of the aliases
// the dialect lists: numeric is decimal, timestamp is rowversion.
func parseType(s string, canonical map[string]string) *analysis.TypeExpr {
s = strings.TrimSpace(strings.ToLower(s))
name, args := s, ""
if open := strings.IndexByte(s, '('); open >= 0 && strings.HasSuffix(s, ")") {
name, args = strings.TrimSpace(s[:open]), s[open+1:len(s)-1]
}
if c, ok := canonical[name]; ok {
name = c
}
t := &analysis.TypeExpr{Name: name}
if args == "" {
return t
}
for _, arg := range strings.Split(args, ",") {
arg = strings.TrimSpace(arg)
if n, err := strconv.ParseInt(arg, 10, 64); err == nil {
t.Args = append(t.Args, analysis.TypeArg{Int: &n})
} else {
ident := arg
t.Args = append(t.Args, analysis.TypeArg{Ident: &ident})
}
}
return t
}
// withNullable copies a type with its nullability set.
func withNullable(t *analysis.TypeExpr, nullable bool) *analysis.TypeExpr {
if t == nil {
return nil
}
out := *t
out.Nullable = nullable
return &out
}
// lookup finds a column of the case's database in the catalog.
func (a *analyzer) lookup(ref columnRef) (column, bool) {
cols, ok := a.catalog[relation{strings.ToLower(ref.schema), strings.ToLower(ref.table)}]
if !ok {
return column{}, false
}
for _, col := range cols {
if strings.EqualFold(col.name, ref.column) {
return col, true
}
}
return column{}, false
}
// parameter is what sp_describe_undeclared_parameters says about one
// variable the statement leaves undeclared.
type parameter struct {
systemType string
userType sql.NullString
}
// undeclaredParameters asks the server what type it would give each
// parameter, keyed by the variable's name without the @.
func (a *analyzer) undeclaredParameters(ctx context.Context, query string) (map[string]parameter, error) {
rows, err := a.conn.QueryContext(ctx, "EXEC sp_describe_undeclared_parameters @p1", query)
if err != nil {
return nil, err
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return nil, err
}
index := map[string]int{}
for i, c := range cols {
index[c] = i
}
params := map[string]parameter{}
for rows.Next() {
vals := make([]any, len(cols))
ptrs := make([]any, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err != nil {
return nil, err
}
str := func(col string) sql.NullString {
i, ok := index[col]
if !ok || vals[i] == nil {
return sql.NullString{}
}
return sql.NullString{String: fmt.Sprint(vals[i]), Valid: true}
}
name := strings.TrimPrefix(str("name").String, "@")
params[name] = parameter{
systemType: str("suggested_system_type_name").String,
userType: str("suggested_user_type_name"),
}
}
return params, rows.Err()
}
// analyzeQuery describes one query.
func (a *analyzer) analyzeQuery(ctx context.Context, q endtoend.Query) (analysis.Query, error) {
sql, phs := bind(q.SQL)
aq := analysis.Query{
Name: q.Name,
Cmd: q.Cmd,
Columns: []analysis.Column{},
Params: []analysis.Param{},
}
described, err := a.describe(ctx, sql)
if err != nil {
return analysis.Query{}, err
}
for _, d := range described {
ac := analysis.Column{Name: strings.ToLower(d.name), Type: withNullable(d.typ, d.nullable)}
if d.source != nil {
if col, ok := a.lookup(*d.source); ok {
ac.Type = withNullable(col.typ, d.nullable)
}
ac.Table = strings.ToLower(d.source.table)
}
aq.Columns = append(aq.Columns, ac)
}
if len(phs) == 0 {
return aq, nil
}
params, err := a.undeclaredParameters(ctx, sql)
if err != nil {
return analysis.Query{}, err
}
var decl []string
for _, ph := range phs {
for _, v := range ph.Vars {
p, ok := params[v]
if !ok {
return analysis.Query{}, fmt.Errorf("the server did not describe parameter @%s", v)
}
decl = append(decl, "@"+v+" "+p.systemType)
}
}
partners, err := a.partners(ctx, "DECLARE "+strings.Join(decl, ", ")+";\n"+sql)
if err != nil {
return analysis.Query{}, err
}
for _, ph := range phs {
ac := analysis.Column{}
found := false
for _, v := range ph.Vars {
ref, ok := partners[v]
if !ok {
continue
}
col, ok := a.lookup(ref)
if !ok {
continue
}
ac = analysis.Column{Name: col.name, Type: withNullable(col.typ, col.nullable), Table: strings.ToLower(ref.table)}
found = true
break
}
if !found {
p := params[ph.Vars[0]]
ac.Type = a.typeOf(p.systemType, p.userType)
}
aq.Params = append(aq.Params, analysis.Param{Number: ph.Number, Column: ac})
}
return aq, nil
}

View File

@@ -0,0 +1,153 @@
// Package mssql generates the SQL Server dialect seed under
// internal/engine/mssql/dialect from a live server, and verifies the SQL
// Server analyze cases under internal/endtoend/testdata against the same
// server.
//
// SQL Server keeps no catalog of its built-in functions or operators — the
// intrinsic functions such as GETDATE and LEN are not objects — so
// types.jsonl and functions.jsonl are written by hand and are not this
// package's business. What it does describe is its catalog: relations.jsonl
// is every view of the sys and INFORMATION_SCHEMA schemas, with each view's
// columns as the server itself describes a SELECT * from it.
//
// The package also verifies the SQL Server analyze cases under
// internal/endtoend/testdata against the same server: each case's schema
// and fixture are loaded into a database of their own and its queries
// described there, without being run. Result columns come from
// sys.dm_exec_describe_first_result_set, which says what each column is
// called, what type it has, whether it can be NULL and which table column
// it is read from; parameters from sp_describe_undeclared_parameters, which
// says what type the server would give each, and from the estimated
// showplan, which says which column each is compared with or assigned to.
// The answer is printed in the JSON shape sqlc analyze prints and compared
// with the case's committed stdout.json byte for byte.
//
// The server is named by MSSQL_SERVER_URI, in any form the go-mssqldb
// driver accepts, such as
// sqlserver://sa:password@127.0.0.1:1433?encrypt=disable, and has to be
// the major release in Major, since each release adds to the catalog views.
package mssql
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"strconv"
"strings"
_ "github.com/microsoft/go-mssqldb"
"github.com/microsoft/go-mssqldb/msdsn"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// Engine is the name sqlc knows the dialect by, and the name of the analyze
// case directories under internal/endtoend/testdata.
const Engine = "mssql"
// Major is the SQL Server major release the dialect is generated from, as
// SERVERPROPERTY('ProductMajorVersion') reports it: 17 is SQL Server 2025.
// Bumping it is a deliberate change: every release adds views and columns
// to the catalog, so regenerate and review the dialect after changing it.
const Major = 17
// Locate returns the server to generate from, named by MSSQL_SERVER_URI.
func Locate() (string, error) {
dsn := os.Getenv("MSSQL_SERVER_URI")
if dsn == "" {
return "", errors.New("MSSQL_SERVER_URI is not set")
}
if _, err := msdsn.Parse(dsn); err != nil {
return "", fmt.Errorf("MSSQL_SERVER_URI: %w", err)
}
return dsn, nil
}
// open connects to the server and holds one session, since a case's
// database is the session's current database and the showplan is a
// session setting.
func open(ctx context.Context, dsn string) (*sql.Conn, error) {
db, err := sql.Open("sqlserver", dsn)
if err != nil {
return nil, err
}
conn, err := db.Conn(ctx)
if err != nil {
db.Close()
return nil, err
}
return conn, nil
}
// Version reports the release a server is.
func Version(ctx context.Context, dsn string) (string, error) {
conn, err := open(ctx, dsn)
if err != nil {
return "", err
}
defer conn.Close()
var v string
if err := conn.QueryRowContext(ctx, "SELECT @@VERSION").Scan(&v); err != nil {
return "", err
}
// The first line names the product and build; the rest is the copyright
// and the platform.
v, _, _ = strings.Cut(v, "\n")
return strings.TrimSpace(v), nil
}
// checkVersion refuses a server of another major release than the dialect
// is generated from, whose catalog would differ from the committed one
// without anything being wrong.
func checkVersion(ctx context.Context, conn *sql.Conn) error {
var v string
if err := conn.QueryRowContext(ctx, "SELECT CAST(SERVERPROPERTY('ProductMajorVersion') AS nvarchar(10))").Scan(&v); err != nil {
return err
}
major, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("ProductMajorVersion %q: %w", v, err)
}
if major != Major {
return fmt.Errorf("the dialect is generated from SQL Server major release %d, but the server is release %d", Major, major)
}
return nil
}
// Generate reads the dialect from the server: relations.jsonl, the views of
// sys and INFORMATION_SCHEMA. They are read from a database of their own,
// since the views a query sees are the ones a user database has, and
// master lists internal views of its own that no query can name.
func Generate(ctx context.Context, dsn string) (dialect.Files, error) {
conn, err := open(ctx, dsn)
if err != nil {
return nil, err
}
defer conn.Close()
if err := checkVersion(ctx, conn); err != nil {
return nil, err
}
db := quote("goldeneye_dialect")
for _, stmt := range []string{
"USE master",
"DROP DATABASE IF EXISTS " + db,
"CREATE DATABASE " + db,
"USE " + db,
} {
if _, err := conn.ExecContext(ctx, stmt); err != nil {
return nil, err
}
}
defer conn.ExecContext(context.WithoutCancel(ctx), "USE master; DROP DATABASE IF EXISTS "+db)
relations, err := readRelations(ctx, conn)
if err != nil {
return nil, err
}
blob, err := dialect.JSONL(relations)
if err != nil {
return nil, err
}
return dialect.Files{dialect.RelationsFile: blob}, nil
}

View File

@@ -0,0 +1,66 @@
package mssql
import (
"context"
"testing"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// TestDialect verifies the committed SQL Server dialect against what the
// server reports. It skips unless MSSQL_SERVER_URI names a server.
func TestDialect(t *testing.T) {
dsn, err := Locate()
if err != nil {
t.Skip(err)
}
ctx := context.Background()
version, err := Version(ctx, dsn)
if err != nil {
t.Fatal(err)
}
files, err := Generate(ctx, dsn)
if err != nil {
t.Fatal(err)
}
dir, err := dialect.Dir(Engine)
if err != nil {
t.Fatal(err)
}
report, err := dialect.Check(dir, files)
if err != nil {
t.Fatal(err)
}
if report != "" {
t.Errorf("%s does not match what %s reports:\n%s", dir, version, report)
}
}
// TestAnalyzeCases verifies every SQL Server analyze case under
// internal/endtoend/testdata against what the server reports. It skips
// unless MSSQL_SERVER_URI names a server.
func TestAnalyzeCases(t *testing.T) {
dsn, err := Locate()
if err != nil {
t.Skip(err)
}
cases, err := endtoend.Cases(Engine)
if err != nil {
t.Fatal(err)
}
if len(cases) == 0 {
t.Fatal("no mssql analyze cases found")
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
diff, err := Check(context.Background(), dsn, c)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Errorf("%s does not match what SQL Server reports (-committed +mssql):\n%s", c.Output, diff)
}
})
}
}

View File

@@ -0,0 +1,243 @@
package mssql
import (
"context"
"encoding/xml"
"fmt"
"strings"
)
// The estimated showplan is the one thing that says what a parameter is
// compared with or assigned to. It is compiled, not run, with SET
// SHOWPLAN_XML ON, and prints the plan as XML in which every column is a
// ColumnReference naming its schema, table and column, and every variable
// one naming only @variable. A parameter's partner is the column on the
// other side of the Compare it is an operand of, the column an Assign
// sets to it, or the column of a seek whose range expression it is. An
// expression the plan computes once and refers to by name, Expr1002, is
// followed to its DefinedValue. A parameter the query casts, CAST(@p AS
// T), has no partner: the cast says what it is, as it does to sqlc, and
// the plan shows it as a Convert the query asked for rather than one the
// server added.
// columnRef names a table column the plan refers to.
type columnRef struct {
schema, table, column string
}
// node is one element of the showplan XML.
type node struct {
XMLName xml.Name
Attrs []xml.Attr `xml:",any,attr"`
Children []node `xml:",any"`
}
func (n *node) attr(name string) string {
for _, a := range n.Attrs {
if a.Name.Local == name {
return a.Value
}
}
return ""
}
// unbracket strips the [brackets] the plan quotes names with.
func unbracket(s string) string {
if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
return strings.ReplaceAll(s[1:len(s)-1], "]]", "]")
}
return s
}
// plan is a parsed showplan with the expressions it defines by name.
type plan struct {
root node
defined map[string]*node // Expr1002 -> the ScalarOperator defining it
}
// partners compiles a batch and returns the column each variable is
// compared with or assigned to, keyed by the variable's name without the
// @. A variable with no such column is absent.
func (a *analyzer) partners(ctx context.Context, batch string) (map[string]columnRef, error) {
if _, err := a.conn.ExecContext(ctx, "SET SHOWPLAN_XML ON"); err != nil {
return nil, err
}
defer a.conn.ExecContext(context.WithoutCancel(ctx), "SET SHOWPLAN_XML OFF")
rows, err := a.conn.QueryContext(ctx, batch)
if err != nil {
return nil, fmt.Errorf("showplan: %w", err)
}
defer rows.Close()
var blob string
for rows.Next() {
var s string
if err := rows.Scan(&s); err != nil {
return nil, err
}
blob += s
}
if err := rows.Err(); err != nil {
return nil, err
}
p := &plan{defined: map[string]*node{}}
if err := xml.Unmarshal([]byte(blob), &p.root); err != nil {
return nil, fmt.Errorf("showplan: %w", err)
}
p.collectDefined(&p.root)
partners := map[string]columnRef{}
p.walk(&p.root, partners)
return partners, nil
}
// collectDefined records every DefinedValue that names an expression.
func (p *plan) collectDefined(n *node) {
if n.XMLName.Local == "DefinedValue" && len(n.Children) >= 2 {
ref := &n.Children[0]
if ref.XMLName.Local == "ColumnReference" && ref.attr("Table") == "" && !strings.HasPrefix(ref.attr("Column"), "@") {
p.defined[ref.attr("Column")] = &n.Children[1]
}
}
for i := range n.Children {
p.collectDefined(&n.Children[i])
}
}
// walk finds the partners in a subtree, keeping the first found for each
// variable.
func (p *plan) walk(n *node, partners map[string]columnRef) {
set := func(vars []string, ref columnRef) {
for _, v := range vars {
if _, ok := partners[v]; !ok {
partners[v] = ref
}
}
}
switch n.XMLName.Local {
case "Compare":
var sides []*node
for i := range n.Children {
if n.Children[i].XMLName.Local == "ScalarOperator" {
sides = append(sides, &n.Children[i])
}
}
if len(sides) == 2 {
for i, side := range sides {
vars := p.variables(side, nil)
if len(vars) == 0 {
continue
}
if cols := p.columns(sides[1-i], nil); len(cols) > 0 {
set(vars, cols[0])
}
}
}
case "Assign":
var target *columnRef
for i := range n.Children {
c := &n.Children[i]
switch c.XMLName.Local {
case "ColumnReference":
if ref, ok := refOf(c); ok && target == nil {
target = &ref
}
case "ScalarOperator":
if target != nil {
set(p.variables(c, nil), *target)
}
}
}
case "Prefix", "StartRange", "EndRange":
var cols []columnRef
var exprs []*node
for i := range n.Children {
c := &n.Children[i]
switch c.XMLName.Local {
case "RangeColumns":
for j := range c.Children {
if ref, ok := refOf(&c.Children[j]); ok {
cols = append(cols, ref)
}
}
case "RangeExpressions":
for j := range c.Children {
exprs = append(exprs, &c.Children[j])
}
}
}
for i, e := range exprs {
if i < len(cols) {
set(p.variables(e, nil), cols[i])
}
}
}
for i := range n.Children {
p.walk(&n.Children[i], partners)
}
}
// refOf reads a ColumnReference that names a table column.
func refOf(n *node) (columnRef, bool) {
if n.XMLName.Local != "ColumnReference" || n.attr("Table") == "" {
return columnRef{}, false
}
return columnRef{
schema: unbracket(n.attr("Schema")),
table: unbracket(n.attr("Table")),
column: n.attr("Column"),
}, true
}
// variables lists the variables a subtree refers to, following named
// expressions and leaving out the ones under a cast the query wrote.
func (p *plan) variables(n *node, seen map[string]bool) []string {
var out []string
if n.XMLName.Local == "Convert" && n.attr("Implicit") == "0" {
return nil
}
if n.XMLName.Local == "ColumnReference" {
col := n.attr("Column")
switch {
case strings.HasPrefix(col, "@"):
return []string{col[1:]}
case n.attr("Table") == "":
if def, ok := p.defined[col]; ok && !seen[col] {
if seen == nil {
seen = map[string]bool{}
}
seen[col] = true
vars := p.variables(def, seen)
delete(seen, col)
return vars
}
}
}
for i := range n.Children {
out = append(out, p.variables(&n.Children[i], seen)...)
}
return out
}
// columns lists the table columns a subtree refers to, following named
// expressions.
func (p *plan) columns(n *node, seen map[string]bool) []columnRef {
var out []columnRef
if n.XMLName.Local == "ColumnReference" {
if ref, ok := refOf(n); ok {
return []columnRef{ref}
}
col := n.attr("Column")
if def, ok := p.defined[col]; ok && !seen[col] {
if seen == nil {
seen = map[string]bool{}
}
seen[col] = true
cols := p.columns(def, seen)
delete(seen, col)
return cols
}
}
for i := range n.Children {
out = append(out, p.columns(&n.Children[i], seen)...)
}
return out
}

View File

@@ -0,0 +1,123 @@
package mssql
import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// systemSchemas are the schemas whose relations are generated: the catalog
// views, which are what a query of the server's catalog reads. Both hold
// nothing but views, so every relation a dialect seeds from them is one
// the analysis core never hands codegen as a model.
var systemSchemas = []string{
"sys",
"INFORMATION_SCHEMA",
}
// viewQuery lists a schema's views. They are sorted in Go, by the bytes
// of their lower-cased names, so that the output does not depend on the
// server's collation.
const viewQuery = `
SELECT o.name
FROM sys.all_objects o
JOIN sys.schemas s ON s.schema_id = o.schema_id
WHERE o.type = 'V' AND s.name = @p1`
// describeQuery asks the server to describe a SELECT * from a view: each
// column's name, its type spelled the way a declaration spells it —
// nvarchar(128), decimal(10,2), varbinary(max) — and whether it can be
// NULL. A view the server cannot describe reports an error message on a
// row of its own rather than failing the query.
const describeQuery = `
SELECT column_ordinal, name, system_type_name, is_nullable, error_message
FROM sys.dm_exec_describe_first_result_set(@p1, NULL, 0)
ORDER BY column_ordinal`
// readRelations reads the views of the system schemas. Their names are
// written in lower case: SQL Server matches them in any case under its
// default collations, INFORMATION_SCHEMA spells its own in upper case, and
// sqlc's SQL Server parser lowercases every identifier, so lower case is
// how a query reaches them. The type of each column is spelled the way the
// server describes it to a driver, which is the way a declaration spells
// it, numeric and timestamp included, since the seed reads the aliases
// types.jsonl lists.
func readRelations(ctx context.Context, conn *sql.Conn) ([]dialect.Relation, error) {
var relations []dialect.Relation
for _, schema := range systemSchemas {
names, err := views(ctx, conn, schema)
if err != nil {
return nil, fmt.Errorf("%s: %w", schema, err)
}
for _, name := range names {
columns, err := describe(ctx, conn, schema, name)
if err != nil {
return nil, fmt.Errorf("%s.%s: %w", schema, name, err)
}
relations = append(relations, dialect.Relation{
Schema: strings.ToLower(schema),
Name: strings.ToLower(name),
Kind: "v",
Columns: columns,
})
}
}
return relations, nil
}
func views(ctx context.Context, conn *sql.Conn, schema string) ([]string, error) {
rows, err := conn.QueryContext(ctx, viewQuery, schema)
if err != nil {
return nil, err
}
defer rows.Close()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
names = append(names, name)
}
sort.Slice(names, func(i, j int) bool { return strings.ToLower(names[i]) < strings.ToLower(names[j]) })
return names, rows.Err()
}
func describe(ctx context.Context, conn *sql.Conn, schema, name string) ([]dialect.Column, error) {
rows, err := conn.QueryContext(ctx, describeQuery, "SELECT * FROM "+quote(schema)+"."+quote(name))
if err != nil {
return nil, err
}
defer rows.Close()
var columns []dialect.Column
for rows.Next() {
var (
ordinal int
colName sql.NullString
typeName sql.NullString
nullable sql.NullBool
errMsg sql.NullString
)
if err := rows.Scan(&ordinal, &colName, &typeName, &nullable, &errMsg); err != nil {
return nil, err
}
if errMsg.Valid {
return nil, fmt.Errorf("cannot be described: %s", errMsg.String)
}
columns = append(columns, dialect.Column{
Name: strings.ToLower(colName.String),
Type: typeName.String,
NotNull: !nullable.Bool,
})
}
return columns, rows.Err()
}
// quote brackets an identifier the way T-SQL does.
func quote(name string) string {
return "[" + strings.ReplaceAll(name, "]", "]]") + "]"
}