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
124 lines
3.9 KiB
Go
124 lines
3.9 KiB
Go
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, "]", "]]") + "]"
|
|
}
|