diff --git a/docs/reference/macros.md b/docs/reference/macros.md index 54d44ed5c9..fb8ac2ebe8 100644 --- a/docs/reference/macros.md +++ b/docs/reference/macros.md @@ -115,5 +115,8 @@ WHERE age IN (/*SLICE:ages*/?) Since the `/*SLICE:ages*/` placeholder is dynamically replaced on a per-query basis, this macro can't be used with prepared statements. +On SQL Server the placeholder is `/*SLICE:ages*/@ages`, and each value is bound +to its own `@p` parameter, the name go-mssqldb gives an unnamed argument. + See a full example in [Passing a slice as a parameter to a query](../howto/select.md#mysql-and-sqlite). diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index e1370e48eb..62b59ba7c1 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -439,9 +439,16 @@ func (i *importer) queryImports(filename string) fileImports { return false } - // A query bound by name passes sql.Named arguments. + // A query bound by name passes sql.Named arguments, except for its + // slices, whose values it binds to numbered placeholders. for _, q := range gq { - if q.Arg.NamedArgs && !q.Arg.isEmpty() { + if !q.Arg.NamedArgs || q.Arg.isEmpty() { + continue + } + if q.Arg.HasSqlcSlices() { + std["strconv"] = struct{}{} + } + if q.Arg.hasNamedArg() { std["database/sql"] = struct{}{} } } @@ -454,6 +461,7 @@ func (i *importer) queryImports(filename string) fileImports { if sqlcSliceScan() && !sqlpkg.IsPGX() { std["strings"] = struct{}{} } + if sliceScan() && usesPqArrays(i.Engine, sqlpkg) { pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{} } diff --git a/internal/codegen/golang/query.go b/internal/codegen/golang/query.go index 6bd9b70e25..363ae6a0b0 100644 --- a/internal/codegen/golang/query.go +++ b/internal/codegen/golang/query.go @@ -157,6 +157,12 @@ func (v QueryValue) namedArg(name, expr string) string { return "sql.Named(" + strconv.Quote(name) + ", " + expr + ")" } +// NamedArgForField is the argument expr passed for a field of a query bound +// by name. +func (v QueryValue) NamedArgForField(f Field) string { + return v.namedArg(f.ParamName, v.VariableForField(f)) +} + func (v QueryValue) Params() string { if v.isEmpty() { return "" @@ -224,6 +230,20 @@ func (v QueryValue) HasSqlcSlices() bool { return false } +// hasNamedArg reports whether any parameter is passed as sql.Named, which a +// sqlc.slice() never is. +func (v QueryValue) hasNamedArg() bool { + if v.Struct == nil { + return !v.HasSqlcSlices() + } + for _, f := range v.Struct.Fields { + if !f.HasSqlcSlice() { + return true + } + } + return false +} + func (v QueryValue) Scan() string { var out []string if v.Struct == nil { diff --git a/internal/codegen/golang/templates/stdlib/queryCode.tmpl b/internal/codegen/golang/templates/stdlib/queryCode.tmpl index 8fd67f1567..827fc8c951 100644 --- a/internal/codegen/golang/templates/stdlib/queryCode.tmpl +++ b/internal/codegen/golang/templates/stdlib/queryCode.tmpl @@ -131,7 +131,20 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{- if .Arg.Struct }} {{- $arg := .Arg }} {{- range .Arg.Struct.Fields }} - {{- if .HasSqlcSlice }} + {{- if and .HasSqlcSlice $arg.NamedArgs }} + {{- /* The driver names an unnamed argument @p, its + 1-based position in queryParams. */}} + if len({{$arg.VariableForField .}}) > 0 { + var placeholders []string + for _, v := range {{$arg.VariableForField .}} { + queryParams = append(queryParams, v) + placeholders = append(placeholders, "@p"+strconv.Itoa(len(queryParams))) + } + query = strings.ReplaceAll(query, "/*SLICE:{{.Column.Name}}*/@{{.Column.Name}}", strings.Join(placeholders, ",")) + } else { + query = strings.ReplaceAll(query, "/*SLICE:{{.Column.Name}}*/@{{.Column.Name}}", "NULL") + } + {{- else if .HasSqlcSlice }} if len({{$arg.VariableForField .}}) > 0 { for _, v := range {{$arg.VariableForField .}} { queryParams = append(queryParams, v) @@ -140,6 +153,8 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} } else { query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", "NULL", 1) } + {{- else if $arg.NamedArgs }} + queryParams = append(queryParams, {{$arg.NamedArgForField .}}) {{- else }} queryParams = append(queryParams, {{$arg.VariableForField .}}) {{- end }} @@ -149,6 +164,18 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} in a struct), because .Arg.HasSqlcSlices further up above was true, this section is 100% a slice (impossible to get here otherwise). */}} + {{- if .Arg.NamedArgs }} + if len({{.Arg.Name}}) > 0 { + var placeholders []string + for _, v := range {{.Arg.Name}} { + queryParams = append(queryParams, v) + placeholders = append(placeholders, "@p"+strconv.Itoa(len(queryParams))) + } + query = strings.ReplaceAll(query, "/*SLICE:{{.Arg.Column.Name}}*/@{{.Arg.Column.Name}}", strings.Join(placeholders, ",")) + } else { + query = strings.ReplaceAll(query, "/*SLICE:{{.Arg.Column.Name}}*/@{{.Arg.Column.Name}}", "NULL") + } + {{- else }} if len({{.Arg.Name}}) > 0 { for _, v := range {{.Arg.Name}} { queryParams = append(queryParams, v) @@ -157,6 +184,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} } else { query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", "NULL", 1) } + {{- end }} {{- end }} {{- if emitPreparedQueries }} {{ queryRetval . }} {{ queryMethod . }}(ctx, nil, query, queryParams...) diff --git a/internal/compiler/expand.go b/internal/compiler/expand.go index 8c8a490b6d..83420c9f74 100644 --- a/internal/compiler/expand.go +++ b/internal/compiler/expand.go @@ -66,6 +66,11 @@ func (c *Compiler) quoteIdent(ident string) string { return c.quote(ident) } } + // A column PIVOT names after a value, as [2023], is no regular + // identifier. + if c.conf.Engine == config.EngineMSSQL && !validPostgresIdent.MatchString(strings.ToLower(ident)) { + return c.quote(ident) + } return ident } @@ -73,6 +78,9 @@ func (c *Compiler) quote(x string) string { switch c.conf.Engine { case config.EngineMySQL: return "`" + x + "`" + case config.EngineMSSQL: + // Brackets quote whatever QUOTED_IDENTIFIER is set to. + return "[" + strings.ReplaceAll(x, "]", "]]") + "]" default: return "\"" + x + "\"" } @@ -98,9 +106,15 @@ func (c *Compiler) starOldFunc(parts []string) func(string) int { if hasSeparator := i > 0; hasSeparator { length++ } + // Use the original identifier below: o may already be quoted by + // quoteIdent, and quoting it again would measure a different name. if strings.HasPrefix(s[length:], o) { length += len(o) - } else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) { + } else if quoted := c.quote(parts[i]); strings.HasPrefix(s[length:], quoted) { + length += len(quoted) + } else if quoted := `"` + strings.ReplaceAll(parts[i], `"`, `""`) + `"`; strings.HasPrefix(s[length:], quoted) { + // SQL Server quotes with brackets, but reads double quotes + // as an identifier too. length += len(quoted) } else { length += len(o) diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index 2e1dfc7042..3019144521 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -64,6 +64,7 @@ func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess. for _, col := range res.Columns { cols = append(cols, coreColumn(col)) } + cols = embedColumns(raw, cols, res.Stars, pre.Embeds, c.coreCatalog) placeholders, err := placeholderNames(raw) if err != nil { return nil, err @@ -105,6 +106,84 @@ func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess. }, nil } +// embedColumns restores sqlc.embed() grouping after the preprocessor rewrites +// each macro to table.* and core analysis expands that star into columns. The +// table is the columns' source, so an embed of an alias names the table behind +// it. The struct scans every column of the table in order, so only an +// expansion that is exactly those columns is folded; any other stays flat. +func embedColumns(raw *ast.RawStmt, cols []*Column, stars []core.StarExpansion, embeds preprocess.EmbedSet, cat *core.Catalog) []*Column { + sel, ok := raw.Stmt.(*ast.SelectStmt) + if !ok || len(embeds) == 0 || sel.TargetList == nil { + return cols + } + width := map[int]int{} + for _, star := range stars { + width[star.Location] = len(star.Columns) + } + var out []*Column + i := 0 + for _, item := range sel.TargetList.Items { + res, ok := item.(*ast.ResTarget) + if !ok { + return cols + } + ref, ok := res.Val.(*ast.ColumnRef) + if !ok || !hasStarRef(ref) { + out = append(out, cols[i]) + i++ + continue + } + n, ok := width[res.Location] + if !ok || i+n > len(cols) { + return cols + } + if embed, ok := embeds.Find(ref.Location, res.Location); ok && n > 0 && isWholeTable(cat, cols[i:i+n]) { + out = append(out, &Column{Name: embed.Table.Name, EmbedTable: cols[i].Table}) + } else { + out = append(out, cols[i:i+n]...) + } + i += n + } + return out +} + +// isWholeTable reports whether cols are every column of one table, in the +// table's order, each as nullable as the table's: the optional side of an +// outer join is not, and the table's model could not hold its NULLs. +func isWholeTable(cat *core.Catalog, cols []*Column) bool { + table := cols[0].Table + if table == nil { + return false + } + schema := table.Schema + if schema == "" { + schema = "public" + } + nsOID, err := cat.NamespaceOID(schema) + if err != nil { + return false + } + classOID, err := cat.ClassOID(nsOID, table.Name) + if err != nil { + return false + } + tableCols, err := cat.ClassColumns(classOID) + if err != nil { + return false + } + i := 0 + for _, tc := range tableCols { + if tc.Hidden { + continue + } + if i >= len(cols) || cols[i].Table == nil || *cols[i].Table != *table || cols[i].OriginalName != tc.Name || cols[i].NotNull != tc.NotNull { + return false + } + i++ + } + return i == len(cols) +} + func coreColumn(c core.Column) *Column { col := &Column{ Name: c.Name, diff --git a/internal/core/analyzer/analyzer.go b/internal/core/analyzer/analyzer.go index 2bc94d7338..d8e94a6655 100644 --- a/internal/core/analyzer/analyzer.go +++ b/internal/core/analyzer/analyzer.go @@ -2,9 +2,11 @@ package analyzer import ( "fmt" + "maps" "github.com/sqlc-dev/sqlc/internal/core" "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/astutils" ) func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { @@ -65,6 +67,10 @@ type analyzer struct { // resolving guards against an alias that refers to itself. resolving map[string]bool + // returning maps the names a RETURNING list may give the statement's + // target to the target's own, while that list is analyzed. + returning map[string]string + // stars are the expansions every star in the statement asked for, shared // with the analyzers of the queries nested in it so one statement reports // all of them. @@ -82,10 +88,16 @@ func (a *analyzer) recordStar(s core.StarExpansion) { // placeholder inside the subquery is reported with the rest, and it sees this // query's scope, so a correlated reference resolves. func (a *analyzer) subquery(s *ast.SelectStmt) (*analyzer, error) { + // A set operation, or a query with no FROM, binds no relations of its + // own, so what is nested in it sees the query it is nested in. + outer := a.scope + if outer == nil || len(outer.rels) == 0 { + outer = a.outer + } sub := &analyzer{ cat: a.cat, params: a.params, - outer: a.scope, + outer: outer, ctes: a.ctes, stars: a.stars, } @@ -291,6 +303,12 @@ func (a *analyzer) bindCTEs(with *ast.WithClause) error { if with == nil { return nil } + // The analyzers of nested queries share the map they inherit, so a WITH + // clause binds its names in a copy, out of sight of the enclosing query. + a.ctes = maps.Clone(a.ctes) + if a.ctes == nil { + a.ctes = map[string]scopeRel{} + } for _, item := range listItems(with.Ctes) { cte, ok := item.(*ast.CommonTableExpr) if !ok || cte.Ctename == nil { @@ -300,20 +318,52 @@ func (a *analyzer) bindCTEs(with *ast.WithClause) error { if !ok { continue } + // A recursive CTE refers to itself in the branches after its first, + // which gives the columns, so the first is bound before the rest. + recursive := with.Recursive && sel.Larg != nil && refersTo(sel.Rarg, *cte.Ctename) + if recursive { + anchor := sel + for anchor.Larg != nil { + anchor = anchor.Larg + } + cols, err := a.subqueryColumns(anchor) + if err != nil { + return fmt.Errorf("with %s: %w", *cte.Ctename, err) + } + rel := derivedRel(*cte.Ctename, cols) + renameColumns(&rel, cte.Aliascolnames) + a.ctes[*cte.Ctename] = rel + } cols, err := a.subqueryColumns(sel) if err != nil { return fmt.Errorf("with %s: %w", *cte.Ctename, err) } rel := derivedRel(*cte.Ctename, cols) renameColumns(&rel, cte.Aliascolnames) - if a.ctes == nil { - a.ctes = map[string]scopeRel{} + // A recursive CTE's output no longer has one source column. SQL + // Server also reports all of its columns as nullable. + if recursive { + for i := range rel.cols { + rel.cols[i].AttOID = 0 + if a.cat.RecursiveCTEsNullable() { + rel.cols[i].NotNull = false + } + } } a.ctes[*cte.Ctename] = rel } return nil } +// refersTo reports whether a query reads the relation named name. +func refersTo(n ast.Node, name string) bool { + found := astutils.Search(n, func(n ast.Node) bool { + rv, ok := n.(*ast.RangeVar) + return ok && rv.Relname != nil && *rv.Relname == name && (rv.Schemaname == nil || *rv.Schemaname == "") + }) + return len(found.Items) > 0 +} + // bindAliases records the output names the target list assigns, so a later // clause can refer to a result column by name. func (a *analyzer) bindAliases(targets *ast.List) { diff --git a/internal/core/analyzer/dml.go b/internal/core/analyzer/dml.go index 597723cedd..51975dd5b9 100644 --- a/internal/core/analyzer/dml.go +++ b/internal/core/analyzer/dml.go @@ -11,6 +11,9 @@ func (a *analyzer) analyzeInsert(s *ast.InsertStmt) error { if s.Relation == nil { return fmt.Errorf("insert: missing relation") } + if err := a.bindCTEs(s.WithClause); err != nil { + return err + } rel, err := a.bindRangeVar(s.Relation) if err != nil { return err @@ -24,7 +27,7 @@ func (a *analyzer) analyzeInsert(s *ast.InsertStmt) error { if err := a.bindInsertValues(s.SelectStmt, rel, targets); err != nil { return err } - return a.projectReturning(s.ReturningList) + return a.projectReturning(s.ReturningList, rel) } func (a *analyzer) analyzeUpdate(s *ast.UpdateStmt) error { @@ -57,7 +60,7 @@ func (a *analyzer) analyzeUpdate(s *ast.UpdateStmt) error { return fmt.Errorf("where: %w", err) } } - return a.projectReturning(s.ReturningList) + return a.projectReturning(s.ReturningList, target) } func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error { @@ -75,7 +78,7 @@ func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error { return fmt.Errorf("where: %w", err) } } - return a.projectReturning(s.ReturningList) + return a.projectReturning(s.ReturningList, sc.rels[0]) } // relationScope builds the scope a DML statement operates on: the relations it @@ -142,6 +145,23 @@ func (a *analyzer) bindInsertValues(n ast.Node, rel scopeRel, targets []core.Cla // INSERT ... SELECT inserts whatever the query returns. The rows are not // the statement's result, but the query still holds placeholders. if sel.ValuesLists == nil { + // A placeholder selected as it is takes the type of the column it + // is inserted into, as one in VALUES does. A star selects any + // number of columns, so the items after one are not paired. + if sel.Op == ast.None { + for i, item := range listItems(sel.TargetList) { + rt, ok := item.(*ast.ResTarget) + if !ok || i >= len(targets) { + break + } + if cr, ok := rt.Val.(*ast.ColumnRef); ok && isStar(flattenFields(cr.Fields)) { + break + } + if p, ok := rt.Val.(*ast.ParamRef); ok { + a.inferParam(p, columnType(rel, targets[i])) + } + } + } _, err := a.subqueryColumns(sel) return err } @@ -177,7 +197,16 @@ func (a *analyzer) bindValue(rel scopeRel, target *core.ClassColumn, v ast.Node) return err } -func (a *analyzer) projectReturning(l *ast.List) error { +func (a *analyzer) projectReturning(l *ast.List, target scopeRel) error { + // A dialect's names for the target, such as SQL Server's INSERTED and + // DELETED, refer to it within the list. + if aliases := a.cat.ReturningAliases(); len(aliases) > 0 { + a.returning = map[string]string{} + for _, name := range aliases { + a.returning[name] = target.alias + } + defer func() { a.returning = nil }() + } for _, item := range listItems(l) { rt, ok := item.(*ast.ResTarget) if !ok { diff --git a/internal/core/analyzer/expr.go b/internal/core/analyzer/expr.go index 62a69b273d..14b23bcfc1 100644 --- a/internal/core/analyzer/expr.go +++ b/internal/core/analyzer/expr.go @@ -160,6 +160,9 @@ func (a *analyzer) typeColumnRef(c *ast.ColumnRef) (exprType, error) { relation = parts[0] column = parts[1] } + if target, ok := a.returning[relation]; ok { + relation = target + } rel, col, ok, err := a.resolveColumn(relation, column) if err != nil { return exprType{}, err @@ -984,6 +987,11 @@ func (a *analyzer) returnType(p core.ProcOverload, argTypes []exprType) exprType } if n, ok := argIndex(name); ok { if n < len(argTypes) { + // A placeholder passed there takes the parameter's type, and so + // does the result. + if argTypes[n].typeOID == 0 && argTypes[n].expr == nil && n < len(p.ArgTypes) && !a.isPolymorphicOID(p.ArgTypes[n]) { + return exprType{typeOID: p.ArgTypes[n]} + } return exprType{typeOID: argTypes[n].typeOID, expr: argTypes[n].expr} } return exprType{} diff --git a/internal/core/analyzer/projection.go b/internal/core/analyzer/projection.go index 17fc3cbf47..a4ac3d6be0 100644 --- a/internal/core/analyzer/projection.go +++ b/internal/core/analyzer/projection.go @@ -129,6 +129,9 @@ func (a *analyzer) emitStar(rt *ast.ResTarget, fields []string) { if len(fields) > 1 { relName = fields[0] } + if target, ok := a.returning[relName]; ok { + relName = target + } // The star is reported along with the columns it covers, so the query text // can be rewritten to name them. star := core.StarExpansion{Location: rt.Location, Fields: fields} diff --git a/internal/core/analyzer/scope.go b/internal/core/analyzer/scope.go index 218a8e0b11..b48986d383 100644 --- a/internal/core/analyzer/scope.go +++ b/internal/core/analyzer/scope.go @@ -52,10 +52,25 @@ func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error { sc.rels = append(sc.rels, rel) return nil case *ast.JoinExpr: + start := len(sc.rels) if err := a.appendFromItem(sc, v.Larg); err != nil { return err } - return a.appendFromItem(sc, v.Rarg) + mid := len(sc.rels) + if err := a.appendFromItem(sc, v.Rarg); err != nil { + return err + } + if a.cat.OuterJoinsNullable() { + switch v.Jointype { + case ast.JoinTypeLeft: + nullableRels(sc.rels[mid:]) + case ast.JoinTypeRight: + nullableRels(sc.rels[start:mid]) + case ast.JoinTypeFull: + nullableRels(sc.rels[start:]) + } + } + return nil case *ast.RangeFunction: rel, err := a.bindRangeFunction(v) if err != nil { @@ -70,6 +85,13 @@ func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error { } sc.rels = append(sc.rels, rel) return nil + case *ast.RangePivot: + rel, err := a.bindRangePivot(v) + if err != nil { + return err + } + sc.rels = append(sc.rels, rel) + return nil case *ast.List: // Some engines report a comma-separated FROM as a nested list. for _, item := range listItems(v) { @@ -83,6 +105,20 @@ func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error { } } +// nullableRels makes every column of rels nullable, as the optional side of +// an outer join is. The columns are copied: a relation's column list is the +// catalog's own. +func nullableRels(rels []scopeRel) { + for i := range rels { + cols := make([]core.ClassColumn, len(rels[i].cols)) + for j, c := range rels[i].cols { + c.NotNull = false + cols[j] = c + } + rels[i].cols = cols + } +} + // bindRangeFunction binds a function called in FROM. A set-returning function // stands in for a relation of a single column named after it. func (a *analyzer) bindRangeFunction(rf *ast.RangeFunction) (scopeRel, error) { @@ -122,9 +158,55 @@ func (a *analyzer) bindRangeFunction(rf *ast.RangeFunction) (scopeRel, error) { col.TypeOID, col.NotNull = p.ReturnTypeOID, !p.ReturnNullable } rel.cols = []core.ClassColumn{col} + if cols := a.rowColumns(overloads[0]); cols != nil { + rel.cols = cols + } + if rf.Coldeflist != nil { + rel.cols = a.coldefColumns(rf.Coldeflist, col) + } return rel, nil } +// rowColumns are the columns of a function returning rows of +// table(col type, ...), as a SQL Server table-valued function does. +func (a *analyzer) rowColumns(p core.ProcOverload) []core.ClassColumn { + row, err := a.cat.TypeExprOf(p.ReturnTypeOID) + if err != nil || row.Name != "table" || len(row.Args) == 0 { + return nil + } + var cols []core.ClassColumn + for _, arg := range row.Args { + if arg.Label == "" || arg.Type == nil { + return nil + } + t := a.lookupType(arg.Type.WithNullable(false)) + cols = append(cols, core.ClassColumn{Name: arg.Label, TypeOID: t.typeOID, Type: t.expr, NotNull: !arg.Type.Nullable}) + } + return cols +} + +// coldefColumns are the columns a function's column definition list +// declares, for a function returning rows of more than one column. A +// definition with no type is the function's own result. +func (a *analyzer) coldefColumns(defs *ast.List, result core.ClassColumn) []core.ClassColumn { + var cols []core.ClassColumn + for _, item := range listItems(defs) { + def, ok := item.(*ast.ColumnDef) + if !ok { + continue + } + if def.TypeName == nil { + col := result + col.Name = def.Colname + cols = append(cols, col) + continue + } + t := a.lookupType(core.TypeExprOfTypeName(def.TypeName)) + cols = append(cols, core.ClassColumn{Name: def.Colname, TypeOID: t.typeOID, Type: t.expr, NotNull: def.IsNotNull}) + } + return cols +} + func findFuncCall(l *ast.List) *ast.FuncCall { for _, item := range listItems(l) { switch v := item.(type) { @@ -139,6 +221,67 @@ func findFuncCall(l *ast.List) *ast.FuncCall { return nil } +// bindRangePivot binds PIVOT and UNPIVOT. The columns of the source that +// are not rotated pass through, followed by those the rotation makes, typed +// against the source. +func (a *analyzer) bindRangePivot(p *ast.RangePivot) (scopeRel, error) { + src := &scope{} + if err := a.appendFromItem(src, p.Source); err != nil { + return scopeRel{}, err + } + defer a.binding(src)() + rotated := map[string]bool{} + var made []core.ClassColumn + if p.Unpivot { + // The value column has the type the rotated columns share, and is + // nullable when any of them is. + value := core.ClassColumn{Name: p.ValueColumn, NotNull: true} + for i, item := range listItems(p.InList) { + t, err := a.typeExpr(&ast.ColumnRef{Fields: &ast.List{Items: []ast.Node{item}}}) + if err != nil { + return scopeRel{}, err + } + if i == 0 { + value.TypeOID, value.Type = t.typeOID, t.expr + } + value.NotNull = value.NotNull && !t.nullable + rotated[item.(*ast.String).Str] = true + } + name := a.lookupType(core.TypeExprOfTypeName(p.ForType)) + made = []core.ClassColumn{value, {Name: p.ForColumn, TypeOID: name.typeOID, Type: name.expr}} + } else { + // Each value in the list names a column of the aggregate over the + // rows with that value, null when there are none. + t, err := a.typeExpr(p.Aggregate) + if err != nil { + return scopeRel{}, err + } + rotated[p.ForColumn] = true + for _, arg := range listItems(p.Aggregate.Args) { + if cr, ok := arg.(*ast.ColumnRef); ok { + fields := flattenFields(cr.Fields) + rotated[fields[len(fields)-1]] = true + } + } + for _, item := range listItems(p.InList) { + made = append(made, core.ClassColumn{Name: item.(*ast.String).Str, TypeOID: t.typeOID, Type: t.expr}) + } + } + var rel scopeRel + if p.Alias != nil && p.Alias.Aliasname != nil { + rel.alias = *p.Alias.Aliasname + } + for _, r := range src.rels { + for _, c := range r.cols { + if !rotated[c.Name] { + rel.cols = append(rel.cols, c) + } + } + } + rel.cols = append(rel.cols, made...) + return rel, nil +} + // bindRangeSubselect binds a subquery used as a relation in FROM. func (a *analyzer) bindRangeSubselect(rs *ast.RangeSubselect) (scopeRel, error) { sel, ok := rs.Subquery.(*ast.SelectStmt) diff --git a/internal/core/dialect.go b/internal/core/dialect.go index e192c442c9..41eea18da9 100644 --- a/internal/core/dialect.go +++ b/internal/core/dialect.go @@ -77,6 +77,19 @@ const FlagPropagateNullable = "functions.propagate_nullable" // e.id. const FlagQualifyDuplicateColumns = "columns.qualify_duplicates" +// FlagOuterJoinNullable is set for a dialect whose outer joins fill the +// columns of an unmatched side with NULL, as SQL Server's do. ClickHouse's +// fill them with the type's default instead. +const FlagOuterJoinNullable = "joins.outer_nullable" + +// FlagRecursiveCTENullable marks a dialect whose recursive CTE columns are +// nullable regardless of the nullability of their anchor and recursive terms. +const FlagRecursiveCTENullable = "ctes.recursive_nullable" + +// FlagReturningAliases holds, comma separated, the names a RETURNING list +// may use for the statement's target. +const FlagReturningAliases = "returning.aliases" + // FlagDefaultSchema holds the schema the dialect puts an unqualified object // in, when that is not the catalog's own default: a type in it is reported // without its schema. @@ -196,6 +209,40 @@ func (c *Catalog) QualifiesDuplicateColumns() bool { return v == "true" } +// OuterJoinsNullable reports whether the columns of an outer join's +// optional side are nullable. +func (c *Catalog) OuterJoinsNullable() bool { + if c.dialectOID == 0 { + return false + } + v, _ := c.DialectFlag(c.dialectOID, FlagOuterJoinNullable) + return v == "true" +} + +// RecursiveCTEsNullable reports whether the dialect treats every column of a +// recursive CTE as nullable, regardless of its terms' nullability. +func (c *Catalog) RecursiveCTEsNullable() bool { + if c.dialectOID == 0 { + return false + } + v, _ := c.DialectFlag(c.dialectOID, FlagRecursiveCTENullable) + return v == "true" +} + +// ReturningAliases lists the names a RETURNING list may qualify a column of +// the statement's target with, as SQL Server's OUTPUT clause reads +// INSERTED.id and DELETED.id. +func (c *Catalog) ReturningAliases() []string { + if c.dialectOID == 0 { + return nil + } + v, _ := c.DialectFlag(c.dialectOID, FlagReturningAliases) + if v == "" { + return nil + } + return strings.Split(v, ",") +} + // DefaultNamespaces lists the namespaces a type is reported from without // qualification: the catalog's default, PostgreSQL's system catalog, and // the dialect's own default schema when it names one. diff --git a/internal/core/schema/schema.go b/internal/core/schema/schema.go index e96d8dbe2a..14708920da 100644 --- a/internal/core/schema/schema.go +++ b/internal/core/schema/schema.go @@ -413,6 +413,37 @@ func applyCreateDomain(cat *core.Catalog, stmt *ast.CreateDomainStmt) error { return err } +// functionQuery is the query a function returns the rows of, which a +// converter passes as the "query" option when the function's columns are +// whatever it selects, as a SQL Server inline table-valued function's are. +func functionQuery(stmt *ast.CreateFunctionStmt) ast.Node { + for _, item := range listItems(stmt.Options) { + if def, ok := item.(*ast.DefElem); ok && def.Defname != nil && *def.Defname == "query" { + return def.Arg + } + } + return nil +} + +// queryRow is the row type of a query's results, table(col type, ...). +func queryRow(cat *core.Catalog, query ast.Node) (*core.TypeExpr, error) { + res, err := analyzer.Prepare(cat, query) + if err != nil { + return nil, err + } + row := &core.TypeExpr{Name: "table"} + for _, col := range res.Columns { + t := col.Type + if t == nil { + if t, err = cat.TypeExprOf(col.TypeOID); err != nil { + return nil, err + } + } + row.Args = append(row.Args, core.TypeArg{Label: col.Name, Type: t.WithNullable(!col.NotNull)}) + } + return row, nil +} + // applyCompositeType records a composite type as a type whose arguments are // its fields, labelled by name. func applyCompositeType(cat *core.Catalog, stmt *ast.CompositeTypeStmt) error { @@ -491,7 +522,19 @@ func applyCreateFunction(cat *core.Catalog, stmt *ast.CreateFunctionStmt) error if stmt.Func == nil || stmt.Func.Name == "" || stmt.ReturnType == nil { return nil } - returnOID, err := cat.ResolveType(stmt.ReturnType) + // A Nullable(T) result is a T the function may return NULL for. + ret := core.TypeExprOfTypeName(stmt.ReturnType) + if ret == nil { + return fmt.Errorf("function %q: missing type name", stmt.Func.Name) + } + if query := functionQuery(stmt); query != nil { + row, err := queryRow(cat, query) + if err != nil { + return fmt.Errorf("function %q: %w", stmt.Func.Name, err) + } + ret = row + } + returnOID, err := cat.ResolveTypeExpr(ret.WithNullable(false)) if err != nil { return fmt.Errorf("function %q: %w", stmt.Func.Name, err) } @@ -518,10 +561,11 @@ func applyCreateFunction(cat *core.Catalog, stmt *ast.CreateFunctionStmt) error } } _, err = cat.CreateProc(core.ProcSpec{ - Name: stmt.Func.Name, - ReturnTypeOID: returnOID, - ReturnSet: stmt.ReturnType != nil && stmt.ReturnType.Setof, - Args: args, + Name: stmt.Func.Name, + ReturnTypeOID: returnOID, + ReturnSet: stmt.ReturnType != nil && stmt.ReturnType.Setof, + ReturnNullable: ret.Nullable, + Args: args, }) return err } diff --git a/internal/core/seed/seed.go b/internal/core/seed/seed.go index 064afedae8..2011128cbc 100644 --- a/internal/core/seed/seed.go +++ b/internal/core/seed/seed.go @@ -84,6 +84,19 @@ type Settings struct { // ClickHouse names the second id of a join e.id. QualifyDuplicateColumns bool `json:"qualify_duplicate_columns,omitempty"` + // OuterJoinNullable makes the columns of an outer join's optional side + // nullable, as SQL Server fills an unmatched row with NULL. + OuterJoinNullable bool `json:"outer_join_nullable,omitempty"` + + // RecursiveCTENullable makes all recursive CTE columns nullable, as + // SQL Server reports them regardless of their terms' nullability. + RecursiveCTENullable bool `json:"recursive_cte_nullable,omitempty"` + + // ReturningAliases are the names a RETURNING list may qualify a column + // of the statement's target with, as SQL Server's OUTPUT clause has + // INSERTED and DELETED. + ReturningAliases []string `json:"returning_aliases,omitempty"` + // Comparison operators are registered as (T, T) -> Bool for every type in // ComparisonCategories. Comparison []string `json:"comparison,omitempty"` @@ -660,6 +673,21 @@ func (b *builder) consts() error { return err } } + if b.settings.OuterJoinNullable { + if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagOuterJoinNullable, "true"); err != nil { + return err + } + } + if b.settings.RecursiveCTENullable { + if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagRecursiveCTENullable, "true"); err != nil { + return err + } + } + if len(b.settings.ReturningAliases) > 0 { + if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagReturningAliases, strings.Join(b.settings.ReturningAliases, ",")); err != nil { + return err + } + } // A schema declares types the seed knows nothing about — enums, domains, // arrays, a SQLite column typed whatever the author felt like. Recording // the comparison operators lets the catalog give those types the same ones. diff --git a/internal/core/types.go b/internal/core/types.go index 7c482a1f2b..99c38e2cb3 100644 --- a/internal/core/types.go +++ b/internal/core/types.go @@ -659,7 +659,11 @@ func (c *Catalog) LookupTypeExpr(t *TypeExpr) (TypeLookup, bool) { // The family is known and the instance is not a row. familyOID, _, err := c.internType(&TypeExpr{Name: canonical.Name}, refuse, false) if err != nil { - return TypeLookup{}, false + // The bare name may take default arguments, as SQL Server's + // decimal is decimal(18,0), and so name no row either. + if familyOID, err = c.TypeOID(canonical.Name); err != nil { + return TypeLookup{}, false + } } return TypeLookup{OID: familyOID, FamilyOID: familyOID, Expr: canonical}, true } diff --git a/internal/endtoend/testdata/analyze_apply/mssql/exec.json b/internal/endtoend/testdata/analyze_apply/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_apply/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_apply/mssql/query.sql b/internal/endtoend/testdata/analyze_apply/mssql/query.sql new file mode 100644 index 0000000000..5362c35b24 --- /dev/null +++ b/internal/endtoend/testdata/analyze_apply/mssql/query.sql @@ -0,0 +1,10 @@ +-- name: LatestBook :many +SELECT a.id, a.name, b.title +FROM authors a +CROSS APPLY (SELECT TOP 1 title FROM books WHERE author_id = a.id ORDER BY id DESC) b; + +-- name: TopBooks :many +SELECT a.name, b.title +FROM authors a +OUTER APPLY (SELECT TOP (@n) title FROM books WHERE author_id = a.id ORDER BY id) b +WHERE a.name = @name; diff --git a/internal/endtoend/testdata/analyze_apply/mssql/schema.sql b/internal/endtoend/testdata/analyze_apply/mssql/schema.sql new file mode 100644 index 0000000000..2a49202aaa --- /dev/null +++ b/internal/endtoend/testdata/analyze_apply/mssql/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(100) NOT NULL +); + +CREATE TABLE books ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + author_id BIGINT NOT NULL, + title NVARCHAR(200) NOT NULL +); diff --git a/internal/endtoend/testdata/analyze_apply/mssql/stdout.json b/internal/endtoend/testdata/analyze_apply/mssql/stdout.json new file mode 100644 index 0000000000..927f596197 --- /dev/null +++ b/internal/endtoend/testdata/analyze_apply/mssql/stdout.json @@ -0,0 +1,97 @@ +[ + { + "name": "LatestBook", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + }, + { + "name": "title", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 200 + } + ] + }, + "table": "books" + } + ], + "params": [] + }, + { + "name": "TopBooks", + "cmd": ":many", + "columns": [ + { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + }, + { + "name": "title", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 200 + } + ] + }, + "table": "books" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "n", + "type": { + "name": "bigint" + } + } + }, + { + "number": 2, + "column": { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_cte_insert/mssql/exec.json b/internal/endtoend/testdata/analyze_cte_insert/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_insert/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_cte_insert/mssql/query.sql b/internal/endtoend/testdata/analyze_cte_insert/mssql/query.sql new file mode 100644 index 0000000000..b2b0e55321 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_insert/mssql/query.sql @@ -0,0 +1,5 @@ +-- name: CopyFromAuthor :many +WITH a AS (SELECT id FROM authors WHERE name = @name) +INSERT INTO books (id, author_id, title) +OUTPUT INSERTED.id, INSERTED.title +SELECT CAST(@book_id AS INT), a.id, CAST(@title AS NVARCHAR(200)) FROM a; diff --git a/internal/endtoend/testdata/analyze_cte_insert/mssql/schema.sql b/internal/endtoend/testdata/analyze_cte_insert/mssql/schema.sql new file mode 100644 index 0000000000..ceb008432e --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_insert/mssql/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE books (id INT NOT NULL, author_id INT NOT NULL, title NVARCHAR(200) NOT NULL); +CREATE TABLE authors (id INT NOT NULL, name NVARCHAR(100) NOT NULL); diff --git a/internal/endtoend/testdata/analyze_cte_insert/mssql/stdout.json b/internal/endtoend/testdata/analyze_cte_insert/mssql/stdout.json new file mode 100644 index 0000000000..af61a35a49 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_insert/mssql/stdout.json @@ -0,0 +1,67 @@ +[ + { + "name": "CopyFromAuthor", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "books" + }, + { + "name": "title", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 200 + } + ] + }, + "table": "books" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + } + }, + { + "number": 2, + "column": { + "name": "book_id", + "type": { + "name": "int" + } + } + }, + { + "number": 3, + "column": { + "name": "title", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 200 + } + ] + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/exec.json b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/exec.json new file mode 100644 index 0000000000..85c14aaea3 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "postgresql", "--schema", "schema.sql", "query.sql"], + "contexts": ["core"] +} diff --git a/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/query.sql b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/query.sql new file mode 100644 index 0000000000..912234d004 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/query.sql @@ -0,0 +1,14 @@ +-- name: NestedCTEScope :one +WITH items AS ( + SELECT 1::integer AS value +), +derived AS ( + SELECT value FROM ( + WITH items AS ( + SELECT 'inner'::text AS value + ) + SELECT value FROM items + ) AS nested +) +SELECT items.value AS outer_value, derived.value AS nested_value +FROM items CROSS JOIN derived; diff --git a/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/schema.sql b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/schema.sql new file mode 100644 index 0000000000..19f5486402 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/schema.sql @@ -0,0 +1 @@ +-- This query uses only CTEs and does not require table definitions. diff --git a/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/stdout.json b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/stdout.json new file mode 100644 index 0000000000..c3585a4d38 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_nested_scope/postgresql/stdout.json @@ -0,0 +1,21 @@ +[ + { + "name": "NestedCTEScope", + "cmd": ":one", + "columns": [ + { + "name": "outer_value", + "type": { + "name": "integer" + } + }, + { + "name": "nested_value", + "type": { + "name": "text" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_cte_scope/postgresql/exec.json b/internal/endtoend/testdata/analyze_cte_scope/postgresql/exec.json new file mode 100644 index 0000000000..b102755fb6 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_scope/postgresql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "postgresql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_cte_scope/postgresql/query.sql b/internal/endtoend/testdata/analyze_cte_scope/postgresql/query.sql new file mode 100644 index 0000000000..148528dcd1 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_scope/postgresql/query.sql @@ -0,0 +1,6 @@ +-- name: InnerWithDoesNotLeak :one +WITH x AS (SELECT 1 AS n) +SELECT + (SELECT count(*) FROM (WITH authors AS (SELECT 1 AS id) SELECT id FROM authors) s) AS inner_count, + (SELECT name FROM authors LIMIT 1) AS outer_name +FROM x; diff --git a/internal/endtoend/testdata/analyze_cte_scope/postgresql/schema.sql b/internal/endtoend/testdata/analyze_cte_scope/postgresql/schema.sql new file mode 100644 index 0000000000..b1098ab8ff --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_scope/postgresql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE authors (id int PRIMARY KEY, name text NOT NULL); diff --git a/internal/endtoend/testdata/analyze_cte_scope/postgresql/stdout.json b/internal/endtoend/testdata/analyze_cte_scope/postgresql/stdout.json new file mode 100644 index 0000000000..5794ccc393 --- /dev/null +++ b/internal/endtoend/testdata/analyze_cte_scope/postgresql/stdout.json @@ -0,0 +1,23 @@ +[ + { + "name": "InnerWithDoesNotLeak", + "cmd": ":one", + "columns": [ + { + "name": "inner_count", + "type": { + "name": "bigint", + "nullable": true + } + }, + { + "name": "outer_name", + "type": { + "name": "text", + "nullable": true + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_for_json/mssql/exec.json b/internal/endtoend/testdata/analyze_for_json/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_for_json/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_for_json/mssql/query.sql b/internal/endtoend/testdata/analyze_for_json/mssql/query.sql new file mode 100644 index 0000000000..e1cb5c0698 --- /dev/null +++ b/internal/endtoend/testdata/analyze_for_json/mssql/query.sql @@ -0,0 +1,13 @@ +-- name: AuthorsJSON :many +SELECT id, name FROM authors WHERE id > @min_id FOR JSON PATH; + +-- name: AuthorsXML :many +SELECT id, name FROM authors FOR XML RAW; + +-- name: AuthorsWithBooks :many +SELECT a.id, + (SELECT b.id, b.title FROM books b WHERE b.author_id = a.id FOR JSON PATH) AS books, + (SELECT b.title + N',' FROM books b WHERE b.author_id = a.id FOR XML PATH('')) AS titles, + (SELECT b.id, b.title FROM books b WHERE b.author_id = a.id FOR XML PATH, TYPE) AS books_xml +FROM authors a +WHERE a.name = @name; diff --git a/internal/endtoend/testdata/analyze_for_json/mssql/schema.sql b/internal/endtoend/testdata/analyze_for_json/mssql/schema.sql new file mode 100644 index 0000000000..2a49202aaa --- /dev/null +++ b/internal/endtoend/testdata/analyze_for_json/mssql/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(100) NOT NULL +); + +CREATE TABLE books ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + author_id BIGINT NOT NULL, + title NVARCHAR(200) NOT NULL +); diff --git a/internal/endtoend/testdata/analyze_for_json/mssql/stdout.json b/internal/endtoend/testdata/analyze_for_json/mssql/stdout.json new file mode 100644 index 0000000000..ec333cc25c --- /dev/null +++ b/internal/endtoend/testdata/analyze_for_json/mssql/stdout.json @@ -0,0 +1,107 @@ +[ + { + "name": "AuthorsJSON", + "cmd": ":many", + "columns": [ + { + "name": "json_f52e2b61-18a1-11d1-b105-00805f49916b", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "min_id", + "type": { + "name": "bigint" + }, + "table": "authors" + } + } + ] + }, + { + "name": "AuthorsXML", + "cmd": ":many", + "columns": [ + { + "name": "xml_f52e2b61-18a1-11d1-b105-00805f49916b", + "type": { + "name": "ntext", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "AuthorsWithBooks", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "books", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "titles", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "books_xml", + "type": { + "name": "xml", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_global_variables/mssql/exec.json b/internal/endtoend/testdata/analyze_global_variables/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_global_variables/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_global_variables/mssql/query.sql b/internal/endtoend/testdata/analyze_global_variables/mssql/query.sql new file mode 100644 index 0000000000..d4f08649f4 --- /dev/null +++ b/internal/endtoend/testdata/analyze_global_variables/mssql/query.sql @@ -0,0 +1,13 @@ +-- name: SessionInfo :one +SELECT @@ROWCOUNT AS row_count, + @@IDENTITY AS last_identity, + @@SPID AS session_id, + @@VERSION AS version, + @@TRANCOUNT AS tran_count, + @@ERROR AS error, + @@SERVERNAME AS server_name, + @@DATEFIRST AS date_first, + @@DBTS AS dbts; + +-- name: TouchInTransaction :exec +UPDATE readings SET note = @note WHERE id = @id AND @@TRANCOUNT > 0; diff --git a/internal/endtoend/testdata/analyze_global_variables/mssql/schema.sql b/internal/endtoend/testdata/analyze_global_variables/mssql/schema.sql new file mode 100644 index 0000000000..ac4875448d --- /dev/null +++ b/internal/endtoend/testdata/analyze_global_variables/mssql/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE readings ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + raw VARCHAR(10) NOT NULL, + note VARCHAR(10), + amount DECIMAL(10,2) +); diff --git a/internal/endtoend/testdata/analyze_global_variables/mssql/stdout.json b/internal/endtoend/testdata/analyze_global_variables/mssql/stdout.json new file mode 100644 index 0000000000..3c1996768c --- /dev/null +++ b/internal/endtoend/testdata/analyze_global_variables/mssql/stdout.json @@ -0,0 +1,123 @@ +[ + { + "name": "SessionInfo", + "cmd": ":one", + "columns": [ + { + "name": "row_count", + "type": { + "name": "int" + } + }, + { + "name": "last_identity", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 38 + }, + { + "int": 0 + } + ] + } + }, + { + "name": "session_id", + "type": { + "name": "smallint" + } + }, + { + "name": "version", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 300 + } + ] + } + }, + { + "name": "tran_count", + "type": { + "name": "int" + } + }, + { + "name": "error", + "type": { + "name": "int" + } + }, + { + "name": "server_name", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 128 + } + ] + } + }, + { + "name": "date_first", + "type": { + "name": "tinyint", + "nullable": true + } + }, + { + "name": "dbts", + "type": { + "name": "varbinary", + "args": [ + { + "int": 8 + } + ] + } + } + ], + "params": [] + }, + { + "name": "TouchInTransaction", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "note", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + }, + "table": "readings" + } + }, + { + "number": 2, + "column": { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "readings" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_insert_select_params/mssql/exec.json b/internal/endtoend/testdata/analyze_insert_select_params/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_insert_select_params/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_insert_select_params/mssql/query.sql b/internal/endtoend/testdata/analyze_insert_select_params/mssql/query.sql new file mode 100644 index 0000000000..402d9d8f61 --- /dev/null +++ b/internal/endtoend/testdata/analyze_insert_select_params/mssql/query.sql @@ -0,0 +1,8 @@ +-- name: InsertSelected :exec +INSERT INTO t (id, name, at, n) SELECT @a, @b, @c, @d; + +-- name: CopyRow :exec +INSERT INTO t (id, name, at, n) SELECT @a, name, at, n + @inc FROM t WHERE id = @src; + +-- name: CopyWithStar :exec +INSERT INTO t (id, name, at, n) SELECT t2.*, @n FROM (SELECT id, name, at FROM t) t2; diff --git a/internal/endtoend/testdata/analyze_insert_select_params/mssql/schema.sql b/internal/endtoend/testdata/analyze_insert_select_params/mssql/schema.sql new file mode 100644 index 0000000000..53c28cb58a --- /dev/null +++ b/internal/endtoend/testdata/analyze_insert_select_params/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE t (id INT NOT NULL, name NVARCHAR(50) NULL, at DATETIME2(3) NOT NULL, n BIGINT NOT NULL); diff --git a/internal/endtoend/testdata/analyze_insert_select_params/mssql/stdout.json b/internal/endtoend/testdata/analyze_insert_select_params/mssql/stdout.json new file mode 100644 index 0000000000..46f15394ca --- /dev/null +++ b/internal/endtoend/testdata/analyze_insert_select_params/mssql/stdout.json @@ -0,0 +1,110 @@ +[ + { + "name": "InsertSelected", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "a", + "type": { + "name": "int" + }, + "table": "t" + } + }, + { + "number": 2, + "column": { + "name": "b", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 50 + } + ] + }, + "table": "t" + } + }, + { + "number": 3, + "column": { + "name": "c", + "type": { + "name": "datetime2", + "args": [ + { + "int": 3 + } + ] + }, + "table": "t" + } + }, + { + "number": 4, + "column": { + "name": "d", + "type": { + "name": "bigint" + }, + "table": "t" + } + } + ] + }, + { + "name": "CopyRow", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "a", + "type": { + "name": "int" + }, + "table": "t" + } + }, + { + "number": 2, + "column": { + "name": "inc", + "type": { + "name": "bigint" + }, + "table": "t" + } + }, + { + "number": 3, + "column": { + "name": "src", + "type": { + "name": "int" + }, + "table": "t" + } + } + ] + }, + { + "name": "CopyWithStar", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "n" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_national_literals/mssql/exec.json b/internal/endtoend/testdata/analyze_national_literals/mssql/exec.json new file mode 100644 index 0000000000..7b9f8e17cc --- /dev/null +++ b/internal/endtoend/testdata/analyze_national_literals/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base", "core"] +} diff --git a/internal/endtoend/testdata/analyze_national_literals/mssql/query.sql b/internal/endtoend/testdata/analyze_national_literals/mssql/query.sql new file mode 100644 index 0000000000..f174d191d1 --- /dev/null +++ b/internal/endtoend/testdata/analyze_national_literals/mssql/query.sql @@ -0,0 +1,2 @@ +-- name: Literals :many +SELECT N'hello' AS a1, N'' AS a2, 'hi' AS a3, a + N'xy' AS a4, N'😀' AS a5 FROM p WHERE a LIKE N'%x%' AND b = @b; diff --git a/internal/endtoend/testdata/analyze_national_literals/mssql/schema.sql b/internal/endtoend/testdata/analyze_national_literals/mssql/schema.sql new file mode 100644 index 0000000000..2008fa5e89 --- /dev/null +++ b/internal/endtoend/testdata/analyze_national_literals/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE p (a NVARCHAR(10) NOT NULL, b NVARCHAR(20) NOT NULL, c VARCHAR(5) NOT NULL, d DECIMAL(10,2) NOT NULL, e DECIMAL(5,3) NOT NULL, i INT NOT NULL); diff --git a/internal/endtoend/testdata/analyze_national_literals/mssql/stdout.json b/internal/endtoend/testdata/analyze_national_literals/mssql/stdout.json new file mode 100644 index 0000000000..d98c204f47 --- /dev/null +++ b/internal/endtoend/testdata/analyze_national_literals/mssql/stdout.json @@ -0,0 +1,70 @@ +[ + { + "name": "Literals", + "cmd": ":many", + "columns": [ + { + "name": "a1", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 5 + } + ] + } + }, + { + "name": "a2", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 1 + } + ] + } + }, + { + "name": "a3", + "type": { + "name": "varchar" + } + }, + { + "name": "a4", + "type": { + "name": "nvarchar" + } + }, + { + "name": "a5", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 2 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "b", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 20 + } + ] + }, + "table": "p" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_numeric_literals/mssql/exec.json b/internal/endtoend/testdata/analyze_numeric_literals/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_numeric_literals/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_numeric_literals/mssql/query.sql b/internal/endtoend/testdata/analyze_numeric_literals/mssql/query.sql new file mode 100644 index 0000000000..1a9d4db738 --- /dev/null +++ b/internal/endtoend/testdata/analyze_numeric_literals/mssql/query.sql @@ -0,0 +1,2 @@ +-- name: Literals :many +SELECT 1.5 AS a, 0.5 AS b, 10.00 AS c, 007.10 AS d, 2147483647 AS e, 2147483648 AS f, 12345678901 AS g, -1.5 AS h, 1e5 AS i FROM p; diff --git a/internal/endtoend/testdata/analyze_numeric_literals/mssql/schema.sql b/internal/endtoend/testdata/analyze_numeric_literals/mssql/schema.sql new file mode 100644 index 0000000000..2008fa5e89 --- /dev/null +++ b/internal/endtoend/testdata/analyze_numeric_literals/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE p (a NVARCHAR(10) NOT NULL, b NVARCHAR(20) NOT NULL, c VARCHAR(5) NOT NULL, d DECIMAL(10,2) NOT NULL, e DECIMAL(5,3) NOT NULL, i INT NOT NULL); diff --git a/internal/endtoend/testdata/analyze_numeric_literals/mssql/stdout.json b/internal/endtoend/testdata/analyze_numeric_literals/mssql/stdout.json new file mode 100644 index 0000000000..11c73eecae --- /dev/null +++ b/internal/endtoend/testdata/analyze_numeric_literals/mssql/stdout.json @@ -0,0 +1,111 @@ +[ + { + "name": "Literals", + "cmd": ":many", + "columns": [ + { + "name": "a", + "type": { + "name": "decimal", + "args": [ + { + "int": 2 + }, + { + "int": 1 + } + ] + } + }, + { + "name": "b", + "type": { + "name": "decimal", + "args": [ + { + "int": 1 + }, + { + "int": 1 + } + ] + } + }, + { + "name": "c", + "type": { + "name": "decimal", + "args": [ + { + "int": 4 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "d", + "type": { + "name": "decimal", + "args": [ + { + "int": 3 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "e", + "type": { + "name": "int" + } + }, + { + "name": "f", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 0 + } + ] + } + }, + { + "name": "g", + "type": { + "name": "decimal", + "args": [ + { + "int": 11 + }, + { + "int": 0 + } + ] + } + }, + { + "name": "h", + "type": { + "name": "decimal" + } + }, + { + "name": "i", + "type": { + "name": "float" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_paging/mssql/exec.json b/internal/endtoend/testdata/analyze_paging/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_paging/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_paging/mssql/query.sql b/internal/endtoend/testdata/analyze_paging/mssql/query.sql new file mode 100644 index 0000000000..cb73b7f734 --- /dev/null +++ b/internal/endtoend/testdata/analyze_paging/mssql/query.sql @@ -0,0 +1,5 @@ +-- name: TopAuthors :many +SELECT TOP (@n) id, name FROM authors ORDER BY id; + +-- name: PageAuthors :many +SELECT id, name FROM authors ORDER BY id OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY; diff --git a/internal/endtoend/testdata/analyze_paging/mssql/schema.sql b/internal/endtoend/testdata/analyze_paging/mssql/schema.sql new file mode 100644 index 0000000000..3629635d2e --- /dev/null +++ b/internal/endtoend/testdata/analyze_paging/mssql/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE authors ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(100) NOT NULL, + bio NVARCHAR(MAX), + royalties DECIMAL(10,2) NOT NULL, + created DATETIME2 NOT NULL +); diff --git a/internal/endtoend/testdata/analyze_paging/mssql/stdout.json b/internal/endtoend/testdata/analyze_paging/mssql/stdout.json new file mode 100644 index 0000000000..5fc257bcbb --- /dev/null +++ b/internal/endtoend/testdata/analyze_paging/mssql/stdout.json @@ -0,0 +1,83 @@ +[ + { + "name": "TopAuthors", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "n", + "type": { + "name": "bigint" + } + } + } + ] + }, + { + "name": "PageAuthors", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "skip", + "type": { + "name": "bigint" + } + } + }, + { + "number": 2, + "column": { + "name": "take", + "type": { + "name": "bigint" + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_pivot/mssql/exec.json b/internal/endtoend/testdata/analyze_pivot/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_pivot/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_pivot/mssql/query.sql b/internal/endtoend/testdata/analyze_pivot/mssql/query.sql new file mode 100644 index 0000000000..1ab8ee5bc5 --- /dev/null +++ b/internal/endtoend/testdata/analyze_pivot/mssql/query.sql @@ -0,0 +1,17 @@ +-- name: PivotDerived :many +SELECT pv.* FROM (SELECT author_id, year, amount FROM sales WHERE author_id > @min_author) s +PIVOT (SUM(amount) FOR year IN ([2023], [2024])) pv; + +-- name: PivotTable :many +SELECT * FROM sales PIVOT (COUNT(amount) FOR year IN ([2023], [2024])) AS pv; + +-- name: PivotColumns :many +SELECT pv.author_id, pv.[2023] FROM sales PIVOT (MAX(amount) FOR year IN ([2023])) pv +WHERE pv.id = @id; + +-- name: Unpivot :many +SELECT u.id, u.metric, u.value FROM stats s +UNPIVOT (value FOR metric IN (q1, q2, q3)) u; + +-- name: UnpivotStar :many +SELECT * FROM stats UNPIVOT (value FOR metric IN (q1)) AS u; diff --git a/internal/endtoend/testdata/analyze_pivot/mssql/schema.sql b/internal/endtoend/testdata/analyze_pivot/mssql/schema.sql new file mode 100644 index 0000000000..a32279545a --- /dev/null +++ b/internal/endtoend/testdata/analyze_pivot/mssql/schema.sql @@ -0,0 +1,15 @@ +CREATE TABLE sales ( + id INT NOT NULL PRIMARY KEY, + author_id INT NOT NULL, + year INT NOT NULL, + amount INT NOT NULL, + note NVARCHAR(50) NULL +); + +CREATE TABLE stats ( + id INT NOT NULL PRIMARY KEY, + name VARCHAR(20) NULL, + q1 INT NOT NULL, + q2 INT NULL, + q3 INT NULL +); diff --git a/internal/endtoend/testdata/analyze_pivot/mssql/stdout.json b/internal/endtoend/testdata/analyze_pivot/mssql/stdout.json new file mode 100644 index 0000000000..9c91bb88c8 --- /dev/null +++ b/internal/endtoend/testdata/analyze_pivot/mssql/stdout.json @@ -0,0 +1,215 @@ +[ + { + "name": "PivotDerived", + "cmd": ":many", + "columns": [ + { + "name": "author_id", + "type": { + "name": "int" + }, + "table": "sales" + }, + { + "name": "2023", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "2024", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "min_author", + "type": { + "name": "int" + }, + "table": "sales" + } + } + ] + }, + { + "name": "PivotTable", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "sales" + }, + { + "name": "author_id", + "type": { + "name": "int" + }, + "table": "sales" + }, + { + "name": "note", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 50 + } + ] + }, + "table": "sales" + }, + { + "name": "2023", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "2024", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "PivotColumns", + "cmd": ":many", + "columns": [ + { + "name": "author_id", + "type": { + "name": "int" + }, + "table": "sales" + }, + { + "name": "2023", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "type": { + "name": "int" + }, + "table": "sales" + } + } + ] + }, + { + "name": "Unpivot", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "stats" + }, + { + "name": "metric", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 128 + } + ] + } + }, + { + "name": "value", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "UnpivotStar", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "stats" + }, + { + "name": "name", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 20 + } + ] + }, + "table": "stats" + }, + { + "name": "q2", + "type": { + "name": "int", + "nullable": true + }, + "table": "stats" + }, + { + "name": "q3", + "type": { + "name": "int", + "nullable": true + }, + "table": "stats" + }, + { + "name": "value", + "type": { + "name": "int" + } + }, + { + "name": "metric", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 128 + } + ] + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_recursive_cte/mssql/exec.json b/internal/endtoend/testdata/analyze_recursive_cte/mssql/exec.json new file mode 100644 index 0000000000..7b9f8e17cc --- /dev/null +++ b/internal/endtoend/testdata/analyze_recursive_cte/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base", "core"] +} diff --git a/internal/endtoend/testdata/analyze_recursive_cte/mssql/query.sql b/internal/endtoend/testdata/analyze_recursive_cte/mssql/query.sql new file mode 100644 index 0000000000..bf18662350 --- /dev/null +++ b/internal/endtoend/testdata/analyze_recursive_cte/mssql/query.sql @@ -0,0 +1,18 @@ +-- name: Subtree :many +WITH tree AS ( + SELECT id, parent_id, name, 0 AS depth FROM nodes WHERE id = @root + UNION ALL + SELECT n.id, n.parent_id, n.name, t.depth + 1 + FROM nodes n + JOIN tree t ON n.parent_id = t.id +) +SELECT id, parent_id, name, depth FROM tree +OPTION (MAXRECURSION 100); + +-- name: Numbers :many +WITH seq(i) AS ( + SELECT 1 + UNION ALL + SELECT i + 1 FROM seq WHERE i < @max +) +SELECT i FROM seq; diff --git a/internal/endtoend/testdata/analyze_recursive_cte/mssql/schema.sql b/internal/endtoend/testdata/analyze_recursive_cte/mssql/schema.sql new file mode 100644 index 0000000000..89c256a5ef --- /dev/null +++ b/internal/endtoend/testdata/analyze_recursive_cte/mssql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE nodes ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + parent_id BIGINT, + name NVARCHAR(100) NOT NULL +); diff --git a/internal/endtoend/testdata/analyze_recursive_cte/mssql/stdout.json b/internal/endtoend/testdata/analyze_recursive_cte/mssql/stdout.json new file mode 100644 index 0000000000..f864eb2874 --- /dev/null +++ b/internal/endtoend/testdata/analyze_recursive_cte/mssql/stdout.json @@ -0,0 +1,77 @@ +[ + { + "name": "Subtree", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint", + "nullable": true + } + }, + { + "name": "parent_id", + "type": { + "name": "bigint", + "nullable": true + } + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "depth", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "root", + "type": { + "name": "bigint" + }, + "table": "nodes" + } + } + ] + }, + { + "name": "Numbers", + "cmd": ":many", + "columns": [ + { + "name": "i", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "max", + "type": { + "name": "int" + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_scalar_functions/mssql/exec.json b/internal/endtoend/testdata/analyze_scalar_functions/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_scalar_functions/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_scalar_functions/mssql/query.sql b/internal/endtoend/testdata/analyze_scalar_functions/mssql/query.sql new file mode 100644 index 0000000000..89bb6eef73 --- /dev/null +++ b/internal/endtoend/testdata/analyze_scalar_functions/mssql/query.sql @@ -0,0 +1,2 @@ +-- name: ItemTotals :many +SELECT id, dbo.f_add(id, price) AS total, dbo.f_add(@n, DEFAULT) AS d, dbo.label(@s) AS l FROM items; diff --git a/internal/endtoend/testdata/analyze_scalar_functions/mssql/schema.sql b/internal/endtoend/testdata/analyze_scalar_functions/mssql/schema.sql new file mode 100644 index 0000000000..bbb242872d --- /dev/null +++ b/internal/endtoend/testdata/analyze_scalar_functions/mssql/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE items (id INT NOT NULL, price DECIMAL(10,2) NOT NULL); +GO +CREATE FUNCTION dbo.f_add(@a INT, @b DECIMAL(10,2) = 1) RETURNS DECIMAL(12,2) +AS BEGIN RETURN @a + @b END; +GO +CREATE FUNCTION label(@s NVARCHAR(20)) RETURNS NVARCHAR(30) WITH SCHEMABINDING +AS BEGIN RETURN N'x' + @s END; +GO diff --git a/internal/endtoend/testdata/analyze_scalar_functions/mssql/stdout.json b/internal/endtoend/testdata/analyze_scalar_functions/mssql/stdout.json new file mode 100644 index 0000000000..9da8e78da8 --- /dev/null +++ b/internal/endtoend/testdata/analyze_scalar_functions/mssql/stdout.json @@ -0,0 +1,82 @@ +[ + { + "name": "ItemTotals", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "items" + }, + { + "name": "total", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "d", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "l", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 30 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "n", + "type": { + "name": "int" + } + } + }, + { + "number": 2, + "column": { + "name": "s", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 20 + } + ] + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_table_functions/mssql/exec.json b/internal/endtoend/testdata/analyze_table_functions/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_functions/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_table_functions/mssql/query.sql b/internal/endtoend/testdata/analyze_table_functions/mssql/query.sql new file mode 100644 index 0000000000..677428a56e --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_functions/mssql/query.sql @@ -0,0 +1,19 @@ +-- name: Elements :many +SELECT d.id, j.[key], j.value, j.type +FROM documents d +CROSS APPLY OPENJSON(d.body) j +WHERE d.id = @id; + +-- name: Items :many +SELECT d.id, i.name, i.qty, i.extra +FROM documents d +CROSS APPLY OPENJSON(d.body, '$.items') + WITH (name NVARCHAR(50) '$.name', qty INT '$.qty', extra NVARCHAR(MAX) '$.extra' AS JSON) i; + +-- name: Words :many +SELECT d.id, w.value, w.ordinal +FROM documents d +CROSS APPLY STRING_SPLIT(d.title, N' ', 1) w; + +-- name: Series :many +SELECT value FROM GENERATE_SERIES(1, 10); diff --git a/internal/endtoend/testdata/analyze_table_functions/mssql/schema.sql b/internal/endtoend/testdata/analyze_table_functions/mssql/schema.sql new file mode 100644 index 0000000000..f70422d2a7 --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_functions/mssql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE documents ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + title NVARCHAR(100) NOT NULL, + body NVARCHAR(MAX) +); diff --git a/internal/endtoend/testdata/analyze_table_functions/mssql/stdout.json b/internal/endtoend/testdata/analyze_table_functions/mssql/stdout.json new file mode 100644 index 0000000000..529f8710d7 --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_functions/mssql/stdout.json @@ -0,0 +1,145 @@ +[ + { + "name": "Elements", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "documents" + }, + { + "name": "key", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "value", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "type", + "type": { + "name": "tinyint" + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "documents" + } + } + ] + }, + { + "name": "Items", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "documents" + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 50 + } + ] + } + }, + { + "name": "qty", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "extra", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + } + ], + "params": [] + }, + { + "name": "Words", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "documents" + }, + { + "name": "value", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "ordinal", + "type": { + "name": "bigint" + } + } + ], + "params": [] + }, + { + "name": "Series", + "cmd": ":many", + "columns": [ + { + "name": "value", + "type": { + "name": "int" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_table_valued_functions/mssql/exec.json b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_table_valued_functions/mssql/query.sql b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/query.sql new file mode 100644 index 0000000000..703a1aa0a2 --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/query.sql @@ -0,0 +1,8 @@ +-- name: ApplyBoth :many +SELECT o.id, o.name, o.price, m.id AS mid, m.label FROM dbo.items_over(@p) o CROSS APPLY dbo.ms_items(o.id) m; + +-- name: InlineStar :many +SELECT * FROM items_over(@min); + +-- name: OuterApply :many +SELECT i.id, m.label FROM items i OUTER APPLY ms_items(i.id) m WHERE i.id = @id; diff --git a/internal/endtoend/testdata/analyze_table_valued_functions/mssql/schema.sql b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/schema.sql new file mode 100644 index 0000000000..9f7b6cba9d --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE items (id INT NOT NULL, name NVARCHAR(20) NULL, price DECIMAL(10,2) NOT NULL); +GO +CREATE FUNCTION dbo.items_over(@min DECIMAL(10,2)) RETURNS TABLE +AS RETURN SELECT id, name, price FROM items WHERE price > @min; +GO +CREATE FUNCTION ms_items(@n INT) RETURNS @t TABLE (id INT NOT NULL, label NVARCHAR(30)) +AS BEGIN + INSERT @t SELECT id, name FROM items WHERE id > @n; + RETURN; +END; +GO diff --git a/internal/endtoend/testdata/analyze_table_valued_functions/mssql/stdout.json b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/stdout.json new file mode 100644 index 0000000000..f8997abd13 --- /dev/null +++ b/internal/endtoend/testdata/analyze_table_valued_functions/mssql/stdout.json @@ -0,0 +1,171 @@ +[ + { + "name": "ApplyBoth", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + } + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 20 + } + ] + } + }, + { + "name": "price", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "mid", + "type": { + "name": "int" + } + }, + { + "name": "label", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 30 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "p", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + } + } + ] + }, + { + "name": "InlineStar", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + } + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 20 + } + ] + } + }, + { + "name": "price", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "min", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + } + } + ] + }, + { + "name": "OuterApply", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "items" + }, + { + "name": "label", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 30 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "type": { + "name": "int" + }, + "table": "items" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_tablesample/mssql/exec.json b/internal/endtoend/testdata/analyze_tablesample/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_tablesample/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_tablesample/mssql/query.sql b/internal/endtoend/testdata/analyze_tablesample/mssql/query.sql new file mode 100644 index 0000000000..7c8561d078 --- /dev/null +++ b/internal/endtoend/testdata/analyze_tablesample/mssql/query.sql @@ -0,0 +1,5 @@ +-- name: Sampled :many +SELECT id FROM t TABLESAMPLE (@pct PERCENT) WHERE id > @min; + +-- name: SampledRows :many +SELECT id FROM t TABLESAMPLE SYSTEM (@n ROWS) REPEATABLE (@seed); diff --git a/internal/endtoend/testdata/analyze_tablesample/mssql/schema.sql b/internal/endtoend/testdata/analyze_tablesample/mssql/schema.sql new file mode 100644 index 0000000000..64963bfc13 --- /dev/null +++ b/internal/endtoend/testdata/analyze_tablesample/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE t (id INT NOT NULL, name NVARCHAR(50) NULL, at DATETIME2 NOT NULL); diff --git a/internal/endtoend/testdata/analyze_tablesample/mssql/stdout.json b/internal/endtoend/testdata/analyze_tablesample/mssql/stdout.json new file mode 100644 index 0000000000..9ea3c11619 --- /dev/null +++ b/internal/endtoend/testdata/analyze_tablesample/mssql/stdout.json @@ -0,0 +1,69 @@ +[ + { + "name": "Sampled", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "t" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "pct", + "type": { + "name": "float" + } + } + }, + { + "number": 2, + "column": { + "name": "min", + "type": { + "name": "int" + }, + "table": "t" + } + } + ] + }, + { + "name": "SampledRows", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "t" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "n", + "type": { + "name": "bigint" + } + } + }, + { + "number": 2, + "column": { + "name": "seed", + "type": { + "name": "bigint" + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_temporal/mssql/exec.json b/internal/endtoend/testdata/analyze_temporal/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_temporal/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_temporal/mssql/query.sql b/internal/endtoend/testdata/analyze_temporal/mssql/query.sql new file mode 100644 index 0000000000..06320c8b79 --- /dev/null +++ b/internal/endtoend/testdata/analyze_temporal/mssql/query.sql @@ -0,0 +1,5 @@ +-- name: PriceAsOf :many +SELECT id, amount FROM prices FOR SYSTEM_TIME AS OF @at WHERE id = @id; + +-- name: PriceHistory :many +SELECT id, amount, valid_from, valid_to FROM prices FOR SYSTEM_TIME BETWEEN @from AND @to; diff --git a/internal/endtoend/testdata/analyze_temporal/mssql/schema.sql b/internal/endtoend/testdata/analyze_temporal/mssql/schema.sql new file mode 100644 index 0000000000..971a5a65cd --- /dev/null +++ b/internal/endtoend/testdata/analyze_temporal/mssql/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE prices ( + id INT PRIMARY KEY, + amount DECIMAL(10,2) NOT NULL, + valid_from DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL, + valid_to DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL, + PERIOD FOR SYSTEM_TIME (valid_from, valid_to) +) WITH (SYSTEM_VERSIONING = ON); diff --git a/internal/endtoend/testdata/analyze_temporal/mssql/stdout.json b/internal/endtoend/testdata/analyze_temporal/mssql/stdout.json new file mode 100644 index 0000000000..942935336f --- /dev/null +++ b/internal/endtoend/testdata/analyze_temporal/mssql/stdout.json @@ -0,0 +1,138 @@ +[ + { + "name": "PriceAsOf", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "prices" + }, + { + "name": "amount", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + }, + "table": "prices" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "at", + "type": { + "name": "datetime2", + "args": [ + { + "int": 7 + } + ] + } + } + }, + { + "number": 2, + "column": { + "name": "id", + "type": { + "name": "int" + }, + "table": "prices" + } + } + ] + }, + { + "name": "PriceHistory", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "prices" + }, + { + "name": "amount", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + }, + "table": "prices" + }, + { + "name": "valid_from", + "type": { + "name": "datetime2", + "args": [ + { + "int": 7 + } + ] + }, + "table": "prices" + }, + { + "name": "valid_to", + "type": { + "name": "datetime2", + "args": [ + { + "int": 7 + } + ] + }, + "table": "prices" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "from", + "type": { + "name": "datetime2", + "args": [ + { + "int": 7 + } + ] + } + } + }, + { + "number": 2, + "column": { + "name": "to", + "type": { + "name": "datetime2", + "args": [ + { + "int": 7 + } + ] + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_try_conversions/mssql/exec.json b/internal/endtoend/testdata/analyze_try_conversions/mssql/exec.json new file mode 100644 index 0000000000..7b9f8e17cc --- /dev/null +++ b/internal/endtoend/testdata/analyze_try_conversions/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base", "core"] +} diff --git a/internal/endtoend/testdata/analyze_try_conversions/mssql/query.sql b/internal/endtoend/testdata/analyze_try_conversions/mssql/query.sql new file mode 100644 index 0000000000..a6125ce7cf --- /dev/null +++ b/internal/endtoend/testdata/analyze_try_conversions/mssql/query.sql @@ -0,0 +1,15 @@ +-- name: Converted :many +SELECT id, + TRY_CAST(raw AS INT) AS as_int, + TRY_CONVERT(DECIMAL(5,1), raw) AS as_decimal, + TRY_CONVERT(DATETIME2(3), raw, 126) AS as_datetime, + TRY_CAST(note AS NVARCHAR(MAX)) AS as_text +FROM readings +WHERE TRY_CAST(@min AS INT) > 0; + +-- name: Parsed :many +SELECT id, + TRY_PARSE(raw AS DECIMAL(10,2)) AS try_parsed, + TRY_PARSE(note AS DATETIME2 USING 'en-US') AS try_parsed_date +FROM readings +WHERE PARSE(@limit AS INT) > 0; diff --git a/internal/endtoend/testdata/analyze_try_conversions/mssql/schema.sql b/internal/endtoend/testdata/analyze_try_conversions/mssql/schema.sql new file mode 100644 index 0000000000..ac4875448d --- /dev/null +++ b/internal/endtoend/testdata/analyze_try_conversions/mssql/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE readings ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + raw VARCHAR(10) NOT NULL, + note VARCHAR(10), + amount DECIMAL(10,2) +); diff --git a/internal/endtoend/testdata/analyze_try_conversions/mssql/stdout.json b/internal/endtoend/testdata/analyze_try_conversions/mssql/stdout.json new file mode 100644 index 0000000000..eab9cf4808 --- /dev/null +++ b/internal/endtoend/testdata/analyze_try_conversions/mssql/stdout.json @@ -0,0 +1,128 @@ +[ + { + "name": "Converted", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "readings" + }, + { + "name": "as_int", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "as_decimal", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 5 + }, + { + "int": 1 + } + ] + } + }, + { + "name": "as_datetime", + "type": { + "name": "datetime2", + "nullable": true, + "args": [ + { + "int": 3 + } + ] + } + }, + { + "name": "as_text", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "min", + "type": { + "name": "int" + } + } + } + ] + }, + { + "name": "Parsed", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "readings" + }, + { + "name": "try_parsed", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "try_parsed_date", + "type": { + "name": "datetime2", + "nullable": true, + "args": [ + { + "int": 7 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "limit", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 4000 + } + ] + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_types/mssql/exec.json b/internal/endtoend/testdata/analyze_types/mssql/exec.json index 253b05abbf..7b9f8e17cc 100644 --- a/internal/endtoend/testdata/analyze_types/mssql/exec.json +++ b/internal/endtoend/testdata/analyze_types/mssql/exec.json @@ -1,5 +1,5 @@ { "command": "analyze", "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], - "contexts": ["base"] + "contexts": ["base", "core"] } diff --git a/internal/endtoend/testdata/analyze_types/mssql/query.sql b/internal/endtoend/testdata/analyze_types/mssql/query.sql index e2075d7abd..02d0efff15 100644 --- a/internal/endtoend/testdata/analyze_types/mssql/query.sql +++ b/internal/endtoend/testdata/analyze_types/mssql/query.sql @@ -22,3 +22,6 @@ WHERE CAST(@a AS DECIMAL(5,2)) > 0 -- name: Params :one SELECT id FROM things WHERE price = @price AND body = @body AND phone = @phone AND offset_at = @offset_at; + +-- name: NumericLiterals :one +SELECT 2147483648 AS over_int, 9223372036854775808 AS over_bigint; diff --git a/internal/endtoend/testdata/analyze_types/mssql/stdout.json b/internal/endtoend/testdata/analyze_types/mssql/stdout.json index 2d747d3085..8de370925e 100644 --- a/internal/endtoend/testdata/analyze_types/mssql/stdout.json +++ b/internal/endtoend/testdata/analyze_types/mssql/stdout.json @@ -596,5 +596,40 @@ } } ] + }, + { + "name": "NumericLiterals", + "cmd": ":one", + "columns": [ + { + "name": "over_int", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 0 + } + ] + } + }, + { + "name": "over_bigint", + "type": { + "name": "decimal", + "args": [ + { + "int": 19 + }, + { + "int": 0 + } + ] + } + } + ], + "params": [] } ] diff --git a/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/exec.json b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/query.sql b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/query.sql new file mode 100644 index 0000000000..41eee04589 --- /dev/null +++ b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/query.sql @@ -0,0 +1,2 @@ +-- name: Negated :many +SELECT -CAST(i AS DECIMAL) AS nd, d * CAST(i AS DECIMAL(12,3)) AS product FROM p2; diff --git a/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/schema.sql b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/schema.sql new file mode 100644 index 0000000000..e1944ab1c9 --- /dev/null +++ b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE p2 (d DECIMAL(10,2) NOT NULL, i INT NOT NULL, f FLOAT NOT NULL); diff --git a/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/stdout.json b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/stdout.json new file mode 100644 index 0000000000..29a0af6f68 --- /dev/null +++ b/internal/endtoend/testdata/analyze_uninterned_decimal/mssql/stdout.json @@ -0,0 +1,21 @@ +[ + { + "name": "Negated", + "cmd": ":many", + "columns": [ + { + "name": "nd", + "type": { + "name": "decimal" + } + }, + { + "name": "product", + "type": { + "name": "decimal" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_values_table/mssql/exec.json b/internal/endtoend/testdata/analyze_values_table/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_values_table/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_values_table/mssql/query.sql b/internal/endtoend/testdata/analyze_values_table/mssql/query.sql new file mode 100644 index 0000000000..a2224287b0 --- /dev/null +++ b/internal/endtoend/testdata/analyze_values_table/mssql/query.sql @@ -0,0 +1,16 @@ +-- name: Weights :many +SELECT a.id, a.name, v.weight +FROM authors a +JOIN (VALUES (1, 10), (2, 20)) AS v(id, weight) ON v.id = a.id; + +-- name: Correlated :many +SELECT a.id, v.n +FROM authors a +CROSS APPLY (VALUES (1), (2)) v(n) +WHERE v.n < a.id; + +-- name: Lateral :many +SELECT a.id +FROM authors a +CROSS APPLY (VALUES (a.id), (a.id + 1)) v(n) +WHERE v.n > 1; diff --git a/internal/endtoend/testdata/analyze_values_table/mssql/schema.sql b/internal/endtoend/testdata/analyze_values_table/mssql/schema.sql new file mode 100644 index 0000000000..3629635d2e --- /dev/null +++ b/internal/endtoend/testdata/analyze_values_table/mssql/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE authors ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(100) NOT NULL, + bio NVARCHAR(MAX), + royalties DECIMAL(10,2) NOT NULL, + created DATETIME2 NOT NULL +); diff --git a/internal/endtoend/testdata/analyze_values_table/mssql/stdout.json b/internal/endtoend/testdata/analyze_values_table/mssql/stdout.json new file mode 100644 index 0000000000..ed0b4e767a --- /dev/null +++ b/internal/endtoend/testdata/analyze_values_table/mssql/stdout.json @@ -0,0 +1,68 @@ +[ + { + "name": "Weights", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "authors" + }, + { + "name": "weight", + "type": { + "name": "int" + } + } + ], + "params": [] + }, + { + "name": "Correlated", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "n", + "type": { + "name": "int" + } + } + ], + "params": [] + }, + { + "name": "Lateral", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/mssql_correlated_union/mssql/exec.json b/internal/endtoend/testdata/mssql_correlated_union/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/mssql_correlated_union/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/mssql_correlated_union/mssql/query.sql b/internal/endtoend/testdata/mssql_correlated_union/mssql/query.sql new file mode 100644 index 0000000000..d5d6716d35 --- /dev/null +++ b/internal/endtoend/testdata/mssql_correlated_union/mssql/query.sql @@ -0,0 +1,8 @@ +-- name: Related :many +SELECT a.id, r.n +FROM authors a +CROSS APPLY ( + SELECT 1 AS n FROM books WHERE author_id = a.id + UNION ALL + SELECT 2 FROM authors WHERE id = a.id +) r; diff --git a/internal/endtoend/testdata/mssql_correlated_union/mssql/schema.sql b/internal/endtoend/testdata/mssql_correlated_union/mssql/schema.sql new file mode 100644 index 0000000000..2a49202aaa --- /dev/null +++ b/internal/endtoend/testdata/mssql_correlated_union/mssql/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(100) NOT NULL +); + +CREATE TABLE books ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + author_id BIGINT NOT NULL, + title NVARCHAR(200) NOT NULL +); diff --git a/internal/endtoend/testdata/mssql_correlated_union/mssql/stdout.json b/internal/endtoend/testdata/mssql_correlated_union/mssql/stdout.json new file mode 100644 index 0000000000..c0c2357a52 --- /dev/null +++ b/internal/endtoend/testdata/mssql_correlated_union/mssql/stdout.json @@ -0,0 +1,22 @@ +[ + { + "name": "Related", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "n", + "type": { + "name": "int" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/mssql_embed_outer_join/mssql/exec.json b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/exec.json new file mode 100644 index 0000000000..8ac9f9122f --- /dev/null +++ b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/exec.json @@ -0,0 +1 @@ +{"contexts": ["base", "core"]} diff --git a/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/db.go b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/models.go b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/models.go new file mode 100644 index 0000000000..33cf3d5142 --- /dev/null +++ b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Author struct { + ID int32 + Name string +} + +type Book struct { + ID int32 + AuthorID int32 + Title sql.NullString +} diff --git a/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/query.sql.go b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/query.sql.go new file mode 100644 index 0000000000..2463659348 --- /dev/null +++ b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/go/query.sql.go @@ -0,0 +1,51 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const authorBooks = `-- name: AuthorBooks :many +SELECT a.id, a.name, b.id, b.author_id, b.title FROM authors a LEFT JOIN books b ON b.author_id = a.id; +` + +type AuthorBooksRow struct { + Author Author + ID sql.NullInt32 + AuthorID sql.NullInt32 + Title sql.NullString +} + +func (q *Queries) AuthorBooks(ctx context.Context) ([]AuthorBooksRow, error) { + rows, err := q.db.QueryContext(ctx, authorBooks) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AuthorBooksRow + for rows.Next() { + var i AuthorBooksRow + if err := rows.Scan( + &i.Author.ID, + &i.Author.Name, + &i.ID, + &i.AuthorID, + &i.Title, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/mssql_embed_outer_join/mssql/query.sql b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/query.sql new file mode 100644 index 0000000000..bd127e839d --- /dev/null +++ b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/query.sql @@ -0,0 +1,2 @@ +-- name: AuthorBooks :many +SELECT sqlc.embed(a), sqlc.embed(b) FROM authors a LEFT JOIN books b ON b.author_id = a.id; diff --git a/internal/endtoend/testdata/mssql_embed_outer_join/mssql/schema.sql b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/schema.sql new file mode 100644 index 0000000000..7aaba24c5a --- /dev/null +++ b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE authors (id INT NOT NULL, name NVARCHAR(10) NOT NULL); +CREATE TABLE books (id INT NOT NULL, author_id INT NOT NULL, title NVARCHAR(20) NULL); diff --git a/internal/endtoend/testdata/mssql_embed_outer_join/mssql/sqlc.yaml b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/sqlc.yaml new file mode 100644 index 0000000000..94b0dc8896 --- /dev/null +++ b/internal/endtoend/testdata/mssql_embed_outer_join/mssql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "mssql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "database/sql" diff --git a/internal/endtoend/testdata/mssql_fulltext_params/mssql/exec.json b/internal/endtoend/testdata/mssql_fulltext_params/mssql/exec.json new file mode 100644 index 0000000000..f4e21983b6 --- /dev/null +++ b/internal/endtoend/testdata/mssql_fulltext_params/mssql/exec.json @@ -0,0 +1 @@ +{"contexts": ["base"]} diff --git a/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/db.go b/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/models.go b/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/models.go new file mode 100644 index 0000000000..32a015b046 --- /dev/null +++ b/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/models.go @@ -0,0 +1,10 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +type Book struct { + ID int32 + Title string +} diff --git a/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/query.sql.go b/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/query.sql.go new file mode 100644 index 0000000000..cfee736040 --- /dev/null +++ b/internal/endtoend/testdata/mssql_fulltext_params/mssql/go/query.sql.go @@ -0,0 +1,70 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const searchBooks = `-- name: SearchBooks :many +SELECT id, title FROM books WHERE CONTAINS(title, @query); +` + +func (q *Queries) SearchBooks(ctx context.Context, query string) ([]Book, error) { + rows, err := q.db.QueryContext(ctx, searchBooks, sql.Named("query", query)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Book + for rows.Next() { + var i Book + if err := rows.Scan(&i.ID, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchBooksFreeText = `-- name: SearchBooksFreeText :many +SELECT id, title FROM books WHERE FREETEXT(title, @phrase, LANGUAGE 1033) AND id > @min_id; +` + +type SearchBooksFreeTextParams struct { + Phrase string + MinID int32 +} + +func (q *Queries) SearchBooksFreeText(ctx context.Context, arg SearchBooksFreeTextParams) ([]Book, error) { + rows, err := q.db.QueryContext(ctx, searchBooksFreeText, sql.Named("phrase", arg.Phrase), sql.Named("min_id", arg.MinID)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Book + for rows.Next() { + var i Book + if err := rows.Scan(&i.ID, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/mssql_fulltext_params/mssql/query.sql b/internal/endtoend/testdata/mssql_fulltext_params/mssql/query.sql new file mode 100644 index 0000000000..66a83e8708 --- /dev/null +++ b/internal/endtoend/testdata/mssql_fulltext_params/mssql/query.sql @@ -0,0 +1,5 @@ +-- name: SearchBooks :many +SELECT id, title FROM books WHERE CONTAINS(title, @query); + +-- name: SearchBooksFreeText :many +SELECT id, title FROM books WHERE FREETEXT(title, @phrase, LANGUAGE 1033) AND id > @min_id; diff --git a/internal/endtoend/testdata/mssql_fulltext_params/mssql/schema.sql b/internal/endtoend/testdata/mssql_fulltext_params/mssql/schema.sql new file mode 100644 index 0000000000..baa6ed3a42 --- /dev/null +++ b/internal/endtoend/testdata/mssql_fulltext_params/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE books (id INT PRIMARY KEY, title NVARCHAR(200) NOT NULL); diff --git a/internal/endtoend/testdata/mssql_fulltext_params/mssql/sqlc.yaml b/internal/endtoend/testdata/mssql_fulltext_params/mssql/sqlc.yaml new file mode 100644 index 0000000000..94b0dc8896 --- /dev/null +++ b/internal/endtoend/testdata/mssql_fulltext_params/mssql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "mssql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "database/sql" diff --git a/internal/endtoend/testdata/mssql_outer_join/mssql/exec.json b/internal/endtoend/testdata/mssql_outer_join/mssql/exec.json new file mode 100644 index 0000000000..f4e21983b6 --- /dev/null +++ b/internal/endtoend/testdata/mssql_outer_join/mssql/exec.json @@ -0,0 +1 @@ +{"contexts": ["base"]} diff --git a/internal/endtoend/testdata/mssql_outer_join/mssql/go/db.go b/internal/endtoend/testdata/mssql_outer_join/mssql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/mssql_outer_join/mssql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mssql_outer_join/mssql/go/models.go b/internal/endtoend/testdata/mssql_outer_join/mssql/go/models.go new file mode 100644 index 0000000000..927a05454c --- /dev/null +++ b/internal/endtoend/testdata/mssql_outer_join/mssql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +type Author struct { + ID int64 + Name string +} + +type Book struct { + ID int64 + AuthorID int64 + Title string +} diff --git a/internal/endtoend/testdata/mssql_outer_join/mssql/go/query.sql.go b/internal/endtoend/testdata/mssql_outer_join/mssql/go/query.sql.go new file mode 100644 index 0000000000..cb22db28e4 --- /dev/null +++ b/internal/endtoend/testdata/mssql_outer_join/mssql/go/query.sql.go @@ -0,0 +1,147 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const fullJoin = `-- name: FullJoin :many +SELECT a.name, b.title FROM authors a FULL OUTER JOIN books b ON b.author_id = a.id; +` + +type FullJoinRow struct { + Name sql.NullString + Title sql.NullString +} + +func (q *Queries) FullJoin(ctx context.Context) ([]FullJoinRow, error) { + rows, err := q.db.QueryContext(ctx, fullJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FullJoinRow + for rows.Next() { + var i FullJoinRow + if err := rows.Scan(&i.Name, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const leftJoin = `-- name: LeftJoin :many +SELECT a.id, a.name, b.title FROM authors a LEFT JOIN books b ON b.author_id = a.id; +` + +type LeftJoinRow struct { + ID int64 + Name string + Title sql.NullString +} + +func (q *Queries) LeftJoin(ctx context.Context) ([]LeftJoinRow, error) { + rows, err := q.db.QueryContext(ctx, leftJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LeftJoinRow + for rows.Next() { + var i LeftJoinRow + if err := rows.Scan(&i.ID, &i.Name, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const leftJoinStar = `-- name: LeftJoinStar :many +SELECT a.name, b.id, b.author_id, b.title FROM authors a LEFT JOIN books b ON b.author_id = a.id; +` + +type LeftJoinStarRow struct { + Name string + ID sql.NullInt64 + AuthorID sql.NullInt64 + Title sql.NullString +} + +func (q *Queries) LeftJoinStar(ctx context.Context) ([]LeftJoinStarRow, error) { + rows, err := q.db.QueryContext(ctx, leftJoinStar) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LeftJoinStarRow + for rows.Next() { + var i LeftJoinStarRow + if err := rows.Scan( + &i.Name, + &i.ID, + &i.AuthorID, + &i.Title, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const rightJoin = `-- name: RightJoin :many +SELECT a.name, b.title FROM authors a RIGHT JOIN books b ON b.author_id = a.id; +` + +type RightJoinRow struct { + Name sql.NullString + Title string +} + +func (q *Queries) RightJoin(ctx context.Context) ([]RightJoinRow, error) { + rows, err := q.db.QueryContext(ctx, rightJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []RightJoinRow + for rows.Next() { + var i RightJoinRow + if err := rows.Scan(&i.Name, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/mssql_outer_join/mssql/query.sql b/internal/endtoend/testdata/mssql_outer_join/mssql/query.sql new file mode 100644 index 0000000000..c9696e994a --- /dev/null +++ b/internal/endtoend/testdata/mssql_outer_join/mssql/query.sql @@ -0,0 +1,11 @@ +-- name: LeftJoin :many +SELECT a.id, a.name, b.title FROM authors a LEFT JOIN books b ON b.author_id = a.id; + +-- name: RightJoin :many +SELECT a.name, b.title FROM authors a RIGHT JOIN books b ON b.author_id = a.id; + +-- name: FullJoin :many +SELECT a.name, b.title FROM authors a FULL OUTER JOIN books b ON b.author_id = a.id; + +-- name: LeftJoinStar :many +SELECT a.name, b.* FROM authors a LEFT JOIN books b ON b.author_id = a.id; diff --git a/internal/endtoend/testdata/mssql_outer_join/mssql/schema.sql b/internal/endtoend/testdata/mssql_outer_join/mssql/schema.sql new file mode 100644 index 0000000000..754abf3290 --- /dev/null +++ b/internal/endtoend/testdata/mssql_outer_join/mssql/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(100) NOT NULL +); + +CREATE TABLE books ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + author_id BIGINT NOT NULL, + title NVARCHAR(200) NOT NULL +); diff --git a/internal/endtoend/testdata/mssql_outer_join/mssql/sqlc.yaml b/internal/endtoend/testdata/mssql_outer_join/mssql/sqlc.yaml new file mode 100644 index 0000000000..94b0dc8896 --- /dev/null +++ b/internal/endtoend/testdata/mssql_outer_join/mssql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "mssql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "database/sql" diff --git a/internal/endtoend/testdata/mssql_output_clause/mssql/exec.json b/internal/endtoend/testdata/mssql_output_clause/mssql/exec.json new file mode 100644 index 0000000000..8ac9f9122f --- /dev/null +++ b/internal/endtoend/testdata/mssql_output_clause/mssql/exec.json @@ -0,0 +1 @@ +{"contexts": ["base", "core"]} diff --git a/internal/endtoend/testdata/mssql_output_clause/mssql/go/db.go b/internal/endtoend/testdata/mssql_output_clause/mssql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/mssql_output_clause/mssql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mssql_output_clause/mssql/go/models.go b/internal/endtoend/testdata/mssql_output_clause/mssql/go/models.go new file mode 100644 index 0000000000..730bdef708 --- /dev/null +++ b/internal/endtoend/testdata/mssql_output_clause/mssql/go/models.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Author struct { + ID int64 + Name string + Bio sql.NullString +} + +type Book struct { + ID int64 + AuthorID int64 + Title string + Price sql.NullFloat64 +} diff --git a/internal/endtoend/testdata/mssql_output_clause/mssql/go/query.sql.go b/internal/endtoend/testdata/mssql_output_clause/mssql/go/query.sql.go new file mode 100644 index 0000000000..76833127fa --- /dev/null +++ b/internal/endtoend/testdata/mssql_output_clause/mssql/go/query.sql.go @@ -0,0 +1,123 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const createBook = `-- name: CreateBook :one +INSERT INTO books (id, author_id, title) OUTPUT inserted.id, inserted.author_id, inserted.title, inserted.price VALUES (@id, @author_id, @title); +` + +type CreateBookParams struct { + ID int64 + AuthorID int64 + Title string +} + +func (q *Queries) CreateBook(ctx context.Context, arg CreateBookParams) (Book, error) { + row := q.db.QueryRowContext(ctx, createBook, sql.Named("id", arg.ID), sql.Named("author_id", arg.AuthorID), sql.Named("title", arg.Title)) + var i Book + err := row.Scan( + &i.ID, + &i.AuthorID, + &i.Title, + &i.Price, + ) + return i, err +} + +const deleteBook = `-- name: DeleteBook :one +DELETE FROM books OUTPUT deleted.id, deleted.author_id, deleted.title, deleted.price WHERE id = @id; +` + +func (q *Queries) DeleteBook(ctx context.Context, id int64) (Book, error) { + row := q.db.QueryRowContext(ctx, deleteBook, sql.Named("id", id)) + var i Book + err := row.Scan( + &i.ID, + &i.AuthorID, + &i.Title, + &i.Price, + ) + return i, err +} + +const deleteByAuthor = `-- name: DeleteByAuthor :many +DELETE b OUTPUT DELETED.id, DELETED.title +FROM books b JOIN authors a ON a.id = b.author_id +WHERE a.name = @name; +` + +type DeleteByAuthorRow struct { + ID int64 + Title string +} + +func (q *Queries) DeleteByAuthor(ctx context.Context, name string) ([]DeleteByAuthorRow, error) { + rows, err := q.db.QueryContext(ctx, deleteByAuthor, sql.Named("name", name)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DeleteByAuthorRow + for rows.Next() { + var i DeleteByAuthorRow + if err := rows.Scan(&i.ID, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const retitleByAuthor = `-- name: RetitleByAuthor :many +UPDATE b SET title = @title +OUTPUT INSERTED.id, DELETED.title AS old_title +FROM books b JOIN authors a ON a.id = b.author_id +WHERE a.name = @name; +` + +type RetitleByAuthorParams struct { + Title string + Name string +} + +type RetitleByAuthorRow struct { + ID int64 + OldTitle string +} + +func (q *Queries) RetitleByAuthor(ctx context.Context, arg RetitleByAuthorParams) ([]RetitleByAuthorRow, error) { + rows, err := q.db.QueryContext(ctx, retitleByAuthor, sql.Named("title", arg.Title), sql.Named("name", arg.Name)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []RetitleByAuthorRow + for rows.Next() { + var i RetitleByAuthorRow + if err := rows.Scan(&i.ID, &i.OldTitle); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/mssql_output_clause/mssql/query.sql b/internal/endtoend/testdata/mssql_output_clause/mssql/query.sql new file mode 100644 index 0000000000..c08f806278 --- /dev/null +++ b/internal/endtoend/testdata/mssql_output_clause/mssql/query.sql @@ -0,0 +1,16 @@ +-- name: CreateBook :one +INSERT INTO books (id, author_id, title) OUTPUT INSERTED.* VALUES (@id, @author_id, @title); + +-- name: RetitleByAuthor :many +UPDATE b SET title = @title +OUTPUT INSERTED.id, DELETED.title AS old_title +FROM books b JOIN authors a ON a.id = b.author_id +WHERE a.name = @name; + +-- name: DeleteByAuthor :many +DELETE b OUTPUT DELETED.id, DELETED.title +FROM books b JOIN authors a ON a.id = b.author_id +WHERE a.name = @name; + +-- name: DeleteBook :one +DELETE FROM books OUTPUT DELETED.* WHERE id = @id; diff --git a/internal/endtoend/testdata/mssql_output_clause/mssql/schema.sql b/internal/endtoend/testdata/mssql_output_clause/mssql/schema.sql new file mode 100644 index 0000000000..8b4a7c39cb --- /dev/null +++ b/internal/endtoend/testdata/mssql_output_clause/mssql/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE authors (id BIGINT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(100) NOT NULL, bio NVARCHAR(MAX)); +CREATE TABLE books (id BIGINT PRIMARY KEY, author_id BIGINT NOT NULL, title NVARCHAR(200) NOT NULL, price FLOAT); diff --git a/internal/endtoend/testdata/mssql_output_clause/mssql/sqlc.yaml b/internal/endtoend/testdata/mssql_output_clause/mssql/sqlc.yaml new file mode 100644 index 0000000000..94b0dc8896 --- /dev/null +++ b/internal/endtoend/testdata/mssql_output_clause/mssql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "mssql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "database/sql" diff --git a/internal/endtoend/testdata/mssql_sqlc_macros/mssql/exec.json b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/exec.json new file mode 100644 index 0000000000..f4e21983b6 --- /dev/null +++ b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/exec.json @@ -0,0 +1 @@ +{"contexts": ["base"]} diff --git a/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/db.go b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/models.go b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/models.go new file mode 100644 index 0000000000..4073af03ce --- /dev/null +++ b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/models.go @@ -0,0 +1,21 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Author struct { + ID int64 + Name string + Bio sql.NullString +} + +type Book struct { + ID int64 + AuthorID int64 + Title string +} diff --git a/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/query.sql.go b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/query.sql.go new file mode 100644 index 0000000000..4bad0e6fab --- /dev/null +++ b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/go/query.sql.go @@ -0,0 +1,303 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + "strconv" + "strings" +) + +const authorBooks = `-- name: AuthorBooks :many +SELECT a.id, a.name, a.bio, books.id, books.author_id, books.title, books.title AS book_title +FROM authors a JOIN books ON books.author_id = a.id +WHERE a.id = @author_id +ORDER BY books.id; +` + +type AuthorBooksRow struct { + Author Author + Book Book + BookTitle string +} + +func (q *Queries) AuthorBooks(ctx context.Context, authorID int64) ([]AuthorBooksRow, error) { + rows, err := q.db.QueryContext(ctx, authorBooks, sql.Named("author_id", authorID)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AuthorBooksRow + for rows.Next() { + var i AuthorBooksRow + if err := rows.Scan( + &i.Author.ID, + &i.Author.Name, + &i.Author.Bio, + &i.Book.ID, + &i.Book.AuthorID, + &i.Book.Title, + &i.BookTitle, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const createAuthor = `-- name: CreateAuthor :exec +INSERT INTO authors (id, name, bio) VALUES (@id, @name, @bio); +` + +type CreateAuthorParams struct { + ID int64 + Name string + Bio sql.NullString +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) error { + _, err := q.db.ExecContext(ctx, createAuthor, sql.Named("id", arg.ID), sql.Named("name", arg.Name), sql.Named("bio", arg.Bio)) + return err +} + +const embedNarrowSubquery = `-- name: EmbedNarrowSubquery :many +SELECT x.id, x.name FROM (SELECT id, name FROM authors) x; +` + +type EmbedNarrowSubqueryRow struct { + ID int64 + Name string +} + +func (q *Queries) EmbedNarrowSubquery(ctx context.Context) ([]EmbedNarrowSubqueryRow, error) { + rows, err := q.db.QueryContext(ctx, embedNarrowSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EmbedNarrowSubqueryRow + for rows.Next() { + var i EmbedNarrowSubqueryRow + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const embedReorderedSubquery = `-- name: EmbedReorderedSubquery :many +SELECT x.name, x.id, x.bio FROM (SELECT name, id, bio FROM authors) x; +` + +type EmbedReorderedSubqueryRow struct { + Name string + ID int64 + Bio sql.NullString +} + +func (q *Queries) EmbedReorderedSubquery(ctx context.Context) ([]EmbedReorderedSubqueryRow, error) { + rows, err := q.db.QueryContext(ctx, embedReorderedSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EmbedReorderedSubqueryRow + for rows.Next() { + var i EmbedReorderedSubqueryRow + if err := rows.Scan(&i.Name, &i.ID, &i.Bio); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAuthorsByIDs = `-- name: ListAuthorsByIDs :many +SELECT id, name FROM authors WHERE id IN (/*SLICE:ids*/@ids) ORDER BY id; +` + +type ListAuthorsByIDsRow struct { + ID int64 + Name string +} + +func (q *Queries) ListAuthorsByIDs(ctx context.Context, ids []int64) ([]ListAuthorsByIDsRow, error) { + query := listAuthorsByIDs + var queryParams []any + if len(ids) > 0 { + var placeholders []string + for _, v := range ids { + queryParams = append(queryParams, v) + placeholders = append(placeholders, "@p"+strconv.Itoa(len(queryParams))) + } + query = strings.ReplaceAll(query, "/*SLICE:ids*/@ids", strings.Join(placeholders, ",")) + } else { + query = strings.ReplaceAll(query, "/*SLICE:ids*/@ids", "NULL") + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAuthorsByIDsRow + for rows.Next() { + var i ListAuthorsByIDsRow + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAuthorsByIDsAndName = `-- name: ListAuthorsByIDsAndName :many +SELECT id, name FROM authors WHERE name <> @name AND id IN (/*SLICE:ids*/@ids) AND bio IS NULL ORDER BY id; +` + +type ListAuthorsByIDsAndNameParams struct { + Name string + Ids []int64 +} + +type ListAuthorsByIDsAndNameRow struct { + ID int64 + Name string +} + +func (q *Queries) ListAuthorsByIDsAndName(ctx context.Context, arg ListAuthorsByIDsAndNameParams) ([]ListAuthorsByIDsAndNameRow, error) { + query := listAuthorsByIDsAndName + var queryParams []any + queryParams = append(queryParams, sql.Named("name", arg.Name)) + if len(arg.Ids) > 0 { + var placeholders []string + for _, v := range arg.Ids { + queryParams = append(queryParams, v) + placeholders = append(placeholders, "@p"+strconv.Itoa(len(queryParams))) + } + query = strings.ReplaceAll(query, "/*SLICE:ids*/@ids", strings.Join(placeholders, ",")) + } else { + query = strings.ReplaceAll(query, "/*SLICE:ids*/@ids", "NULL") + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAuthorsByIDsAndNameRow + for rows.Next() { + var i ListAuthorsByIDsAndNameRow + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAuthorsByIDsTwice = `-- name: ListAuthorsByIDsTwice :many +SELECT id, name FROM authors WHERE id IN (/*SLICE:ids*/@ids) OR id + 100 IN (/*SLICE:ids*/@ids); +` + +type ListAuthorsByIDsTwiceRow struct { + ID int64 + Name string +} + +func (q *Queries) ListAuthorsByIDsTwice(ctx context.Context, ids []int64) ([]ListAuthorsByIDsTwiceRow, error) { + query := listAuthorsByIDsTwice + var queryParams []any + if len(ids) > 0 { + var placeholders []string + for _, v := range ids { + queryParams = append(queryParams, v) + placeholders = append(placeholders, "@p"+strconv.Itoa(len(queryParams))) + } + query = strings.ReplaceAll(query, "/*SLICE:ids*/@ids", strings.Join(placeholders, ",")) + } else { + query = strings.ReplaceAll(query, "/*SLICE:ids*/@ids", "NULL") + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAuthorsByIDsTwiceRow + for rows.Next() { + var i ListAuthorsByIDsTwiceRow + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const rowCount = `-- name: RowCount :one +SELECT CAST(@@ROWCOUNT AS INT) AS row_count; +` + +func (q *Queries) RowCount(ctx context.Context) (int32, error) { + row := q.db.QueryRowContext(ctx, rowCount) + var row_count int32 + err := row.Scan(&row_count) + return row_count, err +} + +const updateBio = `-- name: UpdateBio :execrows +UPDATE authors SET bio = @bio WHERE id = @id; +` + +type UpdateBioParams struct { + Bio sql.NullString + ID int64 +} + +func (q *Queries) UpdateBio(ctx context.Context, arg UpdateBioParams) (int64, error) { + result, err := q.db.ExecContext(ctx, updateBio, sql.Named("bio", arg.Bio), sql.Named("id", arg.ID)) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/internal/endtoend/testdata/mssql_sqlc_macros/mssql/query.sql b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/query.sql new file mode 100644 index 0000000000..3c973ed232 --- /dev/null +++ b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/query.sql @@ -0,0 +1,29 @@ +-- name: CreateAuthor :exec +INSERT INTO authors (id, name, bio) VALUES (sqlc.arg(id), sqlc.arg(name), sqlc.narg(bio)); + +-- name: UpdateBio :execrows +UPDATE authors SET bio = sqlc.narg(bio) WHERE id = @id; + +-- name: ListAuthorsByIDs :many +SELECT id, name FROM authors WHERE id IN (sqlc.slice(ids)) ORDER BY id; + +-- name: ListAuthorsByIDsAndName :many +SELECT id, name FROM authors WHERE name <> @name AND id IN (sqlc.slice(ids)) AND bio IS NULL ORDER BY id; + +-- name: RowCount :one +SELECT CAST(@@ROWCOUNT AS INT) AS row_count; + +-- name: AuthorBooks :many +SELECT sqlc.embed(a), sqlc.embed(books), books.title AS book_title +FROM authors a JOIN books ON books.author_id = a.id +WHERE a.id = sqlc.arg(author_id) +ORDER BY books.id; + +-- name: ListAuthorsByIDsTwice :many +SELECT id, name FROM authors WHERE id IN (sqlc.slice(ids)) OR id + 100 IN (sqlc.slice(ids)); + +-- name: EmbedNarrowSubquery :many +SELECT sqlc.embed(x) FROM (SELECT id, name FROM authors) x; + +-- name: EmbedReorderedSubquery :many +SELECT sqlc.embed(x) FROM (SELECT name, id, bio FROM authors) x; diff --git a/internal/endtoend/testdata/mssql_sqlc_macros/mssql/schema.sql b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/schema.sql new file mode 100644 index 0000000000..95887bf36e --- /dev/null +++ b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE authors ( + id BIGINT PRIMARY KEY, + name NVARCHAR(50) NOT NULL, + bio NVARCHAR(MAX) +); + +CREATE TABLE books ( + id BIGINT PRIMARY KEY, + author_id BIGINT NOT NULL, + title NVARCHAR(100) NOT NULL +); diff --git a/internal/endtoend/testdata/mssql_sqlc_macros/mssql/sqlc.yaml b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/sqlc.yaml new file mode 100644 index 0000000000..94b0dc8896 --- /dev/null +++ b/internal/endtoend/testdata/mssql_sqlc_macros/mssql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "mssql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "database/sql" diff --git a/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/exec.json b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/exec.json new file mode 100644 index 0000000000..8ac9f9122f --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/exec.json @@ -0,0 +1 @@ +{"contexts": ["base", "core"]} diff --git a/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/db.go b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/models.go b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/models.go new file mode 100644 index 0000000000..4167d53357 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/models.go @@ -0,0 +1,14 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Author struct { + ID int32 + Name sql.NullString +} diff --git a/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/query.sql.go b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/query.sql.go new file mode 100644 index 0000000000..b285fd17b3 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/go/query.sql.go @@ -0,0 +1,118 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const a = `-- name: A :many +SELECT [a"b].id, [a"b].name FROM authors AS "a""b"; +` + +func (q *Queries) A(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, a) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const b = `-- name: B :many +SELECT [x]]y].id, [x]]y].name FROM authors AS "x]y"; +` + +func (q *Queries) B(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, b) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const c = `-- name: C :many +SELECT [x]]y].id, [x]]y].name FROM authors AS [x]]y]; +` + +func (q *Queries) C(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, c) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const d = `-- name: D :many +SELECT [select].id, [select].name FROM authors AS "select"; +` + +func (q *Queries) D(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, d) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/query.sql b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/query.sql new file mode 100644 index 0000000000..9e5b6a3408 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/query.sql @@ -0,0 +1,11 @@ +-- name: A :many +SELECT "a""b".* FROM authors AS "a""b"; + +-- name: B :many +SELECT "x]y".* FROM authors AS "x]y"; + +-- name: C :many +SELECT [x]]y].* FROM authors AS [x]]y]; + +-- name: D :many +SELECT "select".* FROM authors AS "select"; diff --git a/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/schema.sql b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/schema.sql new file mode 100644 index 0000000000..531179cece --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE authors (id INT NOT NULL, name NVARCHAR(10) NULL); diff --git a/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/sqlc.yaml b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/sqlc.yaml new file mode 100644 index 0000000000..94b0dc8896 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_escaped_qualifier/mssql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "mssql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "database/sql" diff --git a/internal/endtoend/testdata/mssql_star_quoting/mssql/exec.json b/internal/endtoend/testdata/mssql_star_quoting/mssql/exec.json new file mode 100644 index 0000000000..f4e21983b6 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_quoting/mssql/exec.json @@ -0,0 +1 @@ +{"contexts": ["base"]} diff --git a/internal/endtoend/testdata/mssql_star_quoting/mssql/go/db.go b/internal/endtoend/testdata/mssql_star_quoting/mssql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_quoting/mssql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/mssql_star_quoting/mssql/go/models.go b/internal/endtoend/testdata/mssql_star_quoting/mssql/go/models.go new file mode 100644 index 0000000000..a5ecb804af --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_quoting/mssql/go/models.go @@ -0,0 +1,21 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Order struct { + ID int32 + Select string + WeirdName sql.NullInt32 +} + +type Sale struct { + AuthorID int32 + Year int32 + Amount int32 +} diff --git a/internal/endtoend/testdata/mssql_star_quoting/mssql/go/query.sql.go b/internal/endtoend/testdata/mssql_star_quoting/mssql/go/query.sql.go new file mode 100644 index 0000000000..f145d188af --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_quoting/mssql/go/query.sql.go @@ -0,0 +1,98 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const listOrders = `-- name: ListOrders :many +SELECT o.id, o.[select], o.[weird]]name] FROM [order] o; +` + +func (q *Queries) ListOrders(ctx context.Context) ([]Order, error) { + rows, err := q.db.QueryContext(ctx, listOrders) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Order + for rows.Next() { + var i Order + if err := rows.Scan(&i.ID, &i.Select, &i.WeirdName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listOrdersDoubleQuoted = `-- name: ListOrdersDoubleQuoted :many +SELECT o.id, o.[select], o.[weird]]name] FROM [order] "o"; +` + +func (q *Queries) ListOrdersDoubleQuoted(ctx context.Context) ([]Order, error) { + rows, err := q.db.QueryContext(ctx, listOrdersDoubleQuoted) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Order + for rows.Next() { + var i Order + if err := rows.Scan(&i.ID, &i.Select, &i.WeirdName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const salesByYear = `-- name: SalesByYear :many +SELECT pv.author_id, pv.[2023], pv.[2024] FROM sales PIVOT (SUM(amount) FOR year IN ([2023], [2024])) pv; +` + +type SalesByYearRow struct { + AuthorID int32 + _2023 sql.NullInt32 + _2024 sql.NullInt32 +} + +func (q *Queries) SalesByYear(ctx context.Context) ([]SalesByYearRow, error) { + rows, err := q.db.QueryContext(ctx, salesByYear) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SalesByYearRow + for rows.Next() { + var i SalesByYearRow + if err := rows.Scan(&i.AuthorID, &i._2023, &i._2024); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/mssql_star_quoting/mssql/query.sql b/internal/endtoend/testdata/mssql_star_quoting/mssql/query.sql new file mode 100644 index 0000000000..8f19dc927f --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_quoting/mssql/query.sql @@ -0,0 +1,8 @@ +-- name: ListOrders :many +SELECT o.* FROM [order] o; + +-- name: SalesByYear :many +SELECT pv.* FROM sales PIVOT (SUM(amount) FOR year IN ([2023], [2024])) pv; + +-- name: ListOrdersDoubleQuoted :many +SELECT "o".* FROM [order] "o"; diff --git a/internal/endtoend/testdata/mssql_star_quoting/mssql/schema.sql b/internal/endtoend/testdata/mssql_star_quoting/mssql/schema.sql new file mode 100644 index 0000000000..67eb0ffc8a --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_quoting/mssql/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE [order] ( + id INT PRIMARY KEY, + [select] NVARCHAR(10) NOT NULL, + [weird]]name] INT +); + +CREATE TABLE sales ( + author_id INT NOT NULL, + year INT NOT NULL, + amount INT NOT NULL +); diff --git a/internal/endtoend/testdata/mssql_star_quoting/mssql/sqlc.yaml b/internal/endtoend/testdata/mssql_star_quoting/mssql/sqlc.yaml new file mode 100644 index 0000000000..94b0dc8896 --- /dev/null +++ b/internal/endtoend/testdata/mssql_star_quoting/mssql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "mssql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "database/sql" diff --git a/internal/endtoend/testdata/mssql_string_split_param/mssql/exec.json b/internal/endtoend/testdata/mssql_string_split_param/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/mssql_string_split_param/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mssql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/mssql_string_split_param/mssql/query.sql b/internal/endtoend/testdata/mssql_string_split_param/mssql/query.sql new file mode 100644 index 0000000000..16c0c69a27 --- /dev/null +++ b/internal/endtoend/testdata/mssql_string_split_param/mssql/query.sql @@ -0,0 +1,6 @@ +-- name: ByIDs :many +SELECT id, title FROM documents +WHERE id IN (SELECT CAST(value AS BIGINT) FROM STRING_SPLIT(@ids, ',')); + +-- name: Tokens :many +SELECT value FROM STRING_SPLIT(@list, ','); diff --git a/internal/endtoend/testdata/mssql_string_split_param/mssql/schema.sql b/internal/endtoend/testdata/mssql_string_split_param/mssql/schema.sql new file mode 100644 index 0000000000..f70422d2a7 --- /dev/null +++ b/internal/endtoend/testdata/mssql_string_split_param/mssql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE documents ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + title NVARCHAR(100) NOT NULL, + body NVARCHAR(MAX) +); diff --git a/internal/endtoend/testdata/mssql_string_split_param/mssql/stdout.json b/internal/endtoend/testdata/mssql_string_split_param/mssql/stdout.json new file mode 100644 index 0000000000..430beca540 --- /dev/null +++ b/internal/endtoend/testdata/mssql_string_split_param/mssql/stdout.json @@ -0,0 +1,61 @@ +[ + { + "name": "ByIDs", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "documents" + }, + { + "name": "title", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + }, + "table": "documents" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "ids", + "type": { + "name": "varchar" + } + } + } + ] + }, + { + "name": "Tokens", + "cmd": ":many", + "columns": [ + { + "name": "value", + "type": { + "name": "varchar" + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "list", + "type": { + "name": "varchar" + } + } + } + ] + } +] diff --git a/internal/engine/mssql/convert.go b/internal/engine/mssql/convert.go index bdef335bc6..267c25fb58 100644 --- a/internal/engine/mssql/convert.go +++ b/internal/engine/mssql/convert.go @@ -1,11 +1,14 @@ package mssql import ( + "math" "strconv" "strings" + "unicode/utf16" tsql "github.com/sqlc-dev/teesql/ast" + "github.com/sqlc-dev/sqlc/internal/core" "github.com/sqlc-dev/sqlc/internal/sql/ast" ) @@ -17,6 +20,10 @@ type cc struct { // toByte maps a teesql UTF-16 code-unit offset to a byte offset in the // source, so Location fields agree with StmtLocation/StmtLen. toByte func(int) int + // fromExprs are expressions a FROM item holds that the AST has no place + // for, as FOR SYSTEM_TIME AS OF @t does, analyzed with the query's WHERE + // clause so a placeholder in them is still a parameter. + fromExprs []ast.Node } // loc returns a node's start offset in bytes, for the Location fields of the @@ -63,6 +70,8 @@ func (c *cc) convert(node tsql.Node) ast.Node { return c.convertAlterTableAddTableElementStatement(n) case *tsql.AlterTableDropTableElementStatement: return c.convertAlterTableDropTableElementStatement(n) + case *tsql.CreateFunctionStatement: + return c.convertCreateFunctionStatement(n) case *tsql.CreateTypeUddtStatement: return c.convertCreateTypeUddtStatement(n) default: @@ -97,6 +106,17 @@ func (c *cc) convertSelectStatement(n *tsql.SelectStatement) ast.Node { if !ok { return stmt } + // A query's own FOR JSON or FOR XML result is a column with the name + // SQL Server gives it, and FOR XML is ntext there. + if qs, ok := n.QueryExpression.(*tsql.QuerySpecification); ok { + if tn := forClauseType(qs.ForClause, true); tn != nil { + res := sel.TargetList.Items[0].(*ast.ResTarget) + res.Val.(*ast.TypeCast).TypeName = nullableTypeName(tn) + if name := forClauseColumn(qs.ForClause, tn); name != "" { + res.Name = &name + } + } + } if with := c.convertWithClause(n.WithCtesAndXmlNamespaces); with != nil { sel.WithClause = with } @@ -107,9 +127,12 @@ func (c *cc) convertWithClause(n *tsql.WithCtesAndXmlNamespaces) *ast.WithClause if n == nil || len(n.CommonTableExpressions) == 0 { return nil } + // T-SQL has no RECURSIVE keyword: a common table expression that names + // itself is recursive. with := &ast.WithClause{ - Ctes: &ast.List{}, - Location: c.loc(n), + Ctes: &ast.List{}, + Recursive: true, + Location: c.loc(n), } for _, cte := range n.CommonTableExpressions { name := identifierValue(cte.ExpressionName) @@ -186,16 +209,26 @@ func (c *cc) convertQuerySpecification(n *tsql.QuerySpecification) *ast.SelectSt } } + outerFromExprs := c.fromExprs + c.fromExprs = nil if n.FromClause != nil { stmt.FromClause = &ast.List{} for _, ref := range n.FromClause.TableReferences { stmt.FromClause.Items = append(stmt.FromClause.Items, c.convertTableReference(ref)) } } + fromExprs := c.fromExprs + c.fromExprs = outerFromExprs if n.WhereClause != nil { stmt.WhereClause = c.convertBooleanExpression(n.WhereClause.SearchCondition) } + if len(fromExprs) > 0 { + if stmt.WhereClause != nil { + fromExprs = append([]ast.Node{stmt.WhereClause}, fromExprs...) + } + stmt.WhereClause = &ast.BoolExpr{Boolop: ast.BoolExprTypeAnd, Args: &ast.List{Items: fromExprs}} + } if n.GroupByClause != nil { stmt.GroupClause = &ast.List{} @@ -233,9 +266,63 @@ func (c *cc) convertQuerySpecification(n *tsql.QuerySpecification) *ast.SelectSt stmt.DistinctClause = &ast.List{} } + if tn := forClauseType(n.ForClause, false); tn != nil { + return forClauseResult(stmt, tn, c.loc(n)) + } return stmt } +// forClauseResult is a query with FOR JSON or FOR XML as what it returns: a +// single nullable column of the document its rows make. The query itself is +// kept as a subquery, so its placeholders and stars are still analyzed. +func forClauseResult(query *ast.SelectStmt, tn *ast.TypeName, loc int) *ast.SelectStmt { + return &ast.SelectStmt{TargetList: &ast.List{Items: []ast.Node{&ast.ResTarget{ + Val: &ast.TypeCast{ + Arg: &ast.SubLink{SubLinkType: ast.EXPR_SUBLINK, Subselect: query, Location: loc}, + TypeName: nullableTypeName(tn), + Location: loc, + }, + Location: loc, + }}}} +} + +// forClauseType is the type of the document FOR JSON or FOR XML makes, or +// nil for any other FOR clause. FOR XML without TYPE is ntext as a query's +// result but nvarchar(max) as a subquery's. +func forClauseType(fc tsql.ForClause, topLevel bool) *ast.TypeName { + maxText := &ast.TypeName{Name: "nvarchar", Typmods: appendTypmod(nil, &ast.String{Str: "max"})} + switch f := fc.(type) { + case *tsql.JsonForClause: + return maxText + case *tsql.XmlForClause: + for _, o := range f.Options { + if o.OptionKind == "Type" { + return &ast.TypeName{Name: "xml"} + } + } + if topLevel { + return &ast.TypeName{Name: "ntext"} + } + return maxText + } + return nil +} + +// forClauseColumn is the name SQL Server gives the column FOR JSON or FOR +// XML returns, which XML with TYPE leaves unnamed. +func forClauseColumn(fc tsql.ForClause, tn *ast.TypeName) string { + switch { + case tn.Name == "xml": + return "" + case fc != nil: + if _, ok := fc.(*tsql.JsonForClause); ok { + return identifier("JSON_F52E2B61-18A1-11d1-B105-00805F49916B") + } + return identifier("XML_F52E2B61-18A1-11d1-B105-00805F49916B") + } + return "" +} + func (c *cc) convertSelectElement(elem tsql.SelectElement) ast.Node { switch e := elem.(type) { case *tsql.SelectScalarExpression: @@ -311,6 +398,30 @@ func (c *cc) convertTableReference(ref tsql.TableReference) ast.Node { alias := identifierValue(t.Alias) rv.Alias = &ast.Alias{Aliasname: &alias} } + // The times of FOR SYSTEM_TIME are the datetime2 SQL Server takes. + if tc := t.TemporalClause; tc != nil { + for _, e := range []tsql.ScalarExpression{tc.StartTime, tc.EndTime} { + if e != nil { + c.fromExprs = append(c.fromExprs, &ast.TypeCast{Arg: c.convertScalarExpression(e), TypeName: typeName("datetime2", 7)}) + } + } + } + // TABLESAMPLE takes a percentage as a float and a count of rows + // or a REPEATABLE seed as a bigint. + if ts := t.TableSampleClause; ts != nil { + size := "float" + if ts.TableSampleClauseOption == "Rows" { + size = "bigint" + } + for _, e := range []struct { + expr tsql.ScalarExpression + typ string + }{{ts.SampleNumber, size}, {ts.RepeatSeed, "bigint"}} { + if e.expr != nil { + c.fromExprs = append(c.fromExprs, &ast.TypeCast{Arg: c.convertScalarExpression(e.expr), TypeName: &ast.TypeName{Name: e.typ}}) + } + } + } return rv case *tsql.QueryDerivedTable: sub := &ast.RangeSubselect{ @@ -321,6 +432,8 @@ func (c *cc) convertTableReference(ref tsql.TableReference) ast.Node { sub.Alias = &ast.Alias{Aliasname: &alias} } return sub + case *tsql.InlineDerivedTable: + return c.convertInlineDerivedTable(t) case *tsql.QualifiedJoin: join := &ast.JoinExpr{ Larg: c.convertTableReference(t.FirstTableReference), @@ -341,19 +454,210 @@ func (c *cc) convertTableReference(ref tsql.TableReference) ast.Node { } return join case *tsql.UnqualifiedJoin: - if t.UnqualifiedJoinType != "CrossJoin" { - return todo(ref) + // APPLY is a lateral join: its right side may refer to the left, + // which the analyzer allows for every FROM item bound after another. + // OUTER APPLY keeps unmatched left rows, as a left join does. + join := &ast.JoinExpr{ + Larg: c.convertTableReference(t.FirstTableReference), + Rarg: c.convertTableReference(t.SecondTableReference), } - return &ast.JoinExpr{ - Jointype: ast.JoinTypeInner, - Larg: c.convertTableReference(t.FirstTableReference), - Rarg: c.convertTableReference(t.SecondTableReference), + switch t.UnqualifiedJoinType { + case "CrossJoin", "CrossApply": + join.Jointype = ast.JoinTypeInner + case "OuterApply": + join.Jointype = ast.JoinTypeLeft + default: + return todo(ref) } + return join + case *tsql.OpenJsonTableReference: + return c.convertOpenJson(t) + case *tsql.GlobalFunctionTableReference: + return c.convertGlobalFunctionTableReference(t) + case *tsql.SchemaObjectFunctionTableReference: + if t.SchemaObject == nil || t.SchemaObject.BaseIdentifier == nil { + return todo(t) + } + return c.rangeFunction(t, identifierValue(t.SchemaObject.BaseIdentifier), t.Parameters, t.Alias, nil) + case *tsql.PivotedTableReference: + return c.convertPivot(t) + case *tsql.UnpivotedTableReference: + return c.convertUnpivot(t) default: return todo(ref) } } +// convertPivot converts PIVOT. Its columns depend on those of its source, +// which only the analyzer knows, so it is passed on rather than rewritten +// as the grouped query it stands for. +func (c *cc) convertPivot(n *tsql.PivotedTableReference) ast.Node { + if n.AggregateFunctionIdentifier == nil || len(n.AggregateFunctionIdentifier.Identifiers) == 0 || n.PivotColumn == nil { + return todo(n) + } + ids := n.AggregateFunctionIdentifier.Identifiers + agg := &ast.FuncCall{ + Funcname: &ast.List{Items: []ast.Node{&ast.String{Str: identifierValue(ids[len(ids)-1])}}}, + Args: &ast.List{}, + Location: c.loc(n), + } + for _, col := range n.ValueColumns { + agg.Args.Items = append(agg.Args.Items, c.convertColumnReference(col)) + } + p := &ast.RangePivot{ + Source: c.convertTableReference(n.TableReference), + Aggregate: agg, + ForColumn: columnName(n.PivotColumn), + InList: &ast.List{}, + } + for _, id := range n.InColumns { + p.InList.Items = append(p.InList.Items, NewIdentifier(id.Value)) + } + if n.Alias != nil { + alias := identifierValue(n.Alias) + p.Alias = &ast.Alias{Aliasname: &alias} + } + return p +} + +// convertUnpivot converts UNPIVOT, which, as PIVOT, keeps the columns of +// its source the analyzer knows. +func (c *cc) convertUnpivot(n *tsql.UnpivotedTableReference) ast.Node { + p := &ast.RangePivot{ + Source: c.convertTableReference(n.TableReference), + Unpivot: true, + ValueColumn: identifierValue(n.ValueColumn), + ForColumn: identifierValue(n.PivotColumn), + ForType: typeName("nvarchar", 128), + InList: &ast.List{}, + } + for _, col := range n.InColumns { + p.InList.Items = append(p.InList.Items, &ast.String{Str: columnName(col)}) + } + if n.Alias != nil { + alias := identifierValue(n.Alias) + p.Alias = &ast.Alias{Aliasname: &alias} + } + return p +} + +// columnName is the name a column reference ends in. +func columnName(n *tsql.ColumnReferenceExpression) string { + if n == nil || n.MultiPartIdentifier == nil || len(n.MultiPartIdentifier.Identifiers) == 0 { + return "" + } + ids := n.MultiPartIdentifier.Identifiers + return identifierValue(ids[len(ids)-1]) +} + +// convertInlineDerivedTable converts (VALUES ...) AS v(a, b) to the +// subquery it stands for, its rows selected one after another with UNION +// ALL, so the relation's columns are typed from its first row. +func (c *cc) convertInlineDerivedTable(n *tsql.InlineDerivedTable) ast.Node { + var query *ast.SelectStmt + for _, row := range n.RowValues { + sel := &ast.SelectStmt{TargetList: &ast.List{}} + for _, val := range row.ColumnValues { + sel.TargetList.Items = append(sel.TargetList.Items, &ast.ResTarget{ + Val: c.convertScalarExpression(val), + Location: c.loc(val), + }) + } + if query == nil { + query = sel + continue + } + query = &ast.SelectStmt{Op: ast.Union, All: true, Larg: query, Rarg: sel} + } + if query == nil { + return todo(n) + } + sub := &ast.RangeSubselect{Subquery: query} + if n.Alias != nil { + alias := identifierValue(n.Alias) + sub.Alias = &ast.Alias{Aliasname: &alias, Colnames: &ast.List{}} + for _, col := range n.Columns { + sub.Alias.Colnames.Items = append(sub.Alias.Colnames.Items, NewIdentifier(col.Value)) + } + } + return sub +} + +// rangeFunction is a call in FROM to a function returning rows of the +// columns defs declares, or of its own columns when there are none. +func (c *cc) rangeFunction(n tsql.Node, name string, args []tsql.ScalarExpression, alias *tsql.Identifier, defs []ast.Node) *ast.RangeFunction { + call := &ast.FuncCall{ + Funcname: &ast.List{Items: []ast.Node{&ast.String{Str: name}}}, + Location: c.loc(n), + } + for _, arg := range args { + if call.Args == nil { + call.Args = &ast.List{} + } + call.Args.Items = append(call.Args.Items, c.convertScalarExpression(arg)) + } + rf := &ast.RangeFunction{ + Functions: &ast.List{Items: []ast.Node{&ast.List{Items: []ast.Node{call}}}}, + } + if len(defs) > 0 { + rf.Coldeflist = &ast.List{Items: defs} + } + if alias != nil { + name := identifierValue(alias) + rf.Alias = &ast.Alias{Aliasname: &name} + } + return rf +} + +// convertOpenJson converts OPENJSON, whose rows are the key, value and type +// of each JSON element, or the columns its WITH clause declares. +func (c *cc) convertOpenJson(n *tsql.OpenJsonTableReference) ast.Node { + args := []tsql.ScalarExpression{n.Variable} + if n.RowPattern != nil { + args = append(args, n.RowPattern) + } + var defs []ast.Node + if len(n.SchemaDeclarationItems) == 0 { + defs = []ast.Node{ + &ast.ColumnDef{Colname: "key", TypeName: typeName("nvarchar", 4000), IsNotNull: true}, + &ast.ColumnDef{Colname: "value", TypeName: &ast.TypeName{Name: "nvarchar", Typmods: appendTypmod(nil, &ast.String{Str: "max"})}}, + &ast.ColumnDef{Colname: "type", TypeName: &ast.TypeName{Name: "tinyint"}, IsNotNull: true}, + } + } + for _, item := range n.SchemaDeclarationItems { + if item.ColumnDefinition == nil { + continue + } + defs = append(defs, &ast.ColumnDef{ + Colname: identifierValue(item.ColumnDefinition.ColumnIdentifier), + TypeName: dataTypeName(item.ColumnDefinition.DataType), + }) + } + return c.rangeFunction(n, "openjson", args, n.Alias, defs) +} + +// convertGlobalFunctionTableReference converts STRING_SPLIT and +// GENERATE_SERIES. Each returns a value of its seeded result type, and +// STRING_SPLIT with enable_ordinal set adds the ordinal of each. +func (c *cc) convertGlobalFunctionTableReference(n *tsql.GlobalFunctionTableReference) ast.Node { + name := identifierValue(n.Name) + defs := []ast.Node{&ast.ColumnDef{Colname: "value"}} + switch name { + case "string_split": + if len(n.Parameters) == 3 { + defs = append(defs, &ast.ColumnDef{Colname: "ordinal", TypeName: &ast.TypeName{Name: "bigint"}, IsNotNull: true}) + } + case "generate_series": + default: + return todo(n) + } + return c.rangeFunction(n, name, n.Parameters, n.Alias, defs) +} + +func typeName(name string, length int64) *ast.TypeName { + return &ast.TypeName{Name: name, Typmods: appendTypmod(nil, &ast.A_Const{Val: &ast.Integer{Ival: length}})} +} + func (c *cc) convertBooleanExpression(expr tsql.BooleanExpression) ast.Node { if expr == nil { return nil @@ -447,6 +751,18 @@ func (c *cc) convertBooleanExpression(expr tsql.BooleanExpression) ast.Node { } case *tsql.BooleanScalarPlaceholder: return c.convertScalarExpression(e.Scalar) + case *tsql.FullTextPredicate: + // CONTAINS and FREETEXT are the predicates the dialect seeds under + // their names, over their search condition and language term. + call := &ast.FuncCall{ + Funcname: &ast.List{Items: []ast.Node{&ast.String{Str: identifier(e.FullTextFunctionType)}}}, + Args: &ast.List{Items: []ast.Node{c.convertScalarExpression(e.Value)}}, + Location: c.loc(e), + } + if e.LanguageTerm != nil { + call.Args.Items = append(call.Args.Items, c.convertScalarExpression(e.LanguageTerm)) + } + return call default: return todo(expr) } @@ -481,16 +797,16 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { case *tsql.VariableReference: return c.convertVariableReference(e) case *tsql.IntegerLiteral: - ival, _ := strconv.ParseInt(e.Value, 10, 64) + ival, err := strconv.ParseInt(e.Value, 10, 64) + if err != nil || ival > math.MaxInt32 { + return c.numericLiteral(e.Value, e) + } return &ast.A_Const{ Val: &ast.Integer{Ival: ival}, Location: c.loc(e), } case *tsql.NumericLiteral: - return &ast.A_Const{ - Val: &ast.Float{Str: e.Value}, - Location: c.loc(e), - } + return c.numericLiteral(e.Value, e) case *tsql.RealLiteral: return &ast.A_Const{ Val: &ast.Float{Str: e.Value}, @@ -502,10 +818,15 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { Location: c.loc(e), } case *tsql.StringLiteral: - return &ast.A_Const{ + lit := &ast.A_Const{ Val: &ast.String{Str: e.Value}, Location: c.loc(e), } + // N'...' is an nvarchar as long as the string, and at least 1. + if e.IsNational { + return &ast.TypeCast{Arg: lit, TypeName: typeName("nvarchar", max(1, int64(len(utf16.Encode([]rune(e.Value)))))), Location: c.loc(e)} + } + return lit case *tsql.NullLiteral: return &ast.A_Const{ Val: &ast.Null{}, @@ -555,11 +876,11 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { Location: c.loc(e), } case *tsql.TryCastCall: - return &ast.TypeCast{ - Arg: c.convertScalarExpression(e.Parameter), - TypeName: dataTypeName(e.DataType), - Location: c.loc(e), - } + return c.tryCast(e.Parameter, e.DataType, e) + case *tsql.ParseCall: + return c.parseCall(e.StringValue, e.Culture, e.DataType, false, e) + case *tsql.TryParseCall: + return c.parseCall(e.StringValue, e.Culture, e.DataType, true, e) case *tsql.ConvertCall: return &ast.TypeCast{ Arg: c.convertScalarExpression(e.Parameter), @@ -567,11 +888,7 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { Location: c.loc(e), } case *tsql.TryConvertCall: - return &ast.TypeCast{ - Arg: c.convertScalarExpression(e.Parameter), - TypeName: dataTypeName(e.DataType), - Location: c.loc(e), - } + return c.tryCast(e.Parameter, e.DataType, e) case *tsql.CoalesceExpression: coalesce := &ast.CoalesceExpr{ Args: &ast.List{}, @@ -644,6 +961,15 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { Subselect: c.convertQueryExpression(e.QueryExpression), Location: c.loc(e), } + case *tsql.GlobalVariableExpression: + // @@ROWCOUNT and its relatives are typed as the functions the + // dialect seeds under their names. + return &ast.FuncCall{ + Funcname: &ast.List{ + Items: []ast.Node{&ast.String{Str: identifier(e.Name)}}, + }, + Location: c.loc(e), + } case *tsql.ParameterlessCall: return &ast.FuncCall{ Funcname: &ast.List{ @@ -656,6 +982,57 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { } } +// tryCast models the result type of TRY_CAST and TRY_CONVERT. The source SQL +// is unchanged; a TRY_CONVERT style affects parsing, not the result type. +// Failed conversions can yield NULL, so results from non-parameter operands +// are nullable. A placeholder is typed by the cast as it is by CAST. +func (c *cc) tryCast(arg tsql.ScalarExpression, dataType tsql.DataTypeReference, n tsql.Node) ast.Node { + return &ast.TypeCast{ + Arg: c.convertScalarExpression(arg), + TypeName: tryTypeName(arg, dataType), + Location: c.loc(n), + } +} + +// tryTypeName is the type a TRY_ conversion of arg yields: nullable, unless +// arg is a placeholder, which the conversion types as CAST does. +func tryTypeName(arg tsql.ScalarExpression, dataType tsql.DataTypeReference) *ast.TypeName { + if _, ok := arg.(*tsql.VariableReference); ok { + return dataTypeName(dataType) + } + return nullableTypeName(dataTypeName(dataType)) +} + +func nullableTypeName(tn *ast.TypeName) *ast.TypeName { + if t := core.TypeExprOfTypeName(tn); t != nil { + return &ast.TypeName{Spelling: "Nullable(" + t.String() + ")"} + } + return tn +} + +// parseCall converts PARSE and TRY_PARSE, which convert a string as CAST +// and TRY_CAST do. What they convert is a string, so a placeholder there is +// the nvarchar(4000) SQL Server takes it as. A culture is passed through a +// call, so a placeholder given for it is still a parameter of the query. +func (c *cc) parseCall(value, culture tsql.ScalarExpression, dataType tsql.DataTypeReference, try bool, n tsql.Node) ast.Node { + tn := dataTypeName(dataType) + if try { + tn = nullableTypeName(tn) + } + arg := c.convertScalarExpression(value) + if _, ok := value.(*tsql.VariableReference); ok { + arg = &ast.TypeCast{Arg: arg, TypeName: typeName("nvarchar", 4000), Location: c.loc(value)} + } + if culture != nil { + arg = &ast.FuncCall{ + Funcname: &ast.List{Items: []ast.Node{&ast.String{Str: "parse"}}}, + Args: &ast.List{Items: []ast.Node{arg, c.convertScalarExpression(culture)}}, + Location: c.loc(n), + } + } + return &ast.TypeCast{Arg: arg, TypeName: tn, Location: c.loc(n)} +} + func binaryOperator(binaryExpressionType string) string { switch binaryExpressionType { case "Add": @@ -763,36 +1140,20 @@ func (c *cc) convertFunctionCall(n *tsql.FunctionCall) *ast.FuncCall { } // convertOutputClause converts an OUTPUT clause to a returning list. The -// INSERTED and DELETED pseudo-table qualifiers are stripped so the columns -// resolve against the statement's target table. +// dialect names INSERTED and DELETED as the target's aliases there, so the +// qualifiers are kept, and a star they qualify expands to columns they +// qualify too. func (c *cc) convertOutputClause(n *tsql.OutputClause) *ast.List { if n == nil || len(n.SelectColumns) == 0 { return nil } list := &ast.List{} for _, elem := range n.SelectColumns { - target := c.convertSelectElement(elem) - if res, ok := target.(*ast.ResTarget); ok { - stripOutputQualifier(res.Val) - } - list.Items = append(list.Items, target) + list.Items = append(list.Items, c.convertSelectElement(elem)) } return list } -func stripOutputQualifier(node ast.Node) { - ref, ok := node.(*ast.ColumnRef) - if !ok || ref.Fields == nil || len(ref.Fields.Items) < 2 { - return - } - if first, ok := ref.Fields.Items[0].(*ast.String); ok { - switch first.Str { - case "inserted", "deleted": - ref.Fields.Items = ref.Fields.Items[1:] - } - } -} - func (c *cc) convertInsertStatement(n *tsql.InsertStatement) ast.Node { if n.InsertSpecification == nil { return todo(n) @@ -805,7 +1166,8 @@ func (c *cc) convertInsertStatement(n *tsql.InsertStatement) ast.Node { } stmt := &ast.InsertStmt{ - Relation: c.parseRangeVar(target.SchemaObject), + Relation: c.parseRangeVar(target.SchemaObject), + WithClause: c.convertWithClause(n.WithCtesAndXmlNamespaces), } if len(spec.Columns) > 0 { @@ -1184,6 +1546,97 @@ func (c *cc) convertAlterTableAddTableElementStatement(n *tsql.AlterTableAddTabl return stmt } +// convertCreateFunctionStatement records a function. A scalar function's +// result SQL Server reports nullable whatever its arguments are. A +// table-valued function returns rows of table(col type, ...), the columns +// it declares; an inline one's are those its query selects, which the +// schema works out from the query, passed as the "query" option. +func (c *cc) convertCreateFunctionStatement(n *tsql.CreateFunctionStatement) ast.Node { + if n.Name == nil || n.Name.BaseIdentifier == nil { + return todo(n) + } + var returns *ast.TypeName + var options *ast.List + switch ret := n.ReturnType.(type) { + case *tsql.ScalarFunctionReturnType: + if ret.DataType == nil { + return todo(n) + } + returns = nullableTypeName(dataTypeName(ret.DataType)) + case *tsql.TableValuedFunctionReturnType: + if ret.DeclareTableVariableBody == nil || ret.DeclareTableVariableBody.Definition == nil { + return todo(n) + } + var cols []string + for _, col := range ret.DeclareTableVariableBody.Definition.ColumnDefinitions { + def := c.convertColumnDefinition(col, nil) + t := def.TypeName + if !def.IsNotNull { + t = nullableTypeName(t) + } + spelled := core.TypeExprOfTypeName(t) + if spelled == nil { + return todo(n) + } + cols = append(cols, def.Colname+" "+typeSpelling(t, spelled)) + } + returns = &ast.TypeName{Spelling: "table(" + strings.Join(cols, ", ") + ")", Setof: true} + case *tsql.SelectFunctionReturnType: + if ret.SelectStatement == nil { + return todo(n) + } + returns = &ast.TypeName{Name: "table", Setof: true} + query := "query" + options = &ast.List{Items: []ast.Node{&ast.DefElem{Defname: &query, Arg: c.convertSelectStatement(ret.SelectStatement)}}} + default: + return todo(n) + } + tn := parseTableName(n.Name) + params := &ast.List{} + for _, p := range n.Parameters { + if p.VariableName == nil || p.DataType == nil { + continue + } + name := strings.TrimPrefix(p.VariableName.Value, "@") + param := &ast.FuncParam{Name: &name, Type: dataTypeName(p.DataType)} + if p.Value != nil { + param.DefExpr = c.convertScalarExpression(p.Value) + } + params.Items = append(params.Items, param) + } + return &ast.CreateFunctionStmt{ + Func: &ast.FuncName{Schema: tn.Schema, Name: tn.Name}, + Params: params, + ReturnType: returns, + Options: options, + } +} + +// numericLiteral is a number written with a decimal point, or too large +// for an int, which SQL Server types as the decimal holding its digits: +// 1.50 is a decimal(3,2), 2147483648 a decimal(10,0). +func (c *cc) numericLiteral(value string, n tsql.Node) ast.Node { + whole, frac, _ := strings.Cut(value, ".") + whole = strings.TrimLeft(whole, "0") + scale := int64(len(frac)) + precision := max(int64(len(whole))+scale, 1) + return &ast.TypeCast{ + Arg: &ast.A_Const{Val: &ast.Float{Str: value}, Location: c.loc(n)}, + TypeName: &ast.TypeName{Name: "decimal", Typmods: appendTypmod(appendTypmod(nil, + &ast.A_Const{Val: &ast.Integer{Ival: precision}}), &ast.A_Const{Val: &ast.Integer{Ival: scale}})}, + Location: c.loc(n), + } +} + +// typeSpelling spells a type so that it parses back as the same type, which +// for a nullable one is the Nullable(T) it was spelled as. +func typeSpelling(tn *ast.TypeName, t *core.TypeExpr) string { + if tn.Spelling != "" { + return tn.Spelling + } + return t.String() +} + func (c *cc) convertAlterTableDropTableElementStatement(n *tsql.AlterTableDropTableElementStatement) ast.Node { if n.SchemaObjectName == nil { return todo(n) diff --git a/internal/engine/mssql/dialect/dialect.json b/internal/engine/mssql/dialect/dialect.json index 30ea408744..50042e966e 100644 --- a/internal/engine/mssql/dialect/dialect.json +++ b/internal/engine/mssql/dialect/dialect.json @@ -8,6 +8,10 @@ "bool": "bit" }, "bool": "bit", + "limit": "bigint", + "outer_join_nullable": true, + "recursive_cte_nullable": true, + "returning_aliases": ["inserted", "deleted"], "comparison": ["=", "<>", "!=", "<", "<=", ">", ">="], "comparison_categories": "NBSD", "arithmetic": ["+", "-", "*", "/", "%"], diff --git a/internal/engine/mssql/dialect/functions.jsonl b/internal/engine/mssql/dialect/functions.jsonl index e0cd45408c..99167607ec 100644 --- a/internal/engine/mssql/dialect/functions.jsonl +++ b/internal/engine/mssql/dialect/functions.jsonl @@ -74,3 +74,39 @@ {"name": "rtrim", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} {"name": "trim", "args": [{"type": "varchar"}], "returns": "varchar"} {"name": "trim", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "openjson", "args": [{"type": "nvarchar"}], "returns": "nvarchar", "nullable": true} +{"name": "openjson", "args": [{"type": "nvarchar"}, {"type": "nvarchar"}], "returns": "nvarchar", "nullable": true} +{"name": "string_split", "args": [{"type": "nvarchar"}, {"type": "nvarchar"}], "returns": "$1"} +{"name": "string_split", "args": [{"type": "varchar"}, {"type": "varchar"}], "returns": "$1"} +{"name": "string_split", "args": [{"type": "nvarchar"}, {"type": "nvarchar"}, {"type": "int"}], "returns": "$1"} +{"name": "string_split", "args": [{"type": "varchar"}, {"type": "varchar"}, {"type": "int"}], "returns": "$1"} +{"name": "generate_series", "args": [{"type": "int"}, {"type": "int"}], "returns": "$1"} +{"name": "generate_series", "args": [{"type": "bigint"}, {"type": "bigint"}], "returns": "$1"} +{"name": "generate_series", "args": [{"type": "decimal"}, {"type": "decimal"}], "returns": "$1"} +{"name": "generate_series", "args": [{"type": "int"}, {"type": "int"}, {"type": "int"}], "returns": "$1"} +{"name": "generate_series", "args": [{"type": "bigint"}, {"type": "bigint"}, {"type": "bigint"}], "returns": "$1"} +{"name": "generate_series", "args": [{"type": "decimal"}, {"type": "decimal"}, {"type": "decimal"}], "returns": "$1"} +{"name": "parse", "args": [{"type": "nvarchar"}, {"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "@@rowcount", "returns": "int"} +{"name": "@@identity", "returns": "numeric(38, 0)", "nullable": true} +{"name": "@@spid", "returns": "smallint"} +{"name": "@@version", "returns": "nvarchar(300)", "nullable": true} +{"name": "@@trancount", "returns": "int"} +{"name": "@@error", "returns": "int"} +{"name": "@@fetch_status", "returns": "int"} +{"name": "@@servername", "returns": "nvarchar(128)", "nullable": true} +{"name": "@@servicename", "returns": "nvarchar(128)", "nullable": true} +{"name": "@@language", "returns": "nvarchar(128)", "nullable": true} +{"name": "@@datefirst", "returns": "tinyint", "nullable": true} +{"name": "@@max_precision", "returns": "tinyint"} +{"name": "@@nestlevel", "returns": "int"} +{"name": "@@procid", "returns": "int"} +{"name": "@@dbts", "returns": "varbinary(8)"} +{"name": "@@lock_timeout", "returns": "int"} +{"name": "@@options", "returns": "int"} +{"name": "@@textsize", "returns": "int"} +{"name": "@@cursor_rows", "returns": "int"} +{"name": "contains", "args": [{"type": "nvarchar(4000)"}], "returns": "bit"} +{"name": "contains", "args": [{"type": "nvarchar(4000)"}, {"type": "any"}], "returns": "bit"} +{"name": "freetext", "args": [{"type": "nvarchar(4000)"}], "returns": "bit"} +{"name": "freetext", "args": [{"type": "nvarchar(4000)"}, {"type": "any"}], "returns": "bit"} diff --git a/internal/sql/ast/range_pivot.go b/internal/sql/ast/range_pivot.go new file mode 100644 index 0000000000..f3d7b7a761 --- /dev/null +++ b/internal/sql/ast/range_pivot.go @@ -0,0 +1,56 @@ +package ast + +import "github.com/sqlc-dev/sqlc/internal/sql/format" + +// RangePivot is a FROM item that rotates the rows of another, as SQL +// Server's PIVOT and UNPIVOT do. PIVOT turns the values of ForColumn named +// in InList into columns of Aggregate; UNPIVOT turns the columns in InList +// into rows of ValueColumn, named in ForColumn of type ForType. +type RangePivot struct { + Tag NodeTag[RangePivot] `json:"tag"` + + Source Node `json:"source,omitempty"` + Unpivot bool `json:"unpivot"` + Aggregate *FuncCall `json:"aggregate,omitempty"` + ValueColumn string `json:"value_column"` + ForColumn string `json:"for_column"` + ForType *TypeName `json:"for_type,omitempty"` + InList *List `json:"in_list,omitempty"` + Alias *Alias `json:"alias,omitempty"` +} + +func (n *RangePivot) Pos() int { + return 0 +} + +func (n *RangePivot) Format(buf *TrackedBuffer, d format.Dialect) { + if n == nil { + return + } + buf.astFormat(n.Source, d) + if n.Unpivot { + buf.WriteString(" UNPIVOT (") + buf.WriteString(d.QuoteIdent(n.ValueColumn)) + } else { + buf.WriteString(" PIVOT (") + buf.astFormat(n.Aggregate, d) + } + buf.WriteString(" FOR ") + buf.WriteString(d.QuoteIdent(n.ForColumn)) + buf.WriteString(" IN (") + if n.InList != nil { + for i, item := range n.InList.Items { + if i > 0 { + buf.WriteString(", ") + } + if s, ok := item.(*String); ok { + buf.WriteString(d.QuoteIdent(s.Str)) + } + } + } + buf.WriteString("))") + if n.Alias != nil { + buf.WriteString(" AS ") + buf.astFormat(n.Alias, d) + } +} diff --git a/internal/sql/astutils/rewrite.go b/internal/sql/astutils/rewrite.go index 25b274a16c..d178de1893 100644 --- a/internal/sql/astutils/rewrite.go +++ b/internal/sql/astutils/rewrite.go @@ -967,6 +967,12 @@ func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast. a.apply(n, "Alias", nil, n.Alias) a.apply(n, "Coldeflist", nil, n.Coldeflist) + case *ast.RangePivot: + a.apply(n, "Source", nil, n.Source) + a.apply(n, "Aggregate", nil, n.Aggregate) + a.apply(n, "InList", nil, n.InList) + a.apply(n, "Alias", nil, n.Alias) + case *ast.RangeSubselect: a.apply(n, "Subquery", nil, n.Subquery) a.apply(n, "Alias", nil, n.Alias) diff --git a/internal/sql/astutils/walk.go b/internal/sql/astutils/walk.go index 6d5e80bdc3..dcd5119df5 100644 --- a/internal/sql/astutils/walk.go +++ b/internal/sql/astutils/walk.go @@ -1615,6 +1615,20 @@ func Walk(f Visitor, node ast.Node) { Walk(f, n.Coldeflist) } + case *ast.RangePivot: + if n.Source != nil { + Walk(f, n.Source) + } + if n.Aggregate != nil { + Walk(f, n.Aggregate) + } + if n.InList != nil { + Walk(f, n.InList) + } + if n.Alias != nil { + Walk(f, n.Alias) + } + case *ast.RangeSubselect: if n.Subquery != nil { Walk(f, n.Subquery) diff --git a/internal/sql/preprocess/CLAUDE.md b/internal/sql/preprocess/CLAUDE.md index a7f4d9aafe..cf468c91c0 100644 --- a/internal/sql/preprocess/CLAUDE.md +++ b/internal/sql/preprocess/CLAUDE.md @@ -20,6 +20,7 @@ Native placeholders by dialect: | postgresql | `$1` | sqlc syntax | | mysql | `?` | user variable, left alone | | sqlite | `?1` | sqlc syntax | +| mssql | `@name` | native, kept as written; names compare without regard to case | GoogleSQL and ClickHouse are deliberately absent. They handle their own parameter syntax, so `File` returns their source unchanged and sqlc syntax is @@ -30,9 +31,9 @@ looks like. `lexer` is a dialect-parameterized scanner. It does not parse SQL — it knows only enough to skip the regions where sqlc syntax must not be rewritten: -comments (`--`, `/* */`, `#`), string literals, quoted identifiers, backticks -and PostgreSQL dollar-quoted strings. `Dialect` (dialect.go) is the single -place those lexical rules live. +comments (`--`, `/* */`, `#`), string literals, quoted identifiers, backticks, +SQL Server brackets and PostgreSQL dollar-quoted strings. `Dialect` +(dialect.go) is the single place those lexical rules live. `File(engine, src)` looks up the dialect; an engine with no entry gets its source back untouched. Otherwise it walks the whole file, splits it at diff --git a/internal/sql/preprocess/dialect.go b/internal/sql/preprocess/dialect.go index 4d8d4ae85b..3ed78281dd 100644 --- a/internal/sql/preprocess/dialect.go +++ b/internal/sql/preprocess/dialect.go @@ -14,6 +14,8 @@ const ( StyleQuestion // StyleOrdinal numbers parameters as ?1, ?2, ... (SQLite) StyleOrdinal + // StyleAt names parameters as @name (SQL Server) + StyleAt ) // Dialect describes the lexical rules the preprocessor needs to walk a query @@ -42,6 +44,10 @@ type Dialect struct { // Backtick reports whether `ident` is a quoted identifier. Backtick bool + // Bracket reports whether [ident] is a quoted identifier, with ]] + // standing for a closing bracket in it. + Bracket bool + // HashComment reports whether # starts a line comment. HashComment bool @@ -88,6 +94,14 @@ var dialects = map[config.Engine]Dialect{ Backtick: true, Backslash: true, }, + // SQL Server's @name is already a named parameter, so sqlc.arg(name) + // becomes one. Its comments nest and its identifiers keep their case. + config.EngineMSSQL: { + Style: StyleAt, + AtSign: true, + Bracket: true, + NestedBlockComment: true, + }, // ClickHouse binds with an unnumbered ?, like MySQL, and its identifiers // keep their case. config.EngineClickHouse: { diff --git a/internal/sql/preprocess/lexer.go b/internal/sql/preprocess/lexer.go index d17d80b077..7c9568c3c2 100644 --- a/internal/sql/preprocess/lexer.go +++ b/internal/sql/preprocess/lexer.go @@ -53,6 +53,11 @@ func (l *lexer) skip(i int) int { return -1 } return l.skipQuoted(i, '`', false) + case '[': + if !l.d.Bracket { + return -1 + } + return l.skipQuoted(i, ']', false) case '-': if l.at(i+1) == '-' { return l.skipLine(i) diff --git a/internal/sql/preprocess/preprocess.go b/internal/sql/preprocess/preprocess.go index ad25bc9b2f..ff9c92880a 100644 --- a/internal/sql/preprocess/preprocess.go +++ b/internal/sql/preprocess/preprocess.go @@ -347,11 +347,22 @@ func number(d Dialect, occs []occurrence) *named.ParamSet { numbs[occ.number] = true } ps := named.NewParamSet(numbs, d.hasNamedSupport()) + // SQL Server compares parameter names without regard to case, so @Name + // and @name are one parameter, named by its first spelling. + spelling := map[string]string{} for i := range occs { occ := &occs[i] if occ.kind == kindEmbed || occ.kind == kindNative { continue } + if d.Style == StyleAt { + key := strings.ToLower(occ.name) + if first, ok := spelling[key]; ok { + occ.name = first + } else { + spelling[key] = occ.name + } + } occ.number = ps.Add(param(occ)) } return ps @@ -389,6 +400,15 @@ func (d Dialect) replacement(occ *occurrence) string { return fmt.Sprintf("/*SLICE:%s*/?", occ.name) } return fmt.Sprintf("?%d", occ.number) + case StyleAt: + if occ.kind == kindSlice { + return fmt.Sprintf("/*SLICE:%s*/@%s", occ.name, occ.name) + } + // A bare @name is already native; keep the spelling the user wrote. + if strings.HasPrefix(occ.ident, "@") { + return occ.ident + } + return "@" + occ.name default: if occ.kind == kindSlice { return fmt.Sprintf("/*SLICE:%s*/?", occ.name) diff --git a/internal/sql/preprocess/scan.go b/internal/sql/preprocess/scan.go index b2b4b1f534..9eede256f6 100644 --- a/internal/sql/preprocess/scan.go +++ b/internal/sql/preprocess/scan.go @@ -33,8 +33,15 @@ func (l *lexer) scan(start, end int) []occurrence { } i = j - case c == '@' && l.d.AtSign && isIdentStart(l.at(i+1)): + // A parameter starts with @ followed by an identifier. The second @ + // in a T-SQL system variable such as @@ROWCOUNT must not start one. + case c == '@' && l.d.AtSign && isIdentStart(l.at(i+1)) && l.at(i-1) != '@': j := l.identEnd(i + 1) + // T-SQL parameter names may also contain #, $, and @ after the + // first identifier character. + for l.d.Style == StyleAt && j < len(l.src) && (isIdentPart(l.src[j]) || strings.IndexByte("#$@", l.src[j]) >= 0) { + j++ + } out = append(out, occurrence{ kind: kindArg, start: i, @@ -138,11 +145,33 @@ func (l *lexer) sqlcCall(start, afterSqlc int) (occurrence, int, bool) { } return occ, occ.end, true } + // A dialect that binds @name writes the name into the query, so it has + // to read back as one parameter: @user-id would be a subtraction. An + // embed names a table, not a parameter. + if l.d.Style == StyleAt && fn != "embed" && !isParamName(name) { + occ.err = &sqlerr.Error{ + Message: fmt.Sprintf("parameter name %q passed to sqlc.%s is not a valid @name; use letters, digits and underscores", name, fn), + Location: start, + } + return occ, occ.end, true + } occ.name = name occ.ident = ident return occ, occ.end, true } +func isParamName(name string) bool { + if name == "" || !isIdentStart(name[0]) { + return false + } + for i := 1; i < len(name); i++ { + if !isIdentPart(name[i]) { + return false + } + } + return true +} + // argument interprets the single argument to a sqlc function. It returns the // parameter name and the text to reuse when rebuilding a reference, which keeps // any identifier quoting the user wrote. diff --git a/internal/sql/preprocess/testdata/mssql/arg/input.sql b/internal/sql/preprocess/testdata/mssql/arg/input.sql new file mode 100644 index 0000000000..c689beee0c --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/arg/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = sqlc.arg(x) AND b = sqlc.narg(y); diff --git a/internal/sql/preprocess/testdata/mssql/arg/output.sql b/internal/sql/preprocess/testdata/mssql/arg/output.sql new file mode 100644 index 0000000000..9dd09b1637 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/arg/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = @x AND b = @y; diff --git a/internal/sql/preprocess/testdata/mssql/arg/side_table.json b/internal/sql/preprocess/testdata/mssql/arg/side_table.json new file mode 100644 index 0000000000..a67d864ad8 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/arg/side_table.json @@ -0,0 +1,24 @@ +[ + { + "start": 0, + "end": 45, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "name": "x", + "named": true + }, + { + "number": 2, + "location": 42, + "origin": 51, + "name": "y", + "named": true, + "nullable": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/input.sql b/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/input.sql new file mode 100644 index 0000000000..f67f702a35 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = @Name AND b = sqlc.arg(name) AND c = @NAME; diff --git a/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/output.sql b/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/output.sql new file mode 100644 index 0000000000..145c90ded9 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = @Name AND b = @Name AND c = @NAME; diff --git a/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/side_table.json b/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/side_table.json new file mode 100644 index 0000000000..438b1d69e2 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/at_sign_keeps_spelling/side_table.json @@ -0,0 +1,30 @@ +[ + { + "start": 0, + "end": 65, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "name": "Name", + "named": true + }, + { + "number": 1, + "location": 45, + "origin": 45, + "name": "Name", + "named": true + }, + { + "number": 1, + "location": 59, + "origin": 68, + "name": "Name", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/embed/input.sql b/internal/sql/preprocess/testdata/mssql/embed/input.sql new file mode 100644 index 0000000000..34ec985f7f --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/embed/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed(users) FROM users WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/embed/output.sql b/internal/sql/preprocess/testdata/mssql/embed/output.sql new file mode 100644 index 0000000000..7493847494 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/embed/output.sql @@ -0,0 +1 @@ +SELECT users.* FROM users WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/embed/side_table.json b/internal/sql/preprocess/testdata/mssql/embed/side_table.json new file mode 100644 index 0000000000..fdd0a53517 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/embed/side_table.json @@ -0,0 +1,24 @@ +[ + { + "start": 0, + "end": 41, + "dollar": true, + "params": [ + { + "number": 1, + "location": 37, + "origin": 47, + "name": "id", + "named": true + } + ], + "embeds": [ + { + "table": "users", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(users)" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/embed_quoted_table/input.sql b/internal/sql/preprocess/testdata/mssql/embed_quoted_table/input.sql new file mode 100644 index 0000000000..1d03b5f520 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/embed_quoted_table/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed("order details") FROM [order details] WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/embed_quoted_table/output.sql b/internal/sql/preprocess/testdata/mssql/embed_quoted_table/output.sql new file mode 100644 index 0000000000..5554ca972b --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/embed_quoted_table/output.sql @@ -0,0 +1 @@ +SELECT "order details".* FROM [order details] WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/embed_quoted_table/side_table.json b/internal/sql/preprocess/testdata/mssql/embed_quoted_table/side_table.json new file mode 100644 index 0000000000..4baa50e0c2 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/embed_quoted_table/side_table.json @@ -0,0 +1,24 @@ +[ + { + "start": 0, + "end": 61, + "dollar": true, + "params": [ + { + "number": 1, + "location": 57, + "origin": 67, + "name": "id", + "named": true + } + ], + "embeds": [ + { + "table": "order details", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(\"order details\")" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/input.sql b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/input.sql new file mode 100644 index 0000000000..5e5891a8b2 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/input.sql @@ -0,0 +1,2 @@ +-- name: Bad :many +SELECT id FROM t WHERE user_id = sqlc.arg('user-id') - 1; diff --git a/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/output.sql b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/output.sql new file mode 100644 index 0000000000..5e5891a8b2 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/output.sql @@ -0,0 +1,2 @@ +-- name: Bad :many +SELECT id FROM t WHERE user_id = sqlc.arg('user-id') - 1; diff --git a/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/side_table.json b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/side_table.json new file mode 100644 index 0000000000..e0e4768647 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/side_table.json @@ -0,0 +1,9 @@ +[ + { + "start": 0, + "end": 76, + "dollar": true, + "error": "parameter name \"user-id\" passed to sqlc.arg is not a valid @name; use letters, digits and underscores", + "error_at": 52 + } +] diff --git a/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/stderr.txt b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/stderr.txt new file mode 100644 index 0000000000..89416c54d3 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/error_invalid_param_name/stderr.txt @@ -0,0 +1 @@ +2:34: parameter name "user-id" passed to sqlc.arg is not a valid @name; use letters, digits and underscores diff --git a/internal/sql/preprocess/testdata/mssql/names_with_hash/input.sql b/internal/sql/preprocess/testdata/mssql/names_with_hash/input.sql new file mode 100644 index 0000000000..8f68a05ce5 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/names_with_hash/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id = @a AND parent = @a#b AND other = @a$c AND related = @a@b; diff --git a/internal/sql/preprocess/testdata/mssql/names_with_hash/output.sql b/internal/sql/preprocess/testdata/mssql/names_with_hash/output.sql new file mode 100644 index 0000000000..8f68a05ce5 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/names_with_hash/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id = @a AND parent = @a#b AND other = @a$c AND related = @a@b; diff --git a/internal/sql/preprocess/testdata/mssql/names_with_hash/side_table.json b/internal/sql/preprocess/testdata/mssql/names_with_hash/side_table.json new file mode 100644 index 0000000000..7c2e22ab83 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/names_with_hash/side_table.json @@ -0,0 +1,37 @@ +[ + { + "start": 0, + "end": 89, + "dollar": true, + "params": [ + { + "number": 1, + "location": 32, + "origin": 32, + "name": "a", + "named": true + }, + { + "number": 2, + "location": 48, + "origin": 48, + "name": "a#b", + "named": true + }, + { + "number": 3, + "location": 65, + "origin": 65, + "name": "a$c", + "named": true + }, + { + "number": 4, + "location": 84, + "origin": 84, + "name": "a@b", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/input.sql b/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/input.sql new file mode 100644 index 0000000000..cadc2e6fac --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/input.sql @@ -0,0 +1,2 @@ +-- name: Bracketed :many +SELECT [sqlc.arg(x)], [a;b], [@id], [x]]@y] FROM t WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/output.sql b/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/output.sql new file mode 100644 index 0000000000..cadc2e6fac --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/output.sql @@ -0,0 +1,2 @@ +-- name: Bracketed :many +SELECT [sqlc.arg(x)], [a;b], [@id], [x]]@y] FROM t WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/side_table.json b/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/side_table.json new file mode 100644 index 0000000000..b66e867f66 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/skip_bracketed_identifiers/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 91, + "dollar": true, + "params": [ + { + "number": 1, + "location": 87, + "origin": 87, + "name": "id", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/input.sql b/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/input.sql new file mode 100644 index 0000000000..cd8a15e7bf --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/input.sql @@ -0,0 +1 @@ +SELECT id FROM users /* outer /* sqlc.arg(a) */ @b */ WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/output.sql b/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/output.sql new file mode 100644 index 0000000000..cd8a15e7bf --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/output.sql @@ -0,0 +1 @@ +SELECT id FROM users /* outer /* sqlc.arg(a) */ @b */ WHERE id = @id; diff --git a/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/side_table.json b/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/side_table.json new file mode 100644 index 0000000000..94445664da --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/skip_nested_block_comment/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 69, + "dollar": true, + "params": [ + { + "number": 1, + "location": 65, + "origin": 65, + "name": "id", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/slice/input.sql b/internal/sql/preprocess/testdata/mssql/slice/input.sql new file mode 100644 index 0000000000..6a76a052a4 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/slice/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id IN (sqlc.slice(ids)) AND name = @name; diff --git a/internal/sql/preprocess/testdata/mssql/slice/output.sql b/internal/sql/preprocess/testdata/mssql/slice/output.sql new file mode 100644 index 0000000000..2816aaef80 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/slice/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id IN (/*SLICE:ids*/@ids) AND name = @name; diff --git a/internal/sql/preprocess/testdata/mssql/slice/side_table.json b/internal/sql/preprocess/testdata/mssql/slice/side_table.json new file mode 100644 index 0000000000..32867f1747 --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/slice/side_table.json @@ -0,0 +1,24 @@ +[ + { + "start": 0, + "end": 70, + "dollar": true, + "params": [ + { + "number": 1, + "location": 47, + "origin": 34, + "name": "ids", + "named": true, + "slice": true + }, + { + "number": 2, + "location": 64, + "origin": 62, + "name": "name", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/input.sql b/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/input.sql new file mode 100644 index 0000000000..89714db7ab --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/input.sql @@ -0,0 +1 @@ +UPDATE users SET name = @name WHERE id = @id; SELECT @@ROWCOUNT; diff --git a/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/output.sql b/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/output.sql new file mode 100644 index 0000000000..89714db7ab --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/output.sql @@ -0,0 +1 @@ +UPDATE users SET name = @name WHERE id = @id; SELECT @@ROWCOUNT; diff --git a/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/side_table.json b/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/side_table.json new file mode 100644 index 0000000000..ad18705e8f --- /dev/null +++ b/internal/sql/preprocess/testdata/mssql/system_functions_are_not_parameters/side_table.json @@ -0,0 +1,28 @@ +[ + { + "start": 0, + "end": 45, + "dollar": true, + "params": [ + { + "number": 1, + "location": 24, + "origin": 24, + "name": "name", + "named": true + }, + { + "number": 2, + "location": 41, + "origin": 41, + "name": "id", + "named": true + } + ] + }, + { + "start": 45, + "end": 64, + "dollar": true + } +]