diff --git a/docs/guides/development.md b/docs/guides/development.md index ae5ece2fa6..96757389df 100644 --- a/docs/guides/development.md +++ b/docs/guides/development.md @@ -14,6 +14,64 @@ Install `sqlc-gen-json` to avoid test failure. go build -o ~/go/bin/sqlc-gen-json ./cmd/sqlc-gen-json ``` +## Dialect type-combination policies + +The analysis core owns traversal, column names, catalog resolution and +nullability propagation. Database-specific common-type algorithms belong to +the engine: SQL Server's precedence, length and decimal rules live in +`internal/engine/mssql/typepolicy`, independent of its parser. + +`core.TypeCombiner` receives an operation context and the entire ordered list +of candidate types, retaining nil entries for untyped values. It returns a +type and an explicit `Handled` flag; declining a context preserves the old +inference fallback, while an error is propagated. Engines without a combiner +keep their existing behavior. `Prepare` accepts the combiner through an +optional analyzer option, inherited by nested queries. + +When a converter lowers a function to a shared expression, it can retain the +source function name rather than encoding a result-type instruction in the +AST. `TypeCombinationRequest.FunctionName` passes that qualified identity to +the engine. For example, SQL Server's policy, not shared analysis, selects +ISNULL's first typed argument; ordinary COALESCE still combines its candidates. +The shared first-typed-value helper is only the compatibility fallback for +engines without a policy or for contexts a policy declines. + +The compiler's existing engine assembly selects an optional combiner factory. +It calls that factory after `core.NewCached` returns, on both fresh and +restored catalogs, and passes the instance to analysis. No engine names, +imports or strategy registry are needed in shared core analysis or seeding. +Combiners must allow concurrent analysis and must not mutate candidate types. + +`set_operations` in `dialect.json` is opaque JSON to shared seeding. Its schema +and validation belong to the engine implementation; SQL Server's precedence +and decimal settings are not a universal coercion language. Only the data is +cached; runtime behavior is constructed afresh. Malformed configuration fails +in the engine factory, and configuration without a provider is a setup error. + +To support another dialect, implement its combiner and wire its factory at +the compiler's engine assembly, without editing shared policy dispatch or +validation. Add end-to-end cases rather than copying SQL Server's settings. +Some engines additionally need literal/domain metadata or recursive structured +types; extend the inputs for actual implementations rather than forcing those +semantics through pairwise precedence. Set operations and `CASE`-like +expressions need not share an algorithm. The current cross-engine gaps and +database references are in +[`analyze`](../howto/analyze.md#common-type-inference). + +SQL Server's converter also performs syntax/semantic lowering before seeded +function lookup. `convertMethodCall` handles receivers, static `type::method` +syntax and XML `value`'s type argument; `convertAlternatives` produces the +shared AST nodes that combine candidates and propagate nullability. Ordinary +function calls retain `OVER` rather than being rewritten as alternatives. +JSONL signatures already describe the lowered calls' argument and return +types, but cannot currently request these AST transformations. The method +recognition map must stay synchronized with the leading-dot signatures. +The datepart recognition map distinguishes bare keyword arguments from column +references; `ident_args` in the dialect settings applies to *type* arguments, +not function arguments. Moving these maps into seed metadata would require +new converter metadata and a way to consume it before analysis, not merely +additional function signatures. + ## Running Tests ``` diff --git a/docs/howto/analyze.md b/docs/howto/analyze.md index 09784d2217..e1c629fb04 100644 --- a/docs/howto/analyze.md +++ b/docs/howto/analyze.md @@ -19,6 +19,45 @@ engine's own analysis path, so the two can report a type differently — most visibly, `analyze` reports type names as the catalog stores them, in lower case. +## Common-type inference + +The shared core does not give every database the same coercion rules. SQL +Server supplies its own type combiner for set operations and +alternative-valued expressions such as `CASE`, `COALESCE`, `GREATEST`, `LEAST` +and `CHOOSE`. `ISNULL` instead keeps its first typed argument's type; both +arguments must be nullable for its result to be nullable. + +Dialects without a combiner keep the existing core fallback: a set-operation +column takes its type from the left branch, and an alternative-valued +expression takes the first typed candidate. **This is incomplete static +inference, not those databases' common-type semantics.** It does not change +their normal `generate` paths. + +The database rules differ in ways that need separate policies: + +| Dialect | Database behavior and current core gap | +| --- | --- | +| SQL Server | Uses type precedence and combines decimal scale/integer digits with a precision limit of 38. The enabled policy models these rules, but is not a general implicit-cast validator. | +| PostgreSQL | Selects common types using categories, implicit casts, preferred types, unknown literals and domains. Modifiers survive only when the candidates have the same type and modifier: `numeric(5,2) UNION numeric(8,4)` has unconstrained `numeric`, not SQL Server-style decimal widening. | +| SQLite | Values are dynamically typed; compound-view/subquery affinity can come from any constituent `SELECT`. Declared decimal precision and string length do not impose SQL Server-style limits. | +| MySQL | Aggregates types and lengths across set-operation branches; alternative expressions also need signed/unsigned, numeric, binary/string, temporal and NULL-literal rules. | +| DuckDB | Uses combination casting for set operations, including casts not allowed by ordinary implicit casting. Integer widths and nested types need more than a precedence list. | +| ClickHouse | Computes compatible supertypes, including `Nullable`, signed/unsigned widths and nested types; decimal precision can reach 76. Some combinations have no compatible supertype. | +| GoogleSQL (Spanner) | Intersects supertype sets, distinguishes exact from inexact numeric inputs, and has separate literal and structured-type rules. Its type inventory is not identical to BigQuery's. | + +These are reviewed database semantics, not a claim that sqlc has implemented +or live-tested the missing policies. See the upstream references: +[SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql), +[PostgreSQL common types](https://www.postgresql.org/docs/18/typeconv-union-case.html) +and [modifiers](https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/parser/parse_coerce.c), +[SQLite](https://www.sqlite.org/datatype3.html#column_affinity_for_compound_views), +[MySQL set operations](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/set-operations.html) +and [alternative expressions](https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/flow-control-functions.html), +[DuckDB](https://duckdb.org/docs/2.0/sql/data_types/typecasting), +[ClickHouse UNION](https://clickhouse.com/docs/reference/statements/select/union) +and [decimals](https://clickhouse.com/docs/reference/data-types/decimal), +and [Spanner](https://docs.cloud.google.com/spanner/docs/reference/standard-sql/conversion_rules). + ## Usage ```sh diff --git a/internal/compiler/compile.go b/internal/compiler/compile.go index b6f703d1cb..241c0f093a 100644 --- a/internal/compiler/compile.go +++ b/internal/compiler/compile.go @@ -143,6 +143,23 @@ func (c *Compiler) parseCatalogCore(files []schemaFile, merr *multierr.Error) er // Whatever apply reported is already in merr, which the caller returns. c.coreCatalog = cat + // Runtime behavior is wired after both seed and restore paths. Installing + // it through a seed option would miss cache hits. + if c.newCoreTypeCombiner != nil { + c.coreTypeCombiner, err = c.newCoreTypeCombiner(cat) + if err != nil { + return err + } + } else { + data, err := cat.TypeCombinationData() + if err != nil { + return err + } + if len(data) > 0 { + return fmt.Errorf("type-combination data requires an engine-supplied combiner") + } + } + // Codegen consumes the catalog through the Result, in the legacy shape. legacy, err := coreResultCatalog(cat) if err != nil { diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 959207b12f..f8d774f872 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -13,6 +13,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/engine/duckdb" "github.com/sqlc-dev/sqlc/internal/engine/googlesql" "github.com/sqlc-dev/sqlc/internal/engine/mssql" + mssqltypes "github.com/sqlc-dev/sqlc/internal/engine/mssql/typepolicy" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer" "github.com/sqlc-dev/sqlc/internal/engine/sqlite" @@ -37,8 +38,10 @@ type Compiler struct { // and coreDialect seeds the catalog it does so with. The catalog itself is // not built until the schema is loaded, because the two are cached // together. - coreAnalysis bool - coreDialect core.Option + coreAnalysis bool + coreDialect core.Option + coreTypeCombiner core.TypeCombiner + newCoreTypeCombiner func(*core.Catalog) (core.TypeCombiner, error) // newParser builds a parser for the configured engine, and is set for // every engine either path supports. The core path analyzes statements @@ -153,6 +156,7 @@ func (c *Compiler) initCore() error { c.newParser = func() Parser { return mssql.NewParser() } c.selector = newDefaultSelector() dialect = mssql.Dialect() + c.newCoreTypeCombiner = mssqltypes.New case config.EngineDuckDB: c.newParser = func() Parser { return duckdb.NewParser() } c.selector = newDefaultSelector() diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index 2e1dfc7042..3b6ddbba2b 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -57,7 +57,7 @@ func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess. var params []Parameter switch raw.Stmt.(type) { case *ast.SelectStmt, *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt: - res, err := coreanalyzer.Prepare(c.coreCatalog, raw) + res, err := coreanalyzer.Prepare(c.coreCatalog, raw, coreanalyzer.WithTypeCombiner(c.coreTypeCombiner)) if err != nil { return nil, err } diff --git a/internal/core/analyzer/analyzer.go b/internal/core/analyzer/analyzer.go index 2bc94d7338..f3d2d84ad1 100644 --- a/internal/core/analyzer/analyzer.go +++ b/internal/core/analyzer/analyzer.go @@ -7,7 +7,13 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { +type Option func(*analyzer) + +func WithTypeCombiner(combiner core.TypeCombiner) Option { + return func(a *analyzer) { a.combiner = combiner } +} + +func Prepare(cat *core.Catalog, stmt ast.Node, options ...Option) (core.PrepareResult, error) { if rs, ok := stmt.(*ast.RawStmt); ok { stmt = rs.Stmt } @@ -16,6 +22,9 @@ func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { params: map[int]core.Parameter{}, stars: &[]core.StarExpansion{}, } + for _, option := range options { + option(a) + } switch s := stmt.(type) { case *ast.SelectStmt: if err := a.analyzeSelect(s); err != nil { @@ -44,11 +53,12 @@ func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { } type analyzer struct { - cat *core.Catalog - scope *scope - columns []core.Column - params map[int]core.Parameter - command core.Command + cat *core.Catalog + combiner core.TypeCombiner + scope *scope + columns []core.Column + params map[int]core.Parameter + command core.Command // outer is the scope of the query this one is nested in, which a // correlated subquery refers to. @@ -83,11 +93,12 @@ func (a *analyzer) recordStar(s core.StarExpansion) { // query's scope, so a correlated reference resolves. func (a *analyzer) subquery(s *ast.SelectStmt) (*analyzer, error) { sub := &analyzer{ - cat: a.cat, - params: a.params, - outer: a.scope, - ctes: a.ctes, - stars: a.stars, + cat: a.cat, + combiner: a.combiner, + params: a.params, + outer: a.scope, + ctes: a.ctes, + stars: a.stars, } if err := sub.analyzeSelect(s); err != nil { return nil, err @@ -277,11 +288,18 @@ func (a *analyzer) analyzeSetOperation(s *ast.SelectStmt) error { if err != nil { return err } - if _, err := a.subquery(s.Rarg); err != nil { + right, err := a.subquery(s.Rarg) + if err != nil { return err } a.columns = left.columns a.scope = left.scope + if a.combiner != nil { + a.columns, err = a.combineColumns(s.Op, left.columns, right.columns) + if err != nil { + return err + } + } return nil } diff --git a/internal/core/analyzer/expr.go b/internal/core/analyzer/expr.go index 62a69b273d..c50032e602 100644 --- a/internal/core/analyzer/expr.go +++ b/internal/core/analyzer/expr.go @@ -74,7 +74,11 @@ func (a *analyzer) typeExpr(n ast.Node) (exprType, error) { return a.typeCoalesce(e) case *ast.MinMaxExpr: - return a.typeFirstOf(listItems(e.Args), false) + context := core.CombineGreatest + if e.Op != 0 { + context = core.CombineLeast + } + return a.typeFirstOf(context, listItems(e.Args), false) case *ast.A_ArrayExpr: return a.typeArrayExpr(e) @@ -462,6 +466,13 @@ func (a *analyzer) typeCase(e *ast.CaseExpr) (exprType, error) { continue } // "CASE x WHEN y" compares y against x; "CASE WHEN y" is a predicate. + // A placeholder for x takes the type of what it is compared with. + if pr, ok := e.Arg.(*ast.ParamRef); ok && argT.typeOID == 0 && argT.expr == nil { + if argT, err = a.typeExpr(when.Expr); err != nil { + return exprType{}, err + } + a.inferParam(pr, argT) + } if e.Arg != nil { if err := a.typeOperands(when.Expr, argT); err != nil { return exprType{}, err @@ -474,59 +485,84 @@ func (a *analyzer) typeCase(e *ast.CaseExpr) (exprType, error) { if e.Defresult != nil { results = append(results, e.Defresult) } - t, err := a.typeFirstOf(results, e.Defresult == nil) + t, err := a.typeFirstOf(core.CombineCase, results, e.Defresult == nil) if err != nil { return exprType{}, err } return t, nil } -// typeCoalesce types COALESCE, which is its first typed argument's type and -// is null only when every argument is. +// typeCoalesce types COALESCE-like expressions, which are null only when +// every argument is. Function-specific type selection belongs to the dialect. func (a *analyzer) typeCoalesce(e *ast.CoalesceExpr) (exprType, error) { - var out exprType - found := false + types := make([]exprType, 0, len(listItems(e.Args))) nullable := true for _, n := range listItems(e.Args) { t, err := a.typeExpr(n) if err != nil { return exprType{}, err } - if !found && (t.typeOID != 0 || t.expr != nil) { - // The result is an expression's, not the column's it came from. - out = exprType{typeOID: t.typeOID, expr: t.expr} - found = true - } + types = append(types, t) nullable = nullable && t.nullable } - out.nullable = nullable - return out, nil + req := core.TypeCombinationRequest{Context: core.CombineCoalesce} + if e.Func != nil { + if e.Func.Catalog != "" { + req.FunctionName = append(req.FunctionName, e.Func.Catalog, e.Func.Schema) + } else if e.Func.Schema != "" { + req.FunctionName = append(req.FunctionName, e.Func.Schema) + } + req.FunctionName = append(req.FunctionName, e.Func.Name) + } + t, err := a.commonType(req, types) + if err != nil { + return exprType{}, err + } + // A placeholder there is one that may be NULL. + a.inferAlternatives(listItems(e.Args), t, true) + // The result is an expression's, not the column's it came from. + return exprType{typeOID: t.typeOID, expr: t.expr, nullable: nullable}, nil } -// typeFirstOf types a set of alternative results, taking the first one that has -// a type. The result is nullable when any alternative is, or when the caller -// says the set is not exhaustive. -func (a *analyzer) typeFirstOf(nodes []ast.Node, nullable bool) (exprType, error) { - var out exprType - found := false +// typeFirstOf types a set of alternative results, as the type they combine +// to, which without a dialect's rules is the first typed one's. The result +// is nullable when any alternative is, or when the caller says the set is +// not exhaustive. +func (a *analyzer) typeFirstOf(context core.TypeCombinationContext, nodes []ast.Node, nullable bool) (exprType, error) { + types := make([]exprType, 0, len(nodes)) for _, n := range nodes { t, err := a.typeExpr(n) if err != nil { return exprType{}, err } - if !found && (t.typeOID != 0 || t.expr != nil) { - out = t - found = true - } + types = append(types, t) nullable = nullable || t.nullable } + out, err := a.commonType(core.TypeCombinationRequest{Context: context}, types) + if err != nil { + return exprType{}, err + } + a.inferAlternatives(nodes, out, false) out.nullable = nullable return out, nil } +// inferAlternatives types a placeholder among alternatives as the type the +// others combine to, as GREATEST(1, @n) takes @n as an integer. +func (a *analyzer) inferAlternatives(nodes []ast.Node, t exprType, nullable bool) { + if t.typeOID == 0 && t.expr == nil { + return + } + for _, n := range nodes { + if p, ok := n.(*ast.ParamRef); ok { + a.inferParam(p, exprType{typeOID: t.typeOID, expr: t.expr, nullable: nullable}) + } + } +} + // typeArrayExpr types ARRAY[...], whose type is an array of its elements'. func (a *analyzer) typeArrayExpr(e *ast.A_ArrayExpr) (exprType, error) { - elemT, err := a.typeFirstOf(listItems(e.Elements), false) + elemT, err := a.typeFirstOf(core.CombineArray, listItems(e.Elements), false) if err != nil { return exprType{}, err } diff --git a/internal/core/analyzer/setop.go b/internal/core/analyzer/setop.go new file mode 100644 index 0000000000..e9665c9919 --- /dev/null +++ b/internal/core/analyzer/setop.go @@ -0,0 +1,114 @@ +package analyzer + +import ( + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// combineColumns types each column of a set operation from both of its +// branches by the dialect's rules. A column keeps the first branch's name +// and is read from no one table. It may be NULL in a UNION when either +// branch's may, in an INTERSECT when both may, and in an EXCEPT when the +// first branch's may, since every row it returns is one of those. +func (a *analyzer) combineColumns(op ast.SetOperation, left, right []core.Column) ([]core.Column, error) { + context := core.CombineUnion + switch op { + case ast.Intersect: + context = core.CombineIntersect + case ast.Except: + context = core.CombineExcept + } + out := make([]core.Column, len(left)) + for i, l := range left { + if i >= len(right) { + out[i] = l + continue + } + r := right[i] + col := core.Column{Name: l.Name, NotNull: l.NotNull && r.NotNull} + switch op { + case ast.Intersect: + col.NotNull = l.NotNull || r.NotNull + case ast.Except: + col.NotNull = l.NotNull + } + t, handled, err := a.combineTypes(core.TypeCombinationRequest{Context: context}, []exprType{columnExprType(l), columnExprType(r)}) + if err != nil { + return nil, err + } + if !handled { + out[i] = l + continue + } + t.nullable = !col.NotNull + col.TypeOID = t.typeOID + col.DataType, col.IsArray = a.typeNameOf(t) + col.Type = a.typeExprOf(t) + out[i] = col + } + return out, nil +} + +// combineTypes delegates selection and modifiers to engine-supplied behavior. +// The whole list is passed without discarding untyped candidate positions. +func (a *analyzer) combineTypes(req core.TypeCombinationRequest, types []exprType) (exprType, bool, error) { + if a.combiner == nil { + return exprType{}, false, nil + } + candidates := make([]*core.TypeExpr, len(types)) + typed := 0 + for i, t := range types { + candidates[i] = a.exprOf(t) + if candidates[i] != nil { + typed++ + } + } + req.Candidates = candidates + result, err := a.combiner.Combine(req) + if err != nil { + return exprType{}, false, err + } + if !result.Handled { + return exprType{}, false, nil + } + out := result.Type + if out == nil { + return exprType{}, true, nil + } + // Preserve the existing column provenance when only the first candidate + // is typed and the combiner leaves its type unchanged. + if typed == 1 && candidates[0] != nil && out.Key() == candidates[0].Key() { + return types[0], true, nil + } + // A type with no arguments is its family. Looked up as a declaration + // it would take the dialect's defaults, as varchar is varchar(1). + if len(out.Args) == 0 { + if oid, err := a.cat.TypeOID(out.Name); err == nil { + return exprType{typeOID: oid}, true, nil + } + } + return a.lookupType(out), true, nil +} + +// commonType retains first-typed-value inference when no combiner handles +// the context. This compatibility fallback is not database coercion semantics. +func (a *analyzer) commonType(req core.TypeCombinationRequest, types []exprType) (exprType, error) { + out, handled, err := a.combineTypes(req, types) + if err != nil || handled { + return out, err + } + return firstTyped(types), nil +} + +func firstTyped(types []exprType) exprType { + for i, t := range types { + if t.typeOID == 0 && t.expr == nil { + continue + } + if i > 0 { + t.sourceClassOID, t.sourceAttributeOID, t.sourceTableAlias = 0, 0, "" + } + return t + } + return exprType{} +} diff --git a/internal/core/analyzer/typecombiner_test.go b/internal/core/analyzer/typecombiner_test.go new file mode 100644 index 0000000000..654c1d1e15 --- /dev/null +++ b/internal/core/analyzer/typecombiner_test.go @@ -0,0 +1,63 @@ +package analyzer + +import ( + "errors" + "testing" + + "github.com/sqlc-dev/sqlc/internal/core" +) + +type combinerFunc func(core.TypeCombinationRequest) (core.TypeCombinationResult, error) + +func (f combinerFunc) Combine(req core.TypeCombinationRequest) (core.TypeCombinationResult, error) { + return f(req) +} + +// A deliberately declining/failing engine cannot be selected through the CLI. +func TestTypeCombinerContract(t *testing.T) { + failure := errors.New("policy failure") + for _, name := range []string{"absent", "declined", "untyped result", "error"} { + t.Run(name, func(t *testing.T) { + types := []exprType{{}, {expr: core.ParseTypeExpr("integer")}, {expr: core.ParseTypeExpr("text")}} + a := &analyzer{} + if name != "absent" { + a.combiner = combinerFunc(func(req core.TypeCombinationRequest) (core.TypeCombinationResult, error) { + if req.Context != core.CombineCoalesce || len(req.FunctionName) != 2 || + req.FunctionName[0] != "schema" || req.FunctionName[1] != "fn" || + len(req.Candidates) != 3 || + req.Candidates[0] != nil || req.Candidates[1].Name != "integer" || + req.Candidates[2].Name != "text" { + t.Fatalf("candidate context/order not preserved: %+v", req) + } + switch name { + case "error": + return core.TypeCombinationResult{}, failure + case "untyped result": + return core.TypeCombinationResult{Handled: true}, nil + default: + return core.TypeCombinationResult{}, nil + } + }) + } + got, err := a.commonType(core.TypeCombinationRequest{ + Context: core.CombineCoalesce, FunctionName: []string{"schema", "fn"}, + }, types) + if name == "error" { + if !errors.Is(err, failure) { + t.Fatalf("got %v, want policy failure", err) + } + return + } + if err != nil { + t.Fatal(err) + } + if name == "untyped result" { + if got.typeOID != 0 || got.expr != nil { + t.Fatalf("handled untyped result fell back to %+v", got) + } + } else if got.expr == nil || got.expr.Name != "integer" { + t.Fatalf("first-typed fallback changed: %+v", got) + } + }) + } +} diff --git a/internal/core/dialect.go b/internal/core/dialect.go index e192c442c9..062a84778d 100644 --- a/internal/core/dialect.go +++ b/internal/core/dialect.go @@ -2,6 +2,9 @@ package core import ( "context" + "database/sql" + "encoding/json" + "errors" "fmt" "slices" "strings" @@ -77,6 +80,10 @@ const FlagPropagateNullable = "functions.propagate_nullable" // e.id. const FlagQualifyDuplicateColumns = "columns.qualify_duplicates" +// FlagSetOperations holds opaque JSON interpreted by the dialect's type +// combiner. Shared catalog/seeding code does not define its schema. +const FlagSetOperations = "set_operations" + // 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 +203,25 @@ func (c *Catalog) QualifiesDuplicateColumns() bool { return v == "true" } +// TypeCombinationData returns the dialect-owned configuration, or nil when +// the dialect has none. It is persisted with the catalog, not runtime behavior. +func (c *Catalog) TypeCombinationData() (json.RawMessage, error) { + if c.dialectOID == 0 { + return nil, nil + } + v, err := c.q.DialectFlag(context.Background(), catalogdb.DialectFlagParams{ + DialectOid: c.dialectOID, + Key: FlagSetOperations, + }) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("read type-combination data: %w", err) + } + return json.RawMessage(v), nil +} + // 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/seed/seed.go b/internal/core/seed/seed.go index 064afedae8..ef076e6920 100644 --- a/internal/core/seed/seed.go +++ b/internal/core/seed/seed.go @@ -84,6 +84,10 @@ type Settings struct { // ClickHouse names the second id of a join e.id. QualifyDuplicateColumns bool `json:"qualify_duplicate_columns,omitempty"` + // SetOperations is opaque configuration for an engine-owned type + // combiner. Only that engine interprets and validates its contents. + SetOperations json.RawMessage `json:"set_operations,omitempty"` + // Comparison operators are registered as (T, T) -> Bool for every type in // ComparisonCategories. Comparison []string `json:"comparison,omitempty"` @@ -660,6 +664,11 @@ func (b *builder) consts() error { return err } } + if len(b.settings.SetOperations) > 0 { + if err := b.cat.SetDialectFlag(b.dialectOID, core.FlagSetOperations, string(b.settings.SetOperations)); 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/typecombiner.go b/internal/core/typecombiner.go new file mode 100644 index 0000000000..e4fa8d39e2 --- /dev/null +++ b/internal/core/typecombiner.go @@ -0,0 +1,40 @@ +package core + +// TypeCombinationContext distinguishes constructs whose coercion rules may +// differ even within one dialect. +type TypeCombinationContext string + +const ( + CombineUnion TypeCombinationContext = "union" + CombineIntersect TypeCombinationContext = "intersect" + CombineExcept TypeCombinationContext = "except" + CombineCase TypeCombinationContext = "case" + CombineCoalesce TypeCombinationContext = "coalesce" + CombineGreatest TypeCombinationContext = "greatest" + CombineLeast TypeCombinationContext = "least" + CombineArray TypeCombinationContext = "array" +) + +type TypeCombinationRequest struct { + Context TypeCombinationContext + // FunctionName preserves a lowered function's identity as qualified name + // components, not a result-type instruction. Nil means the ordinary + // construct identified by Context. + FunctionName []string + // Candidates are in expression order, with nil for an untyped value. + // Top-level result nullability is handled by the analyzer. + Candidates []*TypeExpr +} + +type TypeCombinationResult struct { + // Handled distinguishes an untyped result from an unsupported context. + // When false, the analyzer retains its existing inference behavior. + Handled bool + Type *TypeExpr +} + +// TypeCombiner is supplied by a dialect, not selected by shared analysis. +// Implementations must support concurrent calls and not mutate their inputs. +type TypeCombiner interface { + Combine(TypeCombinationRequest) (TypeCombinationResult, error) +} diff --git a/internal/endtoend/testdata/analyze_alternative_params/mssql/exec.json b/internal/endtoend/testdata/analyze_alternative_params/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_alternative_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_alternative_params/mssql/query.sql b/internal/endtoend/testdata/analyze_alternative_params/mssql/query.sql new file mode 100644 index 0000000000..21aae2f85a --- /dev/null +++ b/internal/endtoend/testdata/analyze_alternative_params/mssql/query.sql @@ -0,0 +1,4 @@ +-- name: Alternatives :many +SELECT GREATEST(1, @n) AS g, LEAST(n, @cap) AS l, COALESCE(@x, name) AS c, + CASE WHEN id > 0 THEN @label ELSE name END AS k, IIF(id > 0, @alt, at) AS i +FROM t; diff --git a/internal/endtoend/testdata/analyze_alternative_params/mssql/schema.sql b/internal/endtoend/testdata/analyze_alternative_params/mssql/schema.sql new file mode 100644 index 0000000000..53c28cb58a --- /dev/null +++ b/internal/endtoend/testdata/analyze_alternative_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_alternative_params/mssql/stdout.json b/internal/endtoend/testdata/analyze_alternative_params/mssql/stdout.json new file mode 100644 index 0000000000..23fa44d170 --- /dev/null +++ b/internal/endtoend/testdata/analyze_alternative_params/mssql/stdout.json @@ -0,0 +1,122 @@ +[ + { + "name": "Alternatives", + "cmd": ":many", + "columns": [ + { + "name": "g", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "l", + "type": { + "name": "bigint", + "nullable": true + }, + "table": "t" + }, + { + "name": "c", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 50 + } + ] + } + }, + { + "name": "k", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 50 + } + ] + } + }, + { + "name": "i", + "type": { + "name": "datetime2", + "nullable": true, + "args": [ + { + "int": 3 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "n", + "type": { + "name": "int" + } + } + }, + { + "number": 2, + "column": { + "name": "cap", + "type": { + "name": "bigint" + } + } + }, + { + "number": 3, + "column": { + "name": "x", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 50 + } + ] + } + } + }, + { + "number": 4, + "column": { + "name": "label", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 50 + } + ] + } + } + }, + { + "number": 5, + "column": { + "name": "alt", + "type": { + "name": "datetime2", + "args": [ + { + "int": 3 + } + ] + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_at_time_zone/mssql/exec.json b/internal/endtoend/testdata/analyze_at_time_zone/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_at_time_zone/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_at_time_zone/mssql/query.sql b/internal/endtoend/testdata/analyze_at_time_zone/mssql/query.sql new file mode 100644 index 0000000000..60bb2bd927 --- /dev/null +++ b/internal/endtoend/testdata/analyze_at_time_zone/mssql/query.sql @@ -0,0 +1,6 @@ +-- name: CreatedIn :many +SELECT id, + created AT TIME ZONE 'UTC' AS created_utc, + GETDATE() AT TIME ZONE 'UTC' AS now_utc +FROM authors +WHERE created AT TIME ZONE @tz > SYSDATETIMEOFFSET(); diff --git a/internal/endtoend/testdata/analyze_at_time_zone/mssql/schema.sql b/internal/endtoend/testdata/analyze_at_time_zone/mssql/schema.sql new file mode 100644 index 0000000000..3629635d2e --- /dev/null +++ b/internal/endtoend/testdata/analyze_at_time_zone/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_at_time_zone/mssql/stdout.json b/internal/endtoend/testdata/analyze_at_time_zone/mssql/stdout.json new file mode 100644 index 0000000000..be5e5a7601 --- /dev/null +++ b/internal/endtoend/testdata/analyze_at_time_zone/mssql/stdout.json @@ -0,0 +1,55 @@ +[ + { + "name": "CreatedIn", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "created_utc", + "type": { + "name": "datetimeoffset", + "nullable": true, + "args": [ + { + "int": 7 + } + ] + } + }, + { + "name": "now_utc", + "type": { + "name": "datetimeoffset", + "nullable": true, + "args": [ + { + "int": 3 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "tz", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 4000 + } + ] + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_combined_types/mssql/exec.json b/internal/endtoend/testdata/analyze_combined_types/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_combined_types/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_combined_types/mssql/query.sql b/internal/endtoend/testdata/analyze_combined_types/mssql/query.sql new file mode 100644 index 0000000000..aead99fc6d --- /dev/null +++ b/internal/endtoend/testdata/analyze_combined_types/mssql/query.sql @@ -0,0 +1,22 @@ +-- name: Combined :many +SELECT GREATEST(i, d52) AS greatest_of, + LEAST(d52, i, si) AS least_of, + CHOOSE(@pick, i, d102) AS chosen, + CASE WHEN i > 0 THEN si ELSE d102 END AS case_of, + IIF(i > 0, v20, n10) AS iif_of, + COALESCE(m, d102) AS coalesce_of, + ISNULL(i, d52) AS isnull_int, + ISNULL(n10, c3) AS isnull_text +FROM t; + +-- name: NestedCombined :many +SELECT n.value +FROM (SELECT GREATEST(i, d52) AS value FROM t) AS n; + +-- name: CTECombined :many +WITH combined AS ( + SELECT d52 AS value FROM t + UNION ALL + SELECT i FROM t +) +SELECT value FROM combined; diff --git a/internal/endtoend/testdata/analyze_combined_types/mssql/schema.sql b/internal/endtoend/testdata/analyze_combined_types/mssql/schema.sql new file mode 100644 index 0000000000..df06bb301b --- /dev/null +++ b/internal/endtoend/testdata/analyze_combined_types/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE t (i INT NOT NULL, d52 DECIMAL(5,2) NOT NULL, d102 DECIMAL(10,2), v20 VARCHAR(20) NOT NULL, n10 NVARCHAR(10), c3 CHAR(3), si SMALLINT NOT NULL, m MONEY); diff --git a/internal/endtoend/testdata/analyze_combined_types/mssql/stdout.json b/internal/endtoend/testdata/analyze_combined_types/mssql/stdout.json new file mode 100644 index 0000000000..f14688123c --- /dev/null +++ b/internal/endtoend/testdata/analyze_combined_types/mssql/stdout.json @@ -0,0 +1,164 @@ +[ + { + "name": "Combined", + "cmd": ":many", + "columns": [ + { + "name": "greatest_of", + "type": { + "name": "decimal", + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "least_of", + "type": { + "name": "decimal", + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "chosen", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "case_of", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "iif_of", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 20 + } + ] + } + }, + { + "name": "coalesce_of", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 19 + }, + { + "int": 4 + } + ] + } + }, + { + "name": "isnull_int", + "type": { + "name": "int" + } + }, + { + "name": "isnull_text", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "pick", + "type": { + "name": "int" + } + } + } + ] + }, + { + "name": "NestedCombined", + "cmd": ":many", + "columns": [ + { + "name": "value", + "type": { + "name": "decimal", + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + } + ], + "params": [] + }, + { + "name": "CTECombined", + "cmd": ":many", + "columns": [ + { + "name": "value", + "type": { + "name": "decimal", + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_datepart/mssql/exec.json b/internal/endtoend/testdata/analyze_datepart/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_datepart/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_datepart/mssql/query.sql b/internal/endtoend/testdata/analyze_datepart/mssql/query.sql new file mode 100644 index 0000000000..f1ae69121d --- /dev/null +++ b/internal/endtoend/testdata/analyze_datepart/mssql/query.sql @@ -0,0 +1,13 @@ +-- name: Recent :many +SELECT id, + DATEADD(day, 1, created) AS due, + DATEDIFF(minute, created, SYSUTCDATETIME()) AS age, + DATEPART(month, created) AS month_of, + DATENAME(weekday, created) AS weekday_of +FROM authors +WHERE created > DATEADD(day, @days, SYSUTCDATETIME()); + +-- name: FromStrings :one +SELECT DATEADD(day, 1, '2026-01-01') AS next_day, + DATEADD(day, 1, N'2026-01-01') AS next_day_n, + DATETRUNC(month, '2026-01-15') AS month_start; diff --git a/internal/endtoend/testdata/analyze_datepart/mssql/schema.sql b/internal/endtoend/testdata/analyze_datepart/mssql/schema.sql new file mode 100644 index 0000000000..3629635d2e --- /dev/null +++ b/internal/endtoend/testdata/analyze_datepart/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_datepart/mssql/stdout.json b/internal/endtoend/testdata/analyze_datepart/mssql/stdout.json new file mode 100644 index 0000000000..6e1436e784 --- /dev/null +++ b/internal/endtoend/testdata/analyze_datepart/mssql/stdout.json @@ -0,0 +1,97 @@ +[ + { + "name": "Recent", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "authors" + }, + { + "name": "due", + "type": { + "name": "datetime2", + "nullable": true, + "args": [ + { + "int": 7 + } + ] + } + }, + { + "name": "age", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "month_of", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "weekday_of", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 30 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "days", + "type": { + "name": "int" + } + } + } + ] + }, + { + "name": "FromStrings", + "cmd": ":one", + "columns": [ + { + "name": "next_day", + "type": { + "name": "datetime", + "nullable": true + } + }, + { + "name": "next_day_n", + "type": { + "name": "datetime", + "nullable": true + } + }, + { + "name": "month_start", + "type": { + "name": "datetime2", + "nullable": true, + "args": [ + { + "int": 7 + } + ] + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_isnull/mssql/exec.json b/internal/endtoend/testdata/analyze_isnull/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_isnull/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_isnull/mssql/query.sql b/internal/endtoend/testdata/analyze_isnull/mssql/query.sql new file mode 100644 index 0000000000..8694f979cf --- /dev/null +++ b/internal/endtoend/testdata/analyze_isnull/mssql/query.sql @@ -0,0 +1,15 @@ +-- name: Defaults :many +SELECT id, + ISNULL(note, raw) AS note_or_raw, + ISNULL(note, note) AS still_nullable, + ISNULL(amount, 0) AS amount +FROM readings +WHERE ISNULL(note, raw) = @value; + +-- name: FirstArgumentTypes :many +SELECT ISNULL(raw, CAST(note AS NVARCHAR(40))) AS first_text, + COALESCE(raw, CAST(note AS NVARCHAR(40))) AS combined_text, + ISNULL(NULL, raw) AS literal_null, + ISNULL(@fallback, raw) AS placeholder_first, + ISNULL(note, @replacement) AS placeholder_second +FROM readings; diff --git a/internal/endtoend/testdata/analyze_isnull/mssql/schema.sql b/internal/endtoend/testdata/analyze_isnull/mssql/schema.sql new file mode 100644 index 0000000000..ac4875448d --- /dev/null +++ b/internal/endtoend/testdata/analyze_isnull/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_isnull/mssql/stdout.json b/internal/endtoend/testdata/analyze_isnull/mssql/stdout.json new file mode 100644 index 0000000000..b063e27452 --- /dev/null +++ b/internal/endtoend/testdata/analyze_isnull/mssql/stdout.json @@ -0,0 +1,162 @@ +[ + { + "name": "Defaults", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + }, + "table": "readings" + }, + { + "name": "note_or_raw", + "type": { + "name": "varchar", + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "still_nullable", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "amount", + "type": { + "name": "decimal", + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "value", + "type": { + "name": "varchar", + "args": [ + { + "int": 10 + } + ] + } + } + } + ] + }, + { + "name": "FirstArgumentTypes", + "cmd": ":many", + "columns": [ + { + "name": "first_text", + "type": { + "name": "varchar", + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "combined_text", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 40 + } + ] + } + }, + { + "name": "literal_null", + "type": { + "name": "varchar", + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "placeholder_first", + "type": { + "name": "varchar", + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "placeholder_second", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "fallback", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + } + }, + { + "number": 2, + "column": { + "name": "replacement", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_left_right_trim/mssql/exec.json b/internal/endtoend/testdata/analyze_left_right_trim/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_left_right_trim/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_left_right_trim/mssql/query.sql b/internal/endtoend/testdata/analyze_left_right_trim/mssql/query.sql new file mode 100644 index 0000000000..9eb8705b85 --- /dev/null +++ b/internal/endtoend/testdata/analyze_left_right_trim/mssql/query.sql @@ -0,0 +1,5 @@ +-- name: Trimmed :many +SELECT LEFT(a, 3) AS lf, RIGHT(c, @n) AS rt, TRIM(N'x' FROM a) AS tr, TRIM(LEADING 'x' FROM c) AS tl FROM p; + +-- name: Repeated :many +SELECT REPLICATE(a, @r) AS r, CHARINDEX(N'x', a, @ci) AS ci FROM p; diff --git a/internal/endtoend/testdata/analyze_left_right_trim/mssql/schema.sql b/internal/endtoend/testdata/analyze_left_right_trim/mssql/schema.sql new file mode 100644 index 0000000000..2008fa5e89 --- /dev/null +++ b/internal/endtoend/testdata/analyze_left_right_trim/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_left_right_trim/mssql/stdout.json b/internal/endtoend/testdata/analyze_left_right_trim/mssql/stdout.json new file mode 100644 index 0000000000..4743bd3f92 --- /dev/null +++ b/internal/endtoend/testdata/analyze_left_right_trim/mssql/stdout.json @@ -0,0 +1,95 @@ +[ + { + "name": "Trimmed", + "cmd": ":many", + "columns": [ + { + "name": "lf", + "type": { + "name": "nvarchar", + "nullable": true + } + }, + { + "name": "rt", + "type": { + "name": "varchar", + "nullable": true + } + }, + { + "name": "tr", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "tl", + "type": { + "name": "varchar", + "args": [ + { + "int": 5 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "n", + "type": { + "name": "int" + } + } + } + ] + }, + { + "name": "Repeated", + "cmd": ":many", + "columns": [ + { + "name": "r", + "type": { + "name": "nvarchar", + "nullable": true + } + }, + { + "name": "ci", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "r", + "type": { + "name": "int" + } + } + }, + { + "number": 2, + "column": { + "name": "ci", + "type": { + "name": "int" + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_more_functions/mssql/exec.json b/internal/endtoend/testdata/analyze_more_functions/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_more_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_more_functions/mssql/query.sql b/internal/endtoend/testdata/analyze_more_functions/mssql/query.sql new file mode 100644 index 0000000000..f9db529a01 --- /dev/null +++ b/internal/endtoend/testdata/analyze_more_functions/mssql/query.sql @@ -0,0 +1,22 @@ +-- name: Grouping :many +SELECT g, GROUPING(g) AS gr, GROUPING_ID(g, id) AS gid, APPROX_COUNT_DISTINCT(id) AS approx, CHECKSUM_AGG(id) AS ca +FROM s GROUP BY ROLLUP(g, id); + +-- name: Percentiles :many +SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY v) OVER (PARTITION BY g) AS med FROM s; + +-- name: Metadata :one +SELECT DB_NAME() AS d, SCHEMA_NAME() AS sn, OBJECT_ID(N'dbo.s') AS o, OBJECT_NAME(@oid) AS obj, + IDENT_CURRENT(N's') AS ic, ROWCOUNT_BIG() AS rc, ORIGINAL_LOGIN() AS ol, DB_ID() AS dbid, + ERROR_MESSAGE() AS em, ERROR_NUMBER() AS en, XACT_STATE() AS xs, SESSION_CONTEXT(N'k') AS sc; + +-- name: Strings :many +SELECT STRING_ESCAPE(g, 'json') AS se, SOUNDEX(g) AS sx, DIFFERENCE(g, @other) AS df, + LTRIM(g, N'x') AS lt, RTRIM(g, N'x') AS rt, COMPRESS(g) AS c, REGEXP_REPLACE(g, @pattern, N'b') AS rr, + REGEXP_SUBSTR(g, N'a') AS rs, REGEXP_COUNT(g, N'a') AS rc +FROM s; + +-- name: Bits :many +SELECT BIT_COUNT(id) AS bc, LEFT_SHIFT(id, 1) AS ls, GET_BIT(id, 1) AS gb, SET_BIT(id, @bit) AS sb, + DATE_BUCKET(week, 2, CAST(SYSDATETIME() AS date)) AS db +FROM s; diff --git a/internal/endtoend/testdata/analyze_more_functions/mssql/schema.sql b/internal/endtoend/testdata/analyze_more_functions/mssql/schema.sql new file mode 100644 index 0000000000..dc9cce113a --- /dev/null +++ b/internal/endtoend/testdata/analyze_more_functions/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE s (id INT NOT NULL, g NVARCHAR(10) NOT NULL, v DECIMAL(10,2) NOT NULL, f FLOAT NULL); diff --git a/internal/endtoend/testdata/analyze_more_functions/mssql/stdout.json b/internal/endtoend/testdata/analyze_more_functions/mssql/stdout.json new file mode 100644 index 0000000000..ff93fb5e24 --- /dev/null +++ b/internal/endtoend/testdata/analyze_more_functions/mssql/stdout.json @@ -0,0 +1,367 @@ +[ + { + "name": "Grouping", + "cmd": ":many", + "columns": [ + { + "name": "g", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 10 + } + ] + }, + "table": "s" + }, + { + "name": "gr", + "type": { + "name": "tinyint" + } + }, + { + "name": "gid", + "type": { + "name": "int" + } + }, + { + "name": "approx", + "type": { + "name": "bigint", + "nullable": true + } + }, + { + "name": "ca", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "Percentiles", + "cmd": ":many", + "columns": [ + { + "name": "med", + "type": { + "name": "float", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "Metadata", + "cmd": ":one", + "columns": [ + { + "name": "d", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 128 + } + ] + } + }, + { + "name": "sn", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 128 + } + ] + } + }, + { + "name": "o", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "obj", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 128 + } + ] + } + }, + { + "name": "ic", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 38 + }, + { + "int": 0 + } + ] + } + }, + { + "name": "rc", + "type": { + "name": "bigint" + } + }, + { + "name": "ol", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "dbid", + "type": { + "name": "smallint", + "nullable": true + } + }, + { + "name": "em", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "en", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "xs", + "type": { + "name": "smallint", + "nullable": true + } + }, + { + "name": "sc", + "type": { + "name": "sql_variant", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "oid", + "type": { + "name": "int" + } + } + } + ] + }, + { + "name": "Strings", + "cmd": ":many", + "columns": [ + { + "name": "se", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "sx", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 5 + } + ] + } + }, + { + "name": "df", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "lt", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "rt", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "c", + "type": { + "name": "varbinary", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "rr", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "rs", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + }, + { + "name": "rc", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "other", + "type": { + "name": "varchar" + } + } + }, + { + "number": 2, + "column": { + "name": "pattern" + } + } + ] + }, + { + "name": "Bits", + "cmd": ":many", + "columns": [ + { + "name": "bc", + "type": { + "name": "bigint", + "nullable": true + } + }, + { + "name": "ls", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "gb", + "type": { + "name": "bit", + "nullable": true + } + }, + { + "name": "sb", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "db", + "type": { + "name": "date", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "bit", + "type": { + "name": "int" + } + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/exec.json b/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_arithmetic/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_mssql_arithmetic/mssql/query.sql b/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/query.sql new file mode 100644 index 0000000000..da4ff4cc83 --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/query.sql @@ -0,0 +1,23 @@ +-- name: Numeric :many +SELECT + royalties * 2 AS doubled, + royalties / units AS per_unit, + units * weight AS weighted, + ratio + units AS ratio_units, + ratio * weight AS ratio_weight, + units + total AS units_total, + refunds - flags AS net_refunds, + fee * units AS fee_units, + small_fee + units AS small_fee_units, + small_fee + fee AS fees, + units % refunds AS remainder +FROM sales; + +-- name: Concatenation :many +SELECT + code + code AS codes, + code + title AS code_title, + ncode + title AS ncode_title, + title + name AS title_name, + code + ncode AS code_ncode +FROM sales; diff --git a/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/schema.sql b/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/schema.sql new file mode 100644 index 0000000000..891366facd --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE sales ( + id INT IDENTITY(1,1) PRIMARY KEY, + royalties DECIMAL(10,2), + units INT, + refunds SMALLINT, + flags TINYINT, + total BIGINT, + ratio REAL, + weight FLOAT, + fee MONEY, + small_fee SMALLMONEY, + code CHAR(4), + title VARCHAR(50), + ncode NCHAR(4), + name NVARCHAR(50) +); diff --git a/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/stdout.json b/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/stdout.json new file mode 100644 index 0000000000..7ed41e25d7 --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_arithmetic/mssql/stdout.json @@ -0,0 +1,128 @@ +[ + { + "name": "Numeric", + "cmd": ":many", + "columns": [ + { + "name": "doubled", + "type": { + "name": "decimal", + "nullable": true + } + }, + { + "name": "per_unit", + "type": { + "name": "decimal", + "nullable": true + } + }, + { + "name": "weighted", + "type": { + "name": "float", + "nullable": true + } + }, + { + "name": "ratio_units", + "type": { + "name": "real", + "nullable": true + } + }, + { + "name": "ratio_weight", + "type": { + "name": "float", + "nullable": true + } + }, + { + "name": "units_total", + "type": { + "name": "bigint", + "nullable": true + } + }, + { + "name": "net_refunds", + "type": { + "name": "smallint", + "nullable": true + } + }, + { + "name": "fee_units", + "type": { + "name": "money", + "nullable": true + } + }, + { + "name": "small_fee_units", + "type": { + "name": "smallmoney", + "nullable": true + } + }, + { + "name": "fees", + "type": { + "name": "money", + "nullable": true + } + }, + { + "name": "remainder", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "Concatenation", + "cmd": ":many", + "columns": [ + { + "name": "codes", + "type": { + "name": "char", + "nullable": true + } + }, + { + "name": "code_title", + "type": { + "name": "varchar", + "nullable": true + } + }, + { + "name": "ncode_title", + "type": { + "name": "nvarchar", + "nullable": true + } + }, + { + "name": "title_name", + "type": { + "name": "nvarchar", + "nullable": true + } + }, + { + "name": "code_ncode", + "type": { + "name": "nchar", + "nullable": true + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_mssql_functions/mssql/exec.json b/internal/endtoend/testdata/analyze_mssql_functions/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_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_mssql_functions/mssql/query.sql b/internal/endtoend/testdata/analyze_mssql_functions/mssql/query.sql new file mode 100644 index 0000000000..e8468281a8 --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_functions/mssql/query.sql @@ -0,0 +1,71 @@ +-- name: Ranking :many +SELECT + id, + LAG(price) OVER (ORDER BY id) AS previous_price, + LEAD(title, 1, 'none') OVER (ORDER BY id) AS next_title, + FIRST_VALUE(subtitle) OVER (ORDER BY id) AS first_subtitle, + LAST_VALUE(qty) OVER (ORDER BY id) AS last_qty +FROM books; + +-- name: Nulls :many +SELECT + ISNULL(rating, 0) AS rating, + ISNULL(discount, 0) AS discount, + NULLIF(qty, 0) AS qty, + CHOOSE(1, title, subtitle) AS chosen, + GREATEST(price, discount) AS greatest_price, + LEAST(title, subtitle) AS least_title +FROM books; + +-- name: Strings :many +SELECT + REPLACE(title, 'a', 'b') AS replaced, + REPLACE(note, 'a', 'b') AS replaced_n, + REVERSE(title) AS reversed, + CHARINDEX('a', title) AS position, + PATINDEX('%a%', note) AS pattern_position, + FORMAT(price, 'N2') AS formatted, + QUOTENAME(name) AS quoted, + STR(weight, 10, 2) AS str_weight +FROM books; + +-- name: Aggregates :one +SELECT + STRING_AGG(title, ',') AS titles, + STRING_AGG(note, ',') AS notes, + STDEV(qty) AS qty_stdev +FROM books; + +-- name: Math :many +SELECT + ABS(qty) AS abs_qty, + ABS(price) AS abs_price, + ABS(cost) AS abs_cost, + CEILING(weight) AS ceiling_weight, + FLOOR(rating) AS floor_rating, + ROUND(discount, 1) AS rounded, + POWER(weight, 2) AS squared, + SQRT(qty) AS root, + SIGN(discount) AS sign_discount +FROM books; + +-- name: Dates :many +SELECT + YEAR(published) AS year, + MONTH(updated) AS month, + DAY(published) AS day, + EOMONTH(published) AS month_end, + DATEFROMPARTS(rating, 1, 1) AS start_date, + DATETIME2FROMPARTS(rating, 1, 1, 0, 0, 0, 0, 3) AS start_time, + SWITCHOFFSET(offset_at, '+01:00') AS switched +FROM books; + +-- name: Json :many +SELECT + ISJSON(doc) AS is_json, + JSON_VALUE(doc, '$.a') AS json_value, + JSON_QUERY(doc, '$.a') AS json_query, + JSON_MODIFY(doc, '$.a', 1) AS json_modified, + HASHBYTES('SHA2_256', title) AS hash, + CHECKSUM(title) AS checksum +FROM books; diff --git a/internal/endtoend/testdata/analyze_mssql_functions/mssql/schema.sql b/internal/endtoend/testdata/analyze_mssql_functions/mssql/schema.sql new file mode 100644 index 0000000000..c41fbe240e --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_functions/mssql/schema.sql @@ -0,0 +1,17 @@ +CREATE TABLE books ( + id INT IDENTITY(1,1) PRIMARY KEY, + title VARCHAR(100) NOT NULL, + subtitle VARCHAR(100), + name NVARCHAR(100) NOT NULL, + note NVARCHAR(200), + price DECIMAL(10,2) NOT NULL, + discount DECIMAL(5,2), + qty INT NOT NULL, + rating INT, + weight FLOAT, + cost MONEY, + published DATE, + updated DATETIME2(3), + offset_at DATETIMEOFFSET, + doc NVARCHAR(MAX) +); diff --git a/internal/endtoend/testdata/analyze_mssql_functions/mssql/stdout.json b/internal/endtoend/testdata/analyze_mssql_functions/mssql/stdout.json new file mode 100644 index 0000000000..3983024aad --- /dev/null +++ b/internal/endtoend/testdata/analyze_mssql_functions/mssql/stdout.json @@ -0,0 +1,495 @@ +[ + { + "name": "Ranking", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "books" + }, + { + "name": "previous_price", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "next_title", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "first_subtitle", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "last_qty", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "Nulls", + "cmd": ":many", + "columns": [ + { + "name": "rating", + "type": { + "name": "int" + } + }, + { + "name": "discount", + "type": { + "name": "decimal", + "args": [ + { + "int": 5 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "qty", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "chosen", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "greatest_price", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "least_title", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 100 + } + ] + } + } + ], + "params": [] + }, + { + "name": "Strings", + "cmd": ":many", + "columns": [ + { + "name": "replaced", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 8000 + } + ] + } + }, + { + "name": "replaced_n", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "reversed", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "position", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "pattern_position", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "formatted", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "quoted", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 258 + } + ] + } + }, + { + "name": "str_weight", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 10 + } + ] + } + } + ], + "params": [] + }, + { + "name": "Aggregates", + "cmd": ":one", + "columns": [ + { + "name": "titles", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 8000 + } + ] + } + }, + { + "name": "notes", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "qty_stdev", + "type": { + "name": "float", + "nullable": true + } + } + ], + "params": [] + }, + { + "name": "Math", + "cmd": ":many", + "columns": [ + { + "name": "abs_qty", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "abs_price", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "abs_cost", + "type": { + "name": "money", + "nullable": true + } + }, + { + "name": "ceiling_weight", + "type": { + "name": "float", + "nullable": true + } + }, + { + "name": "floor_rating", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "rounded", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 5 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "squared", + "type": { + "name": "float", + "nullable": true + } + }, + { + "name": "root", + "type": { + "name": "float", + "nullable": true + } + }, + { + "name": "sign_discount", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 5 + }, + { + "int": 2 + } + ] + } + } + ], + "params": [] + }, + { + "name": "Dates", + "cmd": ":many", + "columns": [ + { + "name": "year", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "month", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "day", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "month_end", + "type": { + "name": "date", + "nullable": true + } + }, + { + "name": "start_date", + "type": { + "name": "date", + "nullable": true + } + }, + { + "name": "start_time", + "type": { + "name": "datetime2", + "nullable": true, + "args": [ + { + "int": 3 + } + ] + } + }, + { + "name": "switched", + "type": { + "name": "datetimeoffset", + "nullable": true, + "args": [ + { + "int": 7 + } + ] + } + } + ], + "params": [] + }, + { + "name": "Json", + "cmd": ":many", + "columns": [ + { + "name": "is_json", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "json_value", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "json_query", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "json_modified", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "hash", + "type": { + "name": "varbinary", + "nullable": true, + "args": [ + { + "int": 8000 + } + ] + } + }, + { + "name": "checksum", + "type": { + "name": "int", + "nullable": true + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_round/mssql/exec.json b/internal/endtoend/testdata/analyze_round/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_round/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_round/mssql/query.sql b/internal/endtoend/testdata/analyze_round/mssql/query.sql new file mode 100644 index 0000000000..9c348e718f --- /dev/null +++ b/internal/endtoend/testdata/analyze_round/mssql/query.sql @@ -0,0 +1,7 @@ +-- name: Rounded :many +SELECT ROUND(price, 1) AS rounded, + ROUND(price, 1, 1) AS truncated, + ROUND(ratio, 2, 1) AS ratio_truncated, + ROUND(qty, -1, 1) AS qty_truncated, + ROUND(fee, 2, 1) AS fee_truncated +FROM prices; diff --git a/internal/endtoend/testdata/analyze_round/mssql/schema.sql b/internal/endtoend/testdata/analyze_round/mssql/schema.sql new file mode 100644 index 0000000000..f759dbea38 --- /dev/null +++ b/internal/endtoend/testdata/analyze_round/mssql/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE prices ( + price DECIMAL(10,2), + ratio FLOAT, + qty INT, + fee MONEY +); diff --git a/internal/endtoend/testdata/analyze_round/mssql/stdout.json b/internal/endtoend/testdata/analyze_round/mssql/stdout.json new file mode 100644 index 0000000000..871c0961ce --- /dev/null +++ b/internal/endtoend/testdata/analyze_round/mssql/stdout.json @@ -0,0 +1,60 @@ +[ + { + "name": "Rounded", + "cmd": ":many", + "columns": [ + { + "name": "rounded", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "truncated", + "type": { + "name": "decimal", + "nullable": true, + "args": [ + { + "int": 10 + }, + { + "int": 2 + } + ] + } + }, + { + "name": "ratio_truncated", + "type": { + "name": "float", + "nullable": true + } + }, + { + "name": "qty_truncated", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "fee_truncated", + "type": { + "name": "money", + "nullable": true + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_set_operations/mssql/exec.json b/internal/endtoend/testdata/analyze_set_operations/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_set_operations/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_set_operations/mssql/query.sql b/internal/endtoend/testdata/analyze_set_operations/mssql/query.sql new file mode 100644 index 0000000000..344b803714 --- /dev/null +++ b/internal/endtoend/testdata/analyze_set_operations/mssql/query.sql @@ -0,0 +1,32 @@ +-- name: Everyone :many +SELECT id, name, code, score, joined FROM people WHERE name <> @skip +UNION ALL +SELECT id, title, code, budget, founded FROM orgs; + +-- name: Names :many +SELECT name FROM people +UNION +SELECT nickname FROM people +UNION +SELECT NULL; + +-- name: SharedNicknames :many +SELECT nickname, id FROM people +INTERSECT +SELECT title, id FROM orgs; + +-- name: OnlyPeople :many +SELECT id, score FROM people +EXCEPT +SELECT id, budget FROM orgs; + + +-- name: Amounts :many +SELECT qty AS v FROM ledger +UNION ALL +SELECT amount FROM ledger +UNION ALL +SELECT flag FROM ledger; + +-- name: LiteralUnion :many +SELECT 'active' AS status UNION ALL SELECT 'inactive'; diff --git a/internal/endtoend/testdata/analyze_set_operations/mssql/schema.sql b/internal/endtoend/testdata/analyze_set_operations/mssql/schema.sql new file mode 100644 index 0000000000..d5863672a6 --- /dev/null +++ b/internal/endtoend/testdata/analyze_set_operations/mssql/schema.sql @@ -0,0 +1,22 @@ +CREATE TABLE people ( + id INT IDENTITY(1,1) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + nickname NVARCHAR(20), + code CHAR(3) NOT NULL, + score DECIMAL(10,2) NOT NULL, + joined DATETIME2(3) NOT NULL +); + +CREATE TABLE orgs ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + title NVARCHAR(100) NOT NULL, + code VARCHAR(5), + budget MONEY NOT NULL, + founded DATE NOT NULL +); + +CREATE TABLE ledger ( + qty INT NOT NULL, + flag BIT NOT NULL, + amount DECIMAL(5,2) NOT NULL +); diff --git a/internal/endtoend/testdata/analyze_set_operations/mssql/stdout.json b/internal/endtoend/testdata/analyze_set_operations/mssql/stdout.json new file mode 100644 index 0000000000..97efa9f313 --- /dev/null +++ b/internal/endtoend/testdata/analyze_set_operations/mssql/stdout.json @@ -0,0 +1,183 @@ +[ + { + "name": "Everyone", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + } + }, + { + "name": "name", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "code", + "type": { + "name": "varchar", + "nullable": true, + "args": [ + { + "int": 5 + } + ] + } + }, + { + "name": "score", + "type": { + "name": "decimal", + "args": [ + { + "int": 19 + }, + { + "int": 4 + } + ] + } + }, + { + "name": "joined", + "type": { + "name": "datetime2", + "args": [ + { + "int": 3 + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "skip", + "type": { + "name": "varchar", + "args": [ + { + "int": 50 + } + ] + }, + "table": "people" + } + } + ] + }, + { + "name": "Names", + "cmd": ":many", + "columns": [ + { + "name": "name", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 50 + } + ] + } + } + ], + "params": [] + }, + { + "name": "SharedNicknames", + "cmd": ":many", + "columns": [ + { + "name": "nickname", + "type": { + "name": "nvarchar", + "args": [ + { + "int": 100 + } + ] + } + }, + { + "name": "id", + "type": { + "name": "bigint" + } + } + ], + "params": [] + }, + { + "name": "OnlyPeople", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "bigint" + } + }, + { + "name": "score", + "type": { + "name": "decimal", + "args": [ + { + "int": 19 + }, + { + "int": 4 + } + ] + } + } + ], + "params": [] + }, + { + "name": "Amounts", + "cmd": ":many", + "columns": [ + { + "name": "v", + "type": { + "name": "decimal", + "args": [ + { + "int": 12 + }, + { + "int": 2 + } + ] + } + } + ], + "params": [] + }, + { + "name": "LiteralUnion", + "cmd": ":many", + "columns": [ + { + "name": "status", + "type": { + "name": "varchar" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_type_methods/mssql/exec.json b/internal/endtoend/testdata/analyze_type_methods/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_type_methods/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_type_methods/mssql/query.sql b/internal/endtoend/testdata/analyze_type_methods/mssql/query.sql new file mode 100644 index 0000000000..0dd53b3f26 --- /dev/null +++ b/internal/endtoend/testdata/analyze_type_methods/mssql/query.sql @@ -0,0 +1,10 @@ +-- name: Methods :one +SELECT doc.value('(/a/@n)[1]', 'int') AS v, doc.value('(/a)[1]', 'nvarchar(20)') AS s, + doc.exist('/a') AS e, geo.STDistance(geo) AS dist, + geo.STAsText() AS txt, shape.ToString() AS shape_text, node.ToString() AS path, + node.GetLevel() AS lvl, node.GetAncestor(@up) AS anc, + hierarchyid::GetRoot() AS root, geography::Point(@lat, @long, 4326) AS pt +FROM m WHERE id = @id; + +-- name: Nearby :many +SELECT id FROM m WHERE geo.STDistance(geography::Point(@lat, @long, 4326)) < @meters AND node.IsDescendantOf(@ancestor) = 1; diff --git a/internal/endtoend/testdata/analyze_type_methods/mssql/schema.sql b/internal/endtoend/testdata/analyze_type_methods/mssql/schema.sql new file mode 100644 index 0000000000..88b8cb4b7e --- /dev/null +++ b/internal/endtoend/testdata/analyze_type_methods/mssql/schema.sql @@ -0,0 +1 @@ +CREATE TABLE m (id INT NOT NULL, doc XML NOT NULL, geo GEOGRAPHY NOT NULL, shape GEOMETRY NULL, node HIERARCHYID NOT NULL); diff --git a/internal/endtoend/testdata/analyze_type_methods/mssql/stdout.json b/internal/endtoend/testdata/analyze_type_methods/mssql/stdout.json new file mode 100644 index 0000000000..c587ba6fbb --- /dev/null +++ b/internal/endtoend/testdata/analyze_type_methods/mssql/stdout.json @@ -0,0 +1,196 @@ +[ + { + "name": "Methods", + "cmd": ":one", + "columns": [ + { + "name": "v", + "type": { + "name": "int", + "nullable": true + } + }, + { + "name": "s", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 20 + } + ] + } + }, + { + "name": "e", + "type": { + "name": "bit", + "nullable": true + } + }, + { + "name": "dist", + "type": { + "name": "float", + "nullable": true + } + }, + { + "name": "txt", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "shape_text", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "ident": "max" + } + ] + } + }, + { + "name": "path", + "type": { + "name": "nvarchar", + "nullable": true, + "args": [ + { + "int": 4000 + } + ] + } + }, + { + "name": "lvl", + "type": { + "name": "smallint", + "nullable": true + } + }, + { + "name": "anc", + "type": { + "name": "hierarchyid", + "nullable": true + } + }, + { + "name": "root", + "type": { + "name": "hierarchyid", + "nullable": true + } + }, + { + "name": "pt", + "type": { + "name": "geography", + "nullable": true + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "up", + "type": { + "name": "int" + } + } + }, + { + "number": 2, + "column": { + "name": "lat", + "type": { + "name": "float" + } + } + }, + { + "number": 3, + "column": { + "name": "long", + "type": { + "name": "float" + } + } + }, + { + "number": 4, + "column": { + "name": "id", + "type": { + "name": "int" + }, + "table": "m" + } + } + ] + }, + { + "name": "Nearby", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "int" + }, + "table": "m" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "lat", + "type": { + "name": "float" + } + } + }, + { + "number": 2, + "column": { + "name": "long", + "type": { + "name": "float" + } + } + }, + { + "number": 3, + "column": { + "name": "meters", + "type": { + "name": "float", + "nullable": true + } + } + }, + { + "number": 4, + "column": { + "name": "ancestor", + "type": { + "name": "hierarchyid" + } + } + } + ] + } +] diff --git a/internal/engine/mssql/convert.go b/internal/engine/mssql/convert.go index bdef335bc6..d9a5b95941 100644 --- a/internal/engine/mssql/convert.go +++ b/internal/engine/mssql/convert.go @@ -547,6 +547,18 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { case *tsql.ParenthesisExpression: return c.convertScalarExpression(e.Expression) case *tsql.FunctionCall: + // Methods need receiver/type syntax lowered before signature lookup; + // alternatives need candidate-dependent typing and nullability. + // Keep window calls on the ordinary path so an alternative rewrite + // cannot discard their OVER clause. + if out := c.convertMethodCall(e); out != nil { + return out + } + if e.OverClause == nil { + if out := c.convertAlternatives(e); out != nil { + return out + } + } return c.convertFunctionCall(e) case *tsql.CastCall: return &ast.TypeCast{ @@ -644,6 +656,25 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { Subselect: c.convertQueryExpression(e.QueryExpression), Location: c.loc(e), } + case *tsql.AtTimeZoneCall: + // x AT TIME ZONE tz is typed as the call the dialect seeds under a + // name no function can have. + return &ast.FuncCall{ + Funcname: &ast.List{ + Items: []ast.Node{&ast.String{Str: "at time zone"}}, + }, + Args: &ast.List{ + Items: []ast.Node{ + c.convertScalarExpression(e.DateValue), + c.convertScalarExpression(e.TimeZone), + }, + }, + Location: c.loc(e), + } + case *tsql.LeftFunctionCall: + return c.namedCall("left", e.Parameters, e) + case *tsql.RightFunctionCall: + return c.namedCall("right", e.Parameters, e) case *tsql.ParameterlessCall: return &ast.FuncCall{ Funcname: &ast.List{ @@ -656,6 +687,41 @@ func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { } } +// convertAlternatives converts the functions that yield one of their +// arguments: GREATEST/LEAST combine their candidate types, and CHOOSE becomes +// the CASE over its index. ISNULL keeps its first typed argument's type and +// is NULL only when both arguments are. Fixed JSONL function signatures do +// not describe these AST rewrites or the candidates' combined nullability. +func (c *cc) convertAlternatives(e *tsql.FunctionCall) ast.Node { + args := &ast.List{} + for _, p := range e.Parameters { + args.Items = append(args.Items, c.convertScalarExpression(p)) + } + switch identifierValue(e.FunctionName) { + case "isnull": + if len(args.Items) == 2 { + return &ast.CoalesceExpr{Args: args, Func: &ast.FuncName{Name: "isnull"}, Location: c.loc(e)} + } + case "greatest": + return &ast.MinMaxExpr{Op: 0, Args: args, Location: c.loc(e)} + case "least": + return &ast.MinMaxExpr{Op: 1, Args: args, Location: c.loc(e)} + case "choose": + if len(args.Items) < 2 { + return nil + } + choose := &ast.CaseExpr{Arg: args.Items[0], Args: &ast.List{}, Location: c.loc(e)} + for i, item := range args.Items[1:] { + choose.Args.Items = append(choose.Args.Items, &ast.CaseWhen{ + Expr: &ast.A_Const{Val: &ast.Integer{Ival: int64(i + 1)}}, + Result: item, + }) + } + return choose + } + return nil +} + func binaryOperator(binaryExpressionType string) string { switch binaryExpressionType { case "Add": @@ -725,6 +791,70 @@ type namedParam struct { name string } +// typeMethods are the methods of xml, geography, geometry and hierarchyid +// a query calls on a value, as doc.exist('/a') or node.GetLevel() do. A +// call to one is seeded as ".name" taking the value first, which tells it +// from a function in a schema, as dbo.f(), the parser sees alike. This map +// recognizes receiver syntax before catalog lookup and must stay in sync +// with the leading-dot JSONL signatures. +var typeMethods = map[string]bool{ + "exist": true, + "getlevel": true, "tostring": true, "getancestor": true, "getdescendant": true, + "isdescendantof": true, "getreparentedvalue": true, + "stastext": true, "stasbinary": true, "starea": true, "stlength": true, + "stdistance": true, "stintersects": true, "stcontains": true, "stwithin": true, + "stequals": true, "stbuffer": true, "stunion": true, "stintersection": true, + "stnumpoints": true, "stgeometrytype": true, "stdimension": true, + "stisvalid": true, "stpointn": true, +} + +// convertMethodCall converts a call of a method on a value, or of a static +// one on a type, as geography::Point(...) is, seeded as "geography::point". +// xml's value(path, 'type') returns the type it names, or NULL when the +// path finds nothing. +func (c *cc) convertMethodCall(n *tsql.FunctionCall) ast.Node { + method := identifierValue(n.FunctionName) + var receiver ast.Node + switch t := n.CallTarget.(type) { + case *tsql.UserDefinedTypeCallTarget: + if t.SchemaObjectName == nil || t.SchemaObjectName.BaseIdentifier == nil { + return nil + } + fc := c.convertFunctionCall(n) + fc.Funcname = &ast.List{Items: []ast.Node{&ast.String{Str: identifierValue(t.SchemaObjectName.BaseIdentifier) + "::" + method}}} + return fc + case *tsql.MultiPartIdentifierCallTarget: + if t.MultiPartIdentifier == nil { + return nil + } + receiver = c.convertColumnReference(&tsql.ColumnReferenceExpression{MultiPartIdentifier: t.MultiPartIdentifier}) + case *tsql.ExpressionCallTarget: + receiver = c.convertScalarExpression(t.Expression) + default: + return nil + } + if method == "value" && len(n.Parameters) == 2 { + if lit, ok := n.Parameters[1].(*tsql.StringLiteral); ok { + return &ast.TypeCast{ + Arg: receiver, + TypeName: &ast.TypeName{Spelling: "Nullable(" + strings.ToLower(lit.Value) + ")"}, + Location: c.loc(n), + } + } + } + if !typeMethods[method] { + return nil + } + fc := c.convertFunctionCall(n) + fc.Funcname = &ast.List{Items: []ast.Node{&ast.String{Str: "." + method}}} + args := []ast.Node{receiver} + if fc.Args != nil { + args = append(args, fc.Args.Items...) + } + fc.Args = &ast.List{Items: args} + return fc +} + func (c *cc) convertFunctionCall(n *tsql.FunctionCall) *ast.FuncCall { fc := &ast.FuncCall{ Funcname: &ast.List{ @@ -734,7 +864,15 @@ func (c *cc) convertFunctionCall(n *tsql.FunctionCall) *ast.FuncCall { AggDistinct: n.UniqueRowFilter == "Distinct", } - for _, param := range n.Parameters { + for i, param := range n.Parameters { + // The datepart DATEADD and its relatives take first is a keyword, + // not a column: it is passed on as the string it names. + if i == 0 && datepartFunctions[identifierValue(n.FunctionName)] { + if part := datepart(param); part != "" { + fc.Args = &ast.List{Items: []ast.Node{&ast.A_Const{Val: &ast.String{Str: part}, Location: c.loc(param)}}} + continue + } + } // COUNT(*) and friends carry the star as a wildcard column reference. if col, ok := param.(*tsql.ColumnReferenceExpression); ok && col.ColumnType == "Wildcard" && col.MultiPartIdentifier == nil { fc.AggStar = true @@ -762,6 +900,29 @@ func (c *cc) convertFunctionCall(n *tsql.FunctionCall) *ast.FuncCall { return fc } +// datepartFunctions take a bare keyword which the parser sees as a column. +// JSONL signatures describe its string type, not this syntax; dialect +// ident_args describes type arguments, not function arguments. +var datepartFunctions = map[string]bool{ + "dateadd": true, + "datediff": true, + "datediff_big": true, + "datename": true, + "datepart": true, + "datetrunc": true, + "date_bucket": true, +} + +// datepart returns the datepart keyword an argument spells, as in +// DATEADD(day, ...), or "" when it is not a bare name. +func datepart(n tsql.ScalarExpression) string { + col, ok := n.(*tsql.ColumnReferenceExpression) + if !ok || col.MultiPartIdentifier == nil || len(col.MultiPartIdentifier.Identifiers) != 1 { + return "" + } + return identifierValue(col.MultiPartIdentifier.Identifiers[0]) +} + // 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. @@ -1184,6 +1345,20 @@ func (c *cc) convertAlterTableAddTableElementStatement(n *tsql.AlterTableAddTabl return stmt } +// namedCall is a call of name with args, for a function the parser gives a +// node of its own, as LEFT and RIGHT, which are keywords too. +func (c *cc) namedCall(name string, args []tsql.ScalarExpression, n tsql.Node) *ast.FuncCall { + call := &ast.FuncCall{ + Funcname: &ast.List{Items: []ast.Node{&ast.String{Str: name}}}, + Args: &ast.List{}, + Location: c.loc(n), + } + for _, arg := range args { + call.Args.Items = append(call.Args.Items, c.convertScalarExpression(arg)) + } + return call +} + 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..bd05f86922 100644 --- a/internal/engine/mssql/dialect/dialect.json +++ b/internal/engine/mssql/dialect/dialect.json @@ -8,6 +8,27 @@ "bool": "bit" }, "bool": "bit", + "set_operations": { + "precedence": [ + "sql_variant", "xml", "datetimeoffset", "datetime2", "datetime", + "smalldatetime", "date", "time", "float", "real", "decimal", "money", + "smallmoney", "bigint", "int", "smallint", "tinyint", "bit", "ntext", + "text", "image", "rowversion", "uniqueidentifier", "nvarchar", "nchar", + "varchar", "char", "varbinary", "binary" + ], + "varying": {"char": "varchar", "nchar": "nvarchar", "binary": "varbinary"}, + "decimal": "decimal", + "decimal_max_precision": 38, + "as_decimal": { + "bit": [1, 0], + "tinyint": [3, 0], + "smallint": [5, 0], + "int": [10, 0], + "bigint": [19, 0], + "money": [19, 4], + "smallmoney": [10, 4] + } + }, "comparison": ["=", "<>", "!=", "<", "<=", ">", ">="], "comparison_categories": "NBSD", "arithmetic": ["+", "-", "*", "/", "%"], diff --git a/internal/engine/mssql/dialect/functions.jsonl b/internal/engine/mssql/dialect/functions.jsonl index e0cd45408c..f3c47c5418 100644 --- a/internal/engine/mssql/dialect/functions.jsonl +++ b/internal/engine/mssql/dialect/functions.jsonl @@ -50,6 +50,28 @@ {"name": "max", "kind": "a", "args": [{"type": "nvarchar"}], "returns": "nvarchar", "nullable": true} {"name": "max", "kind": "a", "args": [{"type": "char"}], "returns": "char", "nullable": true} {"name": "max", "kind": "a", "args": [{"type": "nchar"}], "returns": "nchar", "nullable": true} +{"name": "string_agg", "kind": "a", "args": [{"type": "varchar"}, {"type": "varchar"}], "returns": "varchar(8000)", "nullable": true} +{"name": "string_agg", "kind": "a", "args": [{"type": "char"}, {"type": "varchar"}], "returns": "varchar(8000)", "nullable": true} +{"name": "string_agg", "kind": "a", "args": [{"type": "nvarchar"}, {"type": "varchar"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "string_agg", "kind": "a", "args": [{"type": "nchar"}, {"type": "varchar"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "stdev", "kind": "a", "args": [{"type": "any"}], "returns": "float", "nullable": true} +{"name": "stdevp", "kind": "a", "args": [{"type": "any"}], "returns": "float", "nullable": true} +{"name": "var", "kind": "a", "args": [{"type": "any"}], "returns": "float", "nullable": true} +{"name": "varp", "kind": "a", "args": [{"type": "any"}], "returns": "float", "nullable": true} +{"name": "row_number", "kind": "w", "returns": "bigint"} +{"name": "rank", "kind": "w", "returns": "bigint"} +{"name": "dense_rank", "kind": "w", "returns": "bigint"} +{"name": "ntile", "kind": "w", "args": [{"type": "bigint"}], "returns": "bigint"} +{"name": "percent_rank", "kind": "w", "returns": "float", "nullable": true} +{"name": "cume_dist", "kind": "w", "returns": "float", "nullable": true} +{"name": "lag", "kind": "w", "args": [{"type": "any"}], "returns": "$1", "nullable": true} +{"name": "lag", "kind": "w", "args": [{"type": "any"}, {"type": "bigint"}], "returns": "$1", "nullable": true} +{"name": "lag", "kind": "w", "args": [{"type": "any"}, {"type": "bigint"}, {"type": "any"}], "returns": "$1", "nullable": true} +{"name": "lead", "kind": "w", "args": [{"type": "any"}], "returns": "$1", "nullable": true} +{"name": "lead", "kind": "w", "args": [{"type": "any"}, {"type": "bigint"}], "returns": "$1", "nullable": true} +{"name": "lead", "kind": "w", "args": [{"type": "any"}, {"type": "bigint"}, {"type": "any"}], "returns": "$1", "nullable": true} +{"name": "first_value", "kind": "w", "args": [{"type": "any"}], "returns": "$1", "nullable": true} +{"name": "last_value", "kind": "w", "args": [{"type": "any"}], "returns": "$1", "nullable": true} {"name": "getdate", "returns": "datetime"} {"name": "getutcdate", "returns": "datetime"} {"name": "sysdatetime", "returns": "datetime2"} @@ -74,3 +96,290 @@ {"name": "rtrim", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} {"name": "trim", "args": [{"type": "varchar"}], "returns": "varchar"} {"name": "trim", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "nullif", "args": [{"type": "any"}, {"type": "any"}], "returns": "$1", "nullable": true} +{"name": "concat", "args": [{"type": "any"}, {"type": "any"}], "returns": "varchar"} +{"name": "concat", "args": [{"type": "nvarchar"}, {"type": "any"}], "returns": "nvarchar"} +{"name": "concat", "args": [{"type": "any"}, {"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "concat_ws", "args": [{"type": "any"}, {"type": "any"}, {"type": "any"}], "returns": "varchar"} +{"name": "concat_ws", "args": [{"type": "nvarchar"}, {"type": "any"}, {"type": "any"}], "returns": "nvarchar"} +{"name": "concat_ws", "args": [{"type": "any"}, {"type": "nvarchar"}, {"type": "any"}], "returns": "nvarchar"} +{"name": "concat_ws", "args": [{"type": "any"}, {"type": "any"}, {"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "substring", "args": [{"type": "varchar"}, {"type": "bigint"}, {"type": "bigint"}], "returns": "varchar", "nullable": true} +{"name": "substring", "args": [{"type": "char"}, {"type": "bigint"}, {"type": "bigint"}], "returns": "varchar", "nullable": true} +{"name": "substring", "args": [{"type": "nvarchar"}, {"type": "bigint"}, {"type": "bigint"}], "returns": "nvarchar", "nullable": true} +{"name": "substring", "args": [{"type": "nchar"}, {"type": "bigint"}, {"type": "bigint"}], "returns": "nvarchar", "nullable": true} +{"name": "substring", "args": [{"type": "varbinary"}, {"type": "bigint"}, {"type": "bigint"}], "returns": "varbinary", "nullable": true} +{"name": "substring", "args": [{"type": "binary"}, {"type": "bigint"}, {"type": "bigint"}], "returns": "varbinary", "nullable": true} +{"name": "left", "args": [{"type": "varchar"}, {"type": "int"}], "returns": "varchar", "nullable": true} +{"name": "left", "args": [{"type": "char"}, {"type": "int"}], "returns": "varchar", "nullable": true} +{"name": "left", "args": [{"type": "nvarchar"}, {"type": "int"}], "returns": "nvarchar", "nullable": true} +{"name": "left", "args": [{"type": "nchar"}, {"type": "int"}], "returns": "nvarchar", "nullable": true} +{"name": "right", "args": [{"type": "varchar"}, {"type": "int"}], "returns": "varchar", "nullable": true} +{"name": "right", "args": [{"type": "char"}, {"type": "int"}], "returns": "varchar", "nullable": true} +{"name": "right", "args": [{"type": "nvarchar"}, {"type": "int"}], "returns": "nvarchar", "nullable": true} +{"name": "right", "args": [{"type": "nchar"}, {"type": "int"}], "returns": "nvarchar", "nullable": true} +{"name": "replace", "args": [{"type": "varchar"}, {"type": "any"}, {"type": "any"}], "returns": "varchar(8000)", "nullable": true} +{"name": "replace", "args": [{"type": "char"}, {"type": "any"}, {"type": "any"}], "returns": "varchar(8000)", "nullable": true} +{"name": "replace", "args": [{"type": "nvarchar"}, {"type": "any"}, {"type": "any"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "replace", "args": [{"type": "nchar"}, {"type": "any"}, {"type": "any"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "translate", "args": [{"type": "varchar"}, {"type": "any"}, {"type": "any"}], "returns": "varchar(8000)", "nullable": true} +{"name": "translate", "args": [{"type": "nvarchar"}, {"type": "any"}, {"type": "any"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "replicate", "args": [{"type": "varchar"}, {"type": "int"}], "returns": "varchar", "nullable": true} +{"name": "replicate", "args": [{"type": "char"}, {"type": "int"}], "returns": "varchar", "nullable": true} +{"name": "replicate", "args": [{"type": "nvarchar"}, {"type": "int"}], "returns": "nvarchar", "nullable": true} +{"name": "replicate", "args": [{"type": "nchar"}, {"type": "int"}], "returns": "nvarchar", "nullable": true} +{"name": "reverse", "args": [{"type": "varchar"}], "returns": "$1", "nullable": true} +{"name": "reverse", "args": [{"type": "char"}], "returns": "varchar", "nullable": true} +{"name": "reverse", "args": [{"type": "nvarchar"}], "returns": "$1", "nullable": true} +{"name": "reverse", "args": [{"type": "nchar"}], "returns": "nvarchar", "nullable": true} +{"name": "stuff", "args": [{"type": "varchar"}, {"type": "bigint"}, {"type": "bigint"}, {"type": "any"}], "returns": "varchar", "nullable": true} +{"name": "stuff", "args": [{"type": "char"}, {"type": "bigint"}, {"type": "bigint"}, {"type": "any"}], "returns": "varchar", "nullable": true} +{"name": "stuff", "args": [{"type": "nvarchar"}, {"type": "bigint"}, {"type": "bigint"}, {"type": "any"}], "returns": "nvarchar", "nullable": true} +{"name": "stuff", "args": [{"type": "nchar"}, {"type": "bigint"}, {"type": "bigint"}, {"type": "any"}], "returns": "nvarchar", "nullable": true} +{"name": "space", "args": [{"type": "int"}], "returns": "varchar(8000)", "nullable": true} +{"name": "charindex", "args": [{"type": "any"}, {"type": "any"}], "returns": "int", "nullable": true} +{"name": "charindex", "args": [{"type": "any"}, {"type": "any"}, {"type": "int"}], "returns": "int", "nullable": true} +{"name": "patindex", "args": [{"type": "any"}, {"type": "any"}], "returns": "int", "nullable": true} +{"name": "ascii", "args": [{"type": "varchar"}], "returns": "int", "nullable": true} +{"name": "unicode", "args": [{"type": "nvarchar"}], "returns": "int", "nullable": true} +{"name": "format", "args": [{"type": "any"}, {"type": "nvarchar"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "format", "args": [{"type": "any"}, {"type": "nvarchar"}, {"type": "nvarchar"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "quotename", "args": [{"type": "nvarchar"}], "returns": "nvarchar(258)", "nullable": true} +{"name": "quotename", "args": [{"type": "nvarchar"}, {"type": "nvarchar"}], "returns": "nvarchar(258)", "nullable": true} +{"name": "str", "args": [{"type": "float"}], "returns": "varchar(10)", "nullable": true} +{"name": "str", "args": [{"type": "float"}, {"type": "int"}], "returns": "varchar($2)", "nullable": true} +{"name": "str", "args": [{"type": "float"}, {"type": "int"}, {"type": "int"}], "returns": "varchar($2)", "nullable": true} +{"name": "abs", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "abs", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "abs", "args": [{"type": "smallint"}], "returns": "int", "nullable": true} +{"name": "abs", "args": [{"type": "tinyint"}], "returns": "int", "nullable": true} +{"name": "abs", "args": [{"type": "decimal"}], "returns": "$1", "nullable": true} +{"name": "abs", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "abs", "args": [{"type": "smallmoney"}], "returns": "money", "nullable": true} +{"name": "abs", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "abs", "args": [{"type": "real"}], "returns": "float", "nullable": true} +{"name": "sign", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "sign", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "sign", "args": [{"type": "smallint"}], "returns": "int", "nullable": true} +{"name": "sign", "args": [{"type": "tinyint"}], "returns": "int", "nullable": true} +{"name": "sign", "args": [{"type": "decimal"}], "returns": "$1", "nullable": true} +{"name": "sign", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "sign", "args": [{"type": "smallmoney"}], "returns": "money", "nullable": true} +{"name": "sign", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "sign", "args": [{"type": "real"}], "returns": "float", "nullable": true} +{"name": "ceiling", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "ceiling", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "ceiling", "args": [{"type": "smallint"}], "returns": "int", "nullable": true} +{"name": "ceiling", "args": [{"type": "tinyint"}], "returns": "int", "nullable": true} +{"name": "ceiling", "args": [{"type": "decimal"}], "returns": "decimal", "nullable": true} +{"name": "ceiling", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "ceiling", "args": [{"type": "smallmoney"}], "returns": "money", "nullable": true} +{"name": "ceiling", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "ceiling", "args": [{"type": "real"}], "returns": "float", "nullable": true} +{"name": "floor", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "floor", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "floor", "args": [{"type": "smallint"}], "returns": "int", "nullable": true} +{"name": "floor", "args": [{"type": "tinyint"}], "returns": "int", "nullable": true} +{"name": "floor", "args": [{"type": "decimal"}], "returns": "decimal", "nullable": true} +{"name": "floor", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "floor", "args": [{"type": "smallmoney"}], "returns": "money", "nullable": true} +{"name": "floor", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "floor", "args": [{"type": "real"}], "returns": "float", "nullable": true} +{"name": "round", "args": [{"type": "bigint"}, {"type": "int"}], "returns": "bigint", "nullable": true} +{"name": "round", "args": [{"type": "int"}, {"type": "int"}], "returns": "int", "nullable": true} +{"name": "round", "args": [{"type": "smallint"}, {"type": "int"}], "returns": "int", "nullable": true} +{"name": "round", "args": [{"type": "tinyint"}, {"type": "int"}], "returns": "int", "nullable": true} +{"name": "round", "args": [{"type": "decimal"}, {"type": "int"}], "returns": "$1", "nullable": true} +{"name": "round", "args": [{"type": "money"}, {"type": "int"}], "returns": "money", "nullable": true} +{"name": "round", "args": [{"type": "smallmoney"}, {"type": "int"}], "returns": "money", "nullable": true} +{"name": "round", "args": [{"type": "float"}, {"type": "int"}], "returns": "float", "nullable": true} +{"name": "round", "args": [{"type": "real"}, {"type": "int"}], "returns": "float", "nullable": true} +{"name": "round", "args": [{"type": "bigint"}, {"type": "int"}, {"type": "int"}], "returns": "bigint", "nullable": true} +{"name": "round", "args": [{"type": "int"}, {"type": "int"}, {"type": "int"}], "returns": "int", "nullable": true} +{"name": "round", "args": [{"type": "smallint"}, {"type": "int"}, {"type": "int"}], "returns": "int", "nullable": true} +{"name": "round", "args": [{"type": "tinyint"}, {"type": "int"}, {"type": "int"}], "returns": "int", "nullable": true} +{"name": "round", "args": [{"type": "decimal"}, {"type": "int"}, {"type": "int"}], "returns": "$1", "nullable": true} +{"name": "round", "args": [{"type": "money"}, {"type": "int"}, {"type": "int"}], "returns": "money", "nullable": true} +{"name": "round", "args": [{"type": "smallmoney"}, {"type": "int"}, {"type": "int"}], "returns": "money", "nullable": true} +{"name": "round", "args": [{"type": "float"}, {"type": "int"}, {"type": "int"}], "returns": "float", "nullable": true} +{"name": "round", "args": [{"type": "real"}, {"type": "int"}, {"type": "int"}], "returns": "float", "nullable": true} +{"name": "power", "args": [{"type": "bigint"}, {"type": "any"}], "returns": "bigint", "nullable": true} +{"name": "power", "args": [{"type": "int"}, {"type": "any"}], "returns": "int", "nullable": true} +{"name": "power", "args": [{"type": "smallint"}, {"type": "any"}], "returns": "int", "nullable": true} +{"name": "power", "args": [{"type": "tinyint"}, {"type": "any"}], "returns": "int", "nullable": true} +{"name": "power", "args": [{"type": "decimal"}, {"type": "any"}], "returns": "decimal", "nullable": true} +{"name": "power", "args": [{"type": "money"}, {"type": "any"}], "returns": "money", "nullable": true} +{"name": "power", "args": [{"type": "smallmoney"}, {"type": "any"}], "returns": "money", "nullable": true} +{"name": "power", "args": [{"type": "float"}, {"type": "any"}], "returns": "float", "nullable": true} +{"name": "power", "args": [{"type": "real"}, {"type": "any"}], "returns": "float", "nullable": true} +{"name": "sqrt", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "square", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "exp", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "log", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "log", "args": [{"type": "float"}, {"type": "float"}], "returns": "float", "nullable": true} +{"name": "log10", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "pi", "returns": "float"} +{"name": "rand", "returns": "float", "nullable": true} +{"name": "rand", "args": [{"type": "int"}], "returns": "float", "nullable": true} +{"name": "year", "args": [{"type": "any"}], "returns": "int", "nullable": true} +{"name": "month", "args": [{"type": "any"}], "returns": "int", "nullable": true} +{"name": "day", "args": [{"type": "any"}], "returns": "int", "nullable": true} +{"name": "isdate", "args": [{"type": "any"}], "returns": "int", "nullable": true} +{"name": "eomonth", "args": [{"type": "any"}], "returns": "date", "nullable": true} +{"name": "eomonth", "args": [{"type": "any"}, {"type": "int"}], "returns": "date", "nullable": true} +{"name": "datefromparts", "args": [{"type": "int"}, {"type": "int"}, {"type": "int"}], "returns": "date", "nullable": true} +{"name": "datetimefromparts", "args": [{"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}], "returns": "datetime", "nullable": true} +{"name": "datetime2fromparts", "args": [{"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}], "returns": "datetime2($8)", "nullable": true} +{"name": "datetimeoffsetfromparts", "args": [{"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}], "returns": "datetimeoffset($10)", "nullable": true} +{"name": "timefromparts", "args": [{"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}, {"type": "int"}], "returns": "time($5)", "nullable": true} +{"name": "switchoffset", "args": [{"type": "datetimeoffset"}, {"type": "any"}], "returns": "$1", "nullable": true} +{"name": "todatetimeoffset", "args": [{"type": "any"}, {"type": "any"}], "returns": "datetimeoffset", "nullable": true} +{"name": "dateadd", "args": [{"type": "varchar"}, {"type": "int"}, {"type": "any"}], "returns": "$3", "nullable": true} +{"name": "dateadd", "args": [{"type": "varchar"}, {"type": "int"}, {"type": "varchar"}], "returns": "datetime", "nullable": true} +{"name": "dateadd", "args": [{"type": "varchar"}, {"type": "int"}, {"type": "nvarchar"}], "returns": "datetime", "nullable": true} +{"name": "datediff", "args": [{"type": "varchar"}, {"type": "any"}, {"type": "any"}], "returns": "int", "nullable": true} +{"name": "datediff_big", "args": [{"type": "varchar"}, {"type": "any"}, {"type": "any"}], "returns": "bigint", "nullable": true} +{"name": "datepart", "args": [{"type": "varchar"}, {"type": "any"}], "returns": "int", "nullable": true} +{"name": "datename", "args": [{"type": "varchar"}, {"type": "any"}], "returns": "nvarchar(30)", "nullable": true} +{"name": "datetrunc", "args": [{"type": "varchar"}, {"type": "any"}], "returns": "$2", "nullable": true} +{"name": "datetrunc", "args": [{"type": "varchar"}, {"type": "varchar"}], "returns": "datetime2(7)", "nullable": true} +{"name": "datetrunc", "args": [{"type": "varchar"}, {"type": "nvarchar"}], "returns": "datetime2(7)", "nullable": true} +{"name": "isjson", "args": [{"type": "any"}], "returns": "int", "nullable": true} +{"name": "isjson", "args": [{"type": "any"}, {"type": "varchar"}], "returns": "int", "nullable": true} +{"name": "json_value", "args": [{"type": "any"}, {"type": "nvarchar"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "json_query", "args": [{"type": "any"}], "returns": "nvarchar(max)", "nullable": true} +{"name": "json_query", "args": [{"type": "any"}, {"type": "nvarchar"}], "returns": "nvarchar(max)", "nullable": true} +{"name": "json_modify", "args": [{"type": "any"}, {"type": "nvarchar"}, {"type": "any"}], "returns": "nvarchar(max)", "nullable": true} +{"name": "json_object", "returns": "nvarchar(max)", "nullable": true} +{"name": "json_array", "returns": "nvarchar(max)", "nullable": true} +{"name": "hashbytes", "args": [{"type": "varchar"}, {"type": "any"}], "returns": "varbinary(8000)", "nullable": true} +{"name": "checksum", "args": [{"type": "any"}], "returns": "int", "nullable": true} +{"name": "binary_checksum", "args": [{"type": "any"}], "returns": "int", "nullable": true} +{"name": "at time zone", "args": [{"type": "datetimeoffset"}, {"type": "nvarchar(4000)"}], "returns": "datetimeoffset(7)", "nullable": true} +{"name": "at time zone", "args": [{"type": "datetime2"}, {"type": "nvarchar(4000)"}], "returns": "datetimeoffset(7)", "nullable": true} +{"name": "at time zone", "args": [{"type": "datetime"}, {"type": "nvarchar(4000)"}], "returns": "datetimeoffset(3)", "nullable": true} +{"name": "at time zone", "args": [{"type": "smalldatetime"}, {"type": "nvarchar(4000)"}], "returns": "datetimeoffset(0)", "nullable": true} +{"name": ".exist", "args": [{"type": "xml"}, {"type": "nvarchar(4000)"}], "returns": "bit", "nullable": true} +{"name": ".getlevel", "args": [{"type": "hierarchyid"}], "returns": "smallint", "nullable": true} +{"name": ".tostring", "args": [{"type": "hierarchyid"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": ".getancestor", "args": [{"type": "hierarchyid"}, {"type": "int"}], "returns": "hierarchyid", "nullable": true} +{"name": ".getdescendant", "args": [{"type": "hierarchyid"}, {"type": "hierarchyid"}, {"type": "hierarchyid"}], "returns": "hierarchyid", "nullable": true} +{"name": ".isdescendantof", "args": [{"type": "hierarchyid"}, {"type": "hierarchyid"}], "returns": "bit", "nullable": true} +{"name": ".getreparentedvalue", "args": [{"type": "hierarchyid"}, {"type": "hierarchyid"}, {"type": "hierarchyid"}], "returns": "hierarchyid", "nullable": true} +{"name": "hierarchyid::getroot", "args": [], "returns": "hierarchyid", "nullable": true} +{"name": "hierarchyid::parse", "args": [{"type": "nvarchar(4000)"}], "returns": "hierarchyid", "nullable": true} +{"name": ".tostring", "args": [{"type": "geography"}], "returns": "nvarchar(max)", "nullable": true} +{"name": ".stastext", "args": [{"type": "geography"}], "returns": "nvarchar(max)", "nullable": true} +{"name": ".stasbinary", "args": [{"type": "geography"}], "returns": "varbinary(max)", "nullable": true} +{"name": ".starea", "args": [{"type": "geography"}], "returns": "float", "nullable": true} +{"name": ".stlength", "args": [{"type": "geography"}], "returns": "float", "nullable": true} +{"name": ".stdistance", "args": [{"type": "geography"}, {"type": "geography"}], "returns": "float", "nullable": true} +{"name": ".stintersects", "args": [{"type": "geography"}, {"type": "geography"}], "returns": "bit", "nullable": true} +{"name": ".stcontains", "args": [{"type": "geography"}, {"type": "geography"}], "returns": "bit", "nullable": true} +{"name": ".stwithin", "args": [{"type": "geography"}, {"type": "geography"}], "returns": "bit", "nullable": true} +{"name": ".stequals", "args": [{"type": "geography"}, {"type": "geography"}], "returns": "bit", "nullable": true} +{"name": ".stisvalid", "args": [{"type": "geography"}], "returns": "bit", "nullable": true} +{"name": ".stbuffer", "args": [{"type": "geography"}, {"type": "float"}], "returns": "geography", "nullable": true} +{"name": ".stunion", "args": [{"type": "geography"}, {"type": "geography"}], "returns": "geography", "nullable": true} +{"name": ".stintersection", "args": [{"type": "geography"}, {"type": "geography"}], "returns": "geography", "nullable": true} +{"name": ".stnumpoints", "args": [{"type": "geography"}], "returns": "int", "nullable": true} +{"name": ".stdimension", "args": [{"type": "geography"}], "returns": "int", "nullable": true} +{"name": ".stgeometrytype", "args": [{"type": "geography"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": ".stpointn", "args": [{"type": "geography"}, {"type": "int"}], "returns": "geography", "nullable": true} +{"name": "geography::stgeomfromtext", "args": [{"type": "nvarchar(max)"}, {"type": "int"}], "returns": "geography", "nullable": true} +{"name": "geography::stpointfromtext", "args": [{"type": "nvarchar(max)"}, {"type": "int"}], "returns": "geography", "nullable": true} +{"name": ".tostring", "args": [{"type": "geometry"}], "returns": "nvarchar(max)", "nullable": true} +{"name": ".stastext", "args": [{"type": "geometry"}], "returns": "nvarchar(max)", "nullable": true} +{"name": ".stasbinary", "args": [{"type": "geometry"}], "returns": "varbinary(max)", "nullable": true} +{"name": ".starea", "args": [{"type": "geometry"}], "returns": "float", "nullable": true} +{"name": ".stlength", "args": [{"type": "geometry"}], "returns": "float", "nullable": true} +{"name": ".stdistance", "args": [{"type": "geometry"}, {"type": "geometry"}], "returns": "float", "nullable": true} +{"name": ".stintersects", "args": [{"type": "geometry"}, {"type": "geometry"}], "returns": "bit", "nullable": true} +{"name": ".stcontains", "args": [{"type": "geometry"}, {"type": "geometry"}], "returns": "bit", "nullable": true} +{"name": ".stwithin", "args": [{"type": "geometry"}, {"type": "geometry"}], "returns": "bit", "nullable": true} +{"name": ".stequals", "args": [{"type": "geometry"}, {"type": "geometry"}], "returns": "bit", "nullable": true} +{"name": ".stisvalid", "args": [{"type": "geometry"}], "returns": "bit", "nullable": true} +{"name": ".stbuffer", "args": [{"type": "geometry"}, {"type": "float"}], "returns": "geometry", "nullable": true} +{"name": ".stunion", "args": [{"type": "geometry"}, {"type": "geometry"}], "returns": "geometry", "nullable": true} +{"name": ".stintersection", "args": [{"type": "geometry"}, {"type": "geometry"}], "returns": "geometry", "nullable": true} +{"name": ".stnumpoints", "args": [{"type": "geometry"}], "returns": "int", "nullable": true} +{"name": ".stdimension", "args": [{"type": "geometry"}], "returns": "int", "nullable": true} +{"name": ".stgeometrytype", "args": [{"type": "geometry"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": ".stpointn", "args": [{"type": "geometry"}, {"type": "int"}], "returns": "geometry", "nullable": true} +{"name": "geometry::stgeomfromtext", "args": [{"type": "nvarchar(max)"}, {"type": "int"}], "returns": "geometry", "nullable": true} +{"name": "geometry::stpointfromtext", "args": [{"type": "nvarchar(max)"}, {"type": "int"}], "returns": "geometry", "nullable": true} +{"name": "geography::point", "args": [{"type": "float"}, {"type": "float"}, {"type": "int"}], "returns": "geography", "nullable": true} +{"name": "grouping", "kind": "a", "args": [{"type": "any"}], "returns": "tinyint", "never_null": true} +{"name": "grouping_id", "kind": "a", "args": [{"type": "any"}, {"type": "any", "mode": "v"}], "returns": "int", "never_null": true} +{"name": "approx_count_distinct", "kind": "a", "args": [{"type": "any"}], "returns": "bigint", "nullable": true} +{"name": "checksum_agg", "kind": "a", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "percentile_cont", "kind": "w", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "ident_current", "args": [{"type": "nvarchar"}], "returns": "decimal(38,0)", "nullable": true} +{"name": "ident_seed", "args": [{"type": "nvarchar"}], "returns": "decimal(38,0)", "nullable": true} +{"name": "ident_incr", "args": [{"type": "nvarchar"}], "returns": "decimal(38,0)", "nullable": true} +{"name": "rowcount_big", "returns": "bigint", "never_null": true} +{"name": "db_name", "returns": "nvarchar(128)", "nullable": true} +{"name": "suser_sname", "returns": "nvarchar(128)", "nullable": true} +{"name": "schema_name", "returns": "nvarchar(128)", "nullable": true} +{"name": "user_name", "returns": "nvarchar(128)", "nullable": true} +{"name": "db_name", "args": [{"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "suser_sname", "args": [{"type": "varbinary"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "schema_name", "args": [{"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "user_name", "args": [{"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "host_name", "returns": "nvarchar(128)", "nullable": true} +{"name": "app_name", "returns": "nvarchar(128)", "nullable": true} +{"name": "suser_name", "returns": "nvarchar(128)", "nullable": true} +{"name": "current_timezone", "returns": "nvarchar(128)", "nullable": true} +{"name": "current_timezone_id", "returns": "nvarchar(128)", "nullable": true} +{"name": "error_procedure", "returns": "nvarchar(128)", "nullable": true} +{"name": "original_login", "returns": "nvarchar(4000)", "nullable": true} +{"name": "object_id", "args": [{"type": "nvarchar"}], "returns": "int", "nullable": true} +{"name": "object_id", "args": [{"type": "nvarchar"}, {"type": "nvarchar"}], "returns": "int", "nullable": true} +{"name": "object_name", "args": [{"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "object_name", "args": [{"type": "int"}, {"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "object_schema_name", "args": [{"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "object_schema_name", "args": [{"type": "int"}, {"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "object_definition", "args": [{"type": "int"}], "returns": "nvarchar(max)", "nullable": true} +{"name": "db_id", "returns": "smallint", "nullable": true} +{"name": "db_id", "args": [{"type": "nvarchar"}], "returns": "smallint", "nullable": true} +{"name": "user_id", "returns": "smallint", "nullable": true} +{"name": "user_id", "args": [{"type": "nvarchar"}], "returns": "smallint", "nullable": true} +{"name": "suser_id", "returns": "int", "nullable": true} +{"name": "suser_id", "args": [{"type": "nvarchar"}], "returns": "int", "nullable": true} +{"name": "schema_id", "returns": "int", "nullable": true} +{"name": "schema_id", "args": [{"type": "nvarchar"}], "returns": "int", "nullable": true} +{"name": "suser_sid", "returns": "varbinary(85)", "nullable": true} +{"name": "col_length", "args": [{"type": "nvarchar"}, {"type": "nvarchar"}], "returns": "smallint", "nullable": true} +{"name": "col_name", "args": [{"type": "int"}, {"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "type_name", "args": [{"type": "int"}], "returns": "nvarchar(128)", "nullable": true} +{"name": "error_number", "returns": "int", "nullable": true} +{"name": "error_severity", "returns": "int", "nullable": true} +{"name": "error_state", "returns": "int", "nullable": true} +{"name": "error_line", "returns": "int", "nullable": true} +{"name": "error_message", "returns": "nvarchar(4000)", "nullable": true} +{"name": "xact_state", "returns": "smallint", "nullable": true} +{"name": "session_context", "args": [{"type": "nvarchar"}], "returns": "sql_variant", "nullable": true} +{"name": "context_info", "returns": "varbinary(128)", "nullable": true} +{"name": "sql_variant_property", "args": [{"type": "any"}, {"type": "varchar"}], "returns": "sql_variant", "nullable": true} +{"name": "string_escape", "args": [{"type": "nvarchar"}, {"type": "varchar"}], "returns": "nvarchar(max)", "nullable": true} +{"name": "soundex", "args": [{"type": "varchar"}], "returns": "varchar(5)", "nullable": true} +{"name": "difference", "args": [{"type": "varchar"}, {"type": "varchar"}], "returns": "int", "nullable": true} +{"name": "formatmessage", "args": [{"type": "any"}, {"type": "any", "mode": "v"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "ltrim", "args": [{"type": "any"}, {"type": "any"}], "returns": "$1", "nullable": true} +{"name": "rtrim", "args": [{"type": "any"}, {"type": "any"}], "returns": "$1", "nullable": true} +{"name": "compress", "args": [{"type": "any"}], "returns": "varbinary(max)", "nullable": true} +{"name": "decompress", "args": [{"type": "any"}], "returns": "varbinary(max)", "nullable": true} +{"name": "bit_count", "args": [{"type": "any"}], "returns": "bigint", "nullable": true} +{"name": "left_shift", "args": [{"type": "any"}, {"type": "int"}], "returns": "$1", "nullable": true} +{"name": "right_shift", "args": [{"type": "any"}, {"type": "int"}], "returns": "$1", "nullable": true} +{"name": "get_bit", "args": [{"type": "any"}, {"type": "int"}], "returns": "bit", "nullable": true} +{"name": "set_bit", "args": [{"type": "any"}, {"type": "int"}], "returns": "$1", "nullable": true} +{"name": "set_bit", "args": [{"type": "any"}, {"type": "int"}, {"type": "int"}], "returns": "$1", "nullable": true} +{"name": "date_bucket", "args": [{"type": "varchar"}, {"type": "int"}, {"type": "any"}], "returns": "$3", "nullable": true} +{"name": "date_bucket", "args": [{"type": "varchar"}, {"type": "int"}, {"type": "any"}, {"type": "any"}], "returns": "$3", "nullable": true} +{"name": "regexp_replace", "args": [{"type": "any"}, {"type": "any"}, {"type": "any", "mode": "v"}], "returns": "nvarchar(4000)", "nullable": true} +{"name": "regexp_substr", "args": [{"type": "any"}, {"type": "any"}, {"type": "any", "mode": "v"}], "returns": "$1", "nullable": true} +{"name": "regexp_instr", "args": [{"type": "any"}, {"type": "any"}, {"type": "any", "mode": "v"}], "returns": "int", "nullable": true} +{"name": "regexp_count", "args": [{"type": "any"}, {"type": "any"}, {"type": "any", "mode": "v"}], "returns": "int", "nullable": true} +{"name": "vector_distance", "args": [{"type": "varchar"}, {"type": "vector"}, {"type": "vector"}], "returns": "float", "nullable": true} +{"name": "trim", "args": [{"type": "any"}, {"type": "any"}], "returns": "$2"} diff --git a/internal/engine/mssql/dialect/operators.jsonl b/internal/engine/mssql/dialect/operators.jsonl new file mode 100644 index 0000000000..8d241329af --- /dev/null +++ b/internal/engine/mssql/dialect/operators.jsonl @@ -0,0 +1,432 @@ +{"name": "+", "left": "float", "right": "real", "result": "float"} +{"name": "+", "left": "float", "right": "decimal", "result": "float"} +{"name": "+", "left": "float", "right": "money", "result": "float"} +{"name": "+", "left": "float", "right": "smallmoney", "result": "float"} +{"name": "+", "left": "float", "right": "bigint", "result": "float"} +{"name": "+", "left": "float", "right": "int", "result": "float"} +{"name": "+", "left": "float", "right": "smallint", "result": "float"} +{"name": "+", "left": "float", "right": "tinyint", "result": "float"} +{"name": "+", "left": "float", "right": "bit", "result": "float"} +{"name": "+", "left": "real", "right": "float", "result": "float"} +{"name": "+", "left": "real", "right": "decimal", "result": "real"} +{"name": "+", "left": "real", "right": "money", "result": "real"} +{"name": "+", "left": "real", "right": "smallmoney", "result": "real"} +{"name": "+", "left": "real", "right": "bigint", "result": "real"} +{"name": "+", "left": "real", "right": "int", "result": "real"} +{"name": "+", "left": "real", "right": "smallint", "result": "real"} +{"name": "+", "left": "real", "right": "tinyint", "result": "real"} +{"name": "+", "left": "real", "right": "bit", "result": "real"} +{"name": "+", "left": "decimal", "right": "float", "result": "float"} +{"name": "+", "left": "decimal", "right": "real", "result": "real"} +{"name": "+", "left": "decimal", "right": "money", "result": "decimal"} +{"name": "+", "left": "decimal", "right": "smallmoney", "result": "decimal"} +{"name": "+", "left": "decimal", "right": "bigint", "result": "decimal"} +{"name": "+", "left": "decimal", "right": "int", "result": "decimal"} +{"name": "+", "left": "decimal", "right": "smallint", "result": "decimal"} +{"name": "+", "left": "decimal", "right": "tinyint", "result": "decimal"} +{"name": "+", "left": "decimal", "right": "bit", "result": "decimal"} +{"name": "+", "left": "money", "right": "float", "result": "float"} +{"name": "+", "left": "money", "right": "real", "result": "real"} +{"name": "+", "left": "money", "right": "decimal", "result": "decimal"} +{"name": "+", "left": "money", "right": "smallmoney", "result": "money"} +{"name": "+", "left": "money", "right": "bigint", "result": "money"} +{"name": "+", "left": "money", "right": "int", "result": "money"} +{"name": "+", "left": "money", "right": "smallint", "result": "money"} +{"name": "+", "left": "money", "right": "tinyint", "result": "money"} +{"name": "+", "left": "money", "right": "bit", "result": "money"} +{"name": "+", "left": "smallmoney", "right": "float", "result": "float"} +{"name": "+", "left": "smallmoney", "right": "real", "result": "real"} +{"name": "+", "left": "smallmoney", "right": "decimal", "result": "decimal"} +{"name": "+", "left": "smallmoney", "right": "money", "result": "money"} +{"name": "+", "left": "smallmoney", "right": "bigint", "result": "smallmoney"} +{"name": "+", "left": "smallmoney", "right": "int", "result": "smallmoney"} +{"name": "+", "left": "smallmoney", "right": "smallint", "result": "smallmoney"} +{"name": "+", "left": "smallmoney", "right": "tinyint", "result": "smallmoney"} +{"name": "+", "left": "smallmoney", "right": "bit", "result": "smallmoney"} +{"name": "+", "left": "bigint", "right": "float", "result": "float"} +{"name": "+", "left": "bigint", "right": "real", "result": "real"} +{"name": "+", "left": "bigint", "right": "decimal", "result": "decimal"} +{"name": "+", "left": "bigint", "right": "money", "result": "money"} +{"name": "+", "left": "bigint", "right": "smallmoney", "result": "smallmoney"} +{"name": "+", "left": "bigint", "right": "int", "result": "bigint"} +{"name": "+", "left": "bigint", "right": "smallint", "result": "bigint"} +{"name": "+", "left": "bigint", "right": "tinyint", "result": "bigint"} +{"name": "+", "left": "bigint", "right": "bit", "result": "bigint"} +{"name": "+", "left": "int", "right": "float", "result": "float"} +{"name": "+", "left": "int", "right": "real", "result": "real"} +{"name": "+", "left": "int", "right": "decimal", "result": "decimal"} +{"name": "+", "left": "int", "right": "money", "result": "money"} +{"name": "+", "left": "int", "right": "smallmoney", "result": "smallmoney"} +{"name": "+", "left": "int", "right": "bigint", "result": "bigint"} +{"name": "+", "left": "int", "right": "smallint", "result": "int"} +{"name": "+", "left": "int", "right": "tinyint", "result": "int"} +{"name": "+", "left": "int", "right": "bit", "result": "int"} +{"name": "+", "left": "smallint", "right": "float", "result": "float"} +{"name": "+", "left": "smallint", "right": "real", "result": "real"} +{"name": "+", "left": "smallint", "right": "decimal", "result": "decimal"} +{"name": "+", "left": "smallint", "right": "money", "result": "money"} +{"name": "+", "left": "smallint", "right": "smallmoney", "result": "smallmoney"} +{"name": "+", "left": "smallint", "right": "bigint", "result": "bigint"} +{"name": "+", "left": "smallint", "right": "int", "result": "int"} +{"name": "+", "left": "smallint", "right": "tinyint", "result": "smallint"} +{"name": "+", "left": "smallint", "right": "bit", "result": "smallint"} +{"name": "+", "left": "tinyint", "right": "float", "result": "float"} +{"name": "+", "left": "tinyint", "right": "real", "result": "real"} +{"name": "+", "left": "tinyint", "right": "decimal", "result": "decimal"} +{"name": "+", "left": "tinyint", "right": "money", "result": "money"} +{"name": "+", "left": "tinyint", "right": "smallmoney", "result": "smallmoney"} +{"name": "+", "left": "tinyint", "right": "bigint", "result": "bigint"} +{"name": "+", "left": "tinyint", "right": "int", "result": "int"} +{"name": "+", "left": "tinyint", "right": "smallint", "result": "smallint"} +{"name": "+", "left": "tinyint", "right": "bit", "result": "tinyint"} +{"name": "+", "left": "bit", "right": "float", "result": "float"} +{"name": "+", "left": "bit", "right": "real", "result": "real"} +{"name": "+", "left": "bit", "right": "decimal", "result": "decimal"} +{"name": "+", "left": "bit", "right": "money", "result": "money"} +{"name": "+", "left": "bit", "right": "smallmoney", "result": "smallmoney"} +{"name": "+", "left": "bit", "right": "bigint", "result": "bigint"} +{"name": "+", "left": "bit", "right": "int", "result": "int"} +{"name": "+", "left": "bit", "right": "smallint", "result": "smallint"} +{"name": "+", "left": "bit", "right": "tinyint", "result": "tinyint"} +{"name": "-", "left": "float", "right": "real", "result": "float"} +{"name": "-", "left": "float", "right": "decimal", "result": "float"} +{"name": "-", "left": "float", "right": "money", "result": "float"} +{"name": "-", "left": "float", "right": "smallmoney", "result": "float"} +{"name": "-", "left": "float", "right": "bigint", "result": "float"} +{"name": "-", "left": "float", "right": "int", "result": "float"} +{"name": "-", "left": "float", "right": "smallint", "result": "float"} +{"name": "-", "left": "float", "right": "tinyint", "result": "float"} +{"name": "-", "left": "float", "right": "bit", "result": "float"} +{"name": "-", "left": "real", "right": "float", "result": "float"} +{"name": "-", "left": "real", "right": "decimal", "result": "real"} +{"name": "-", "left": "real", "right": "money", "result": "real"} +{"name": "-", "left": "real", "right": "smallmoney", "result": "real"} +{"name": "-", "left": "real", "right": "bigint", "result": "real"} +{"name": "-", "left": "real", "right": "int", "result": "real"} +{"name": "-", "left": "real", "right": "smallint", "result": "real"} +{"name": "-", "left": "real", "right": "tinyint", "result": "real"} +{"name": "-", "left": "real", "right": "bit", "result": "real"} +{"name": "-", "left": "decimal", "right": "float", "result": "float"} +{"name": "-", "left": "decimal", "right": "real", "result": "real"} +{"name": "-", "left": "decimal", "right": "money", "result": "decimal"} +{"name": "-", "left": "decimal", "right": "smallmoney", "result": "decimal"} +{"name": "-", "left": "decimal", "right": "bigint", "result": "decimal"} +{"name": "-", "left": "decimal", "right": "int", "result": "decimal"} +{"name": "-", "left": "decimal", "right": "smallint", "result": "decimal"} +{"name": "-", "left": "decimal", "right": "tinyint", "result": "decimal"} +{"name": "-", "left": "decimal", "right": "bit", "result": "decimal"} +{"name": "-", "left": "money", "right": "float", "result": "float"} +{"name": "-", "left": "money", "right": "real", "result": "real"} +{"name": "-", "left": "money", "right": "decimal", "result": "decimal"} +{"name": "-", "left": "money", "right": "smallmoney", "result": "money"} +{"name": "-", "left": "money", "right": "bigint", "result": "money"} +{"name": "-", "left": "money", "right": "int", "result": "money"} +{"name": "-", "left": "money", "right": "smallint", "result": "money"} +{"name": "-", "left": "money", "right": "tinyint", "result": "money"} +{"name": "-", "left": "money", "right": "bit", "result": "money"} +{"name": "-", "left": "smallmoney", "right": "float", "result": "float"} +{"name": "-", "left": "smallmoney", "right": "real", "result": "real"} +{"name": "-", "left": "smallmoney", "right": "decimal", "result": "decimal"} +{"name": "-", "left": "smallmoney", "right": "money", "result": "money"} +{"name": "-", "left": "smallmoney", "right": "bigint", "result": "smallmoney"} +{"name": "-", "left": "smallmoney", "right": "int", "result": "smallmoney"} +{"name": "-", "left": "smallmoney", "right": "smallint", "result": "smallmoney"} +{"name": "-", "left": "smallmoney", "right": "tinyint", "result": "smallmoney"} +{"name": "-", "left": "smallmoney", "right": "bit", "result": "smallmoney"} +{"name": "-", "left": "bigint", "right": "float", "result": "float"} +{"name": "-", "left": "bigint", "right": "real", "result": "real"} +{"name": "-", "left": "bigint", "right": "decimal", "result": "decimal"} +{"name": "-", "left": "bigint", "right": "money", "result": "money"} +{"name": "-", "left": "bigint", "right": "smallmoney", "result": "smallmoney"} +{"name": "-", "left": "bigint", "right": "int", "result": "bigint"} +{"name": "-", "left": "bigint", "right": "smallint", "result": "bigint"} +{"name": "-", "left": "bigint", "right": "tinyint", "result": "bigint"} +{"name": "-", "left": "bigint", "right": "bit", "result": "bigint"} +{"name": "-", "left": "int", "right": "float", "result": "float"} +{"name": "-", "left": "int", "right": "real", "result": "real"} +{"name": "-", "left": "int", "right": "decimal", "result": "decimal"} +{"name": "-", "left": "int", "right": "money", "result": "money"} +{"name": "-", "left": "int", "right": "smallmoney", "result": "smallmoney"} +{"name": "-", "left": "int", "right": "bigint", "result": "bigint"} +{"name": "-", "left": "int", "right": "smallint", "result": "int"} +{"name": "-", "left": "int", "right": "tinyint", "result": "int"} +{"name": "-", "left": "int", "right": "bit", "result": "int"} +{"name": "-", "left": "smallint", "right": "float", "result": "float"} +{"name": "-", "left": "smallint", "right": "real", "result": "real"} +{"name": "-", "left": "smallint", "right": "decimal", "result": "decimal"} +{"name": "-", "left": "smallint", "right": "money", "result": "money"} +{"name": "-", "left": "smallint", "right": "smallmoney", "result": "smallmoney"} +{"name": "-", "left": "smallint", "right": "bigint", "result": "bigint"} +{"name": "-", "left": "smallint", "right": "int", "result": "int"} +{"name": "-", "left": "smallint", "right": "tinyint", "result": "smallint"} +{"name": "-", "left": "smallint", "right": "bit", "result": "smallint"} +{"name": "-", "left": "tinyint", "right": "float", "result": "float"} +{"name": "-", "left": "tinyint", "right": "real", "result": "real"} +{"name": "-", "left": "tinyint", "right": "decimal", "result": "decimal"} +{"name": "-", "left": "tinyint", "right": "money", "result": "money"} +{"name": "-", "left": "tinyint", "right": "smallmoney", "result": "smallmoney"} +{"name": "-", "left": "tinyint", "right": "bigint", "result": "bigint"} +{"name": "-", "left": "tinyint", "right": "int", "result": "int"} +{"name": "-", "left": "tinyint", "right": "smallint", "result": "smallint"} +{"name": "-", "left": "tinyint", "right": "bit", "result": "tinyint"} +{"name": "-", "left": "bit", "right": "float", "result": "float"} +{"name": "-", "left": "bit", "right": "real", "result": "real"} +{"name": "-", "left": "bit", "right": "decimal", "result": "decimal"} +{"name": "-", "left": "bit", "right": "money", "result": "money"} +{"name": "-", "left": "bit", "right": "smallmoney", "result": "smallmoney"} +{"name": "-", "left": "bit", "right": "bigint", "result": "bigint"} +{"name": "-", "left": "bit", "right": "int", "result": "int"} +{"name": "-", "left": "bit", "right": "smallint", "result": "smallint"} +{"name": "-", "left": "bit", "right": "tinyint", "result": "tinyint"} +{"name": "*", "left": "float", "right": "real", "result": "float"} +{"name": "*", "left": "float", "right": "decimal", "result": "float"} +{"name": "*", "left": "float", "right": "money", "result": "float"} +{"name": "*", "left": "float", "right": "smallmoney", "result": "float"} +{"name": "*", "left": "float", "right": "bigint", "result": "float"} +{"name": "*", "left": "float", "right": "int", "result": "float"} +{"name": "*", "left": "float", "right": "smallint", "result": "float"} +{"name": "*", "left": "float", "right": "tinyint", "result": "float"} +{"name": "*", "left": "float", "right": "bit", "result": "float"} +{"name": "*", "left": "real", "right": "float", "result": "float"} +{"name": "*", "left": "real", "right": "decimal", "result": "real"} +{"name": "*", "left": "real", "right": "money", "result": "real"} +{"name": "*", "left": "real", "right": "smallmoney", "result": "real"} +{"name": "*", "left": "real", "right": "bigint", "result": "real"} +{"name": "*", "left": "real", "right": "int", "result": "real"} +{"name": "*", "left": "real", "right": "smallint", "result": "real"} +{"name": "*", "left": "real", "right": "tinyint", "result": "real"} +{"name": "*", "left": "real", "right": "bit", "result": "real"} +{"name": "*", "left": "decimal", "right": "float", "result": "float"} +{"name": "*", "left": "decimal", "right": "real", "result": "real"} +{"name": "*", "left": "decimal", "right": "money", "result": "decimal"} +{"name": "*", "left": "decimal", "right": "smallmoney", "result": "decimal"} +{"name": "*", "left": "decimal", "right": "bigint", "result": "decimal"} +{"name": "*", "left": "decimal", "right": "int", "result": "decimal"} +{"name": "*", "left": "decimal", "right": "smallint", "result": "decimal"} +{"name": "*", "left": "decimal", "right": "tinyint", "result": "decimal"} +{"name": "*", "left": "decimal", "right": "bit", "result": "decimal"} +{"name": "*", "left": "money", "right": "float", "result": "float"} +{"name": "*", "left": "money", "right": "real", "result": "real"} +{"name": "*", "left": "money", "right": "decimal", "result": "decimal"} +{"name": "*", "left": "money", "right": "smallmoney", "result": "money"} +{"name": "*", "left": "money", "right": "bigint", "result": "money"} +{"name": "*", "left": "money", "right": "int", "result": "money"} +{"name": "*", "left": "money", "right": "smallint", "result": "money"} +{"name": "*", "left": "money", "right": "tinyint", "result": "money"} +{"name": "*", "left": "money", "right": "bit", "result": "money"} +{"name": "*", "left": "smallmoney", "right": "float", "result": "float"} +{"name": "*", "left": "smallmoney", "right": "real", "result": "real"} +{"name": "*", "left": "smallmoney", "right": "decimal", "result": "decimal"} +{"name": "*", "left": "smallmoney", "right": "money", "result": "money"} +{"name": "*", "left": "smallmoney", "right": "bigint", "result": "smallmoney"} +{"name": "*", "left": "smallmoney", "right": "int", "result": "smallmoney"} +{"name": "*", "left": "smallmoney", "right": "smallint", "result": "smallmoney"} +{"name": "*", "left": "smallmoney", "right": "tinyint", "result": "smallmoney"} +{"name": "*", "left": "smallmoney", "right": "bit", "result": "smallmoney"} +{"name": "*", "left": "bigint", "right": "float", "result": "float"} +{"name": "*", "left": "bigint", "right": "real", "result": "real"} +{"name": "*", "left": "bigint", "right": "decimal", "result": "decimal"} +{"name": "*", "left": "bigint", "right": "money", "result": "money"} +{"name": "*", "left": "bigint", "right": "smallmoney", "result": "smallmoney"} +{"name": "*", "left": "bigint", "right": "int", "result": "bigint"} +{"name": "*", "left": "bigint", "right": "smallint", "result": "bigint"} +{"name": "*", "left": "bigint", "right": "tinyint", "result": "bigint"} +{"name": "*", "left": "bigint", "right": "bit", "result": "bigint"} +{"name": "*", "left": "int", "right": "float", "result": "float"} +{"name": "*", "left": "int", "right": "real", "result": "real"} +{"name": "*", "left": "int", "right": "decimal", "result": "decimal"} +{"name": "*", "left": "int", "right": "money", "result": "money"} +{"name": "*", "left": "int", "right": "smallmoney", "result": "smallmoney"} +{"name": "*", "left": "int", "right": "bigint", "result": "bigint"} +{"name": "*", "left": "int", "right": "smallint", "result": "int"} +{"name": "*", "left": "int", "right": "tinyint", "result": "int"} +{"name": "*", "left": "int", "right": "bit", "result": "int"} +{"name": "*", "left": "smallint", "right": "float", "result": "float"} +{"name": "*", "left": "smallint", "right": "real", "result": "real"} +{"name": "*", "left": "smallint", "right": "decimal", "result": "decimal"} +{"name": "*", "left": "smallint", "right": "money", "result": "money"} +{"name": "*", "left": "smallint", "right": "smallmoney", "result": "smallmoney"} +{"name": "*", "left": "smallint", "right": "bigint", "result": "bigint"} +{"name": "*", "left": "smallint", "right": "int", "result": "int"} +{"name": "*", "left": "smallint", "right": "tinyint", "result": "smallint"} +{"name": "*", "left": "smallint", "right": "bit", "result": "smallint"} +{"name": "*", "left": "tinyint", "right": "float", "result": "float"} +{"name": "*", "left": "tinyint", "right": "real", "result": "real"} +{"name": "*", "left": "tinyint", "right": "decimal", "result": "decimal"} +{"name": "*", "left": "tinyint", "right": "money", "result": "money"} +{"name": "*", "left": "tinyint", "right": "smallmoney", "result": "smallmoney"} +{"name": "*", "left": "tinyint", "right": "bigint", "result": "bigint"} +{"name": "*", "left": "tinyint", "right": "int", "result": "int"} +{"name": "*", "left": "tinyint", "right": "smallint", "result": "smallint"} +{"name": "*", "left": "tinyint", "right": "bit", "result": "tinyint"} +{"name": "*", "left": "bit", "right": "float", "result": "float"} +{"name": "*", "left": "bit", "right": "real", "result": "real"} +{"name": "*", "left": "bit", "right": "decimal", "result": "decimal"} +{"name": "*", "left": "bit", "right": "money", "result": "money"} +{"name": "*", "left": "bit", "right": "smallmoney", "result": "smallmoney"} +{"name": "*", "left": "bit", "right": "bigint", "result": "bigint"} +{"name": "*", "left": "bit", "right": "int", "result": "int"} +{"name": "*", "left": "bit", "right": "smallint", "result": "smallint"} +{"name": "*", "left": "bit", "right": "tinyint", "result": "tinyint"} +{"name": "/", "left": "float", "right": "real", "result": "float"} +{"name": "/", "left": "float", "right": "decimal", "result": "float"} +{"name": "/", "left": "float", "right": "money", "result": "float"} +{"name": "/", "left": "float", "right": "smallmoney", "result": "float"} +{"name": "/", "left": "float", "right": "bigint", "result": "float"} +{"name": "/", "left": "float", "right": "int", "result": "float"} +{"name": "/", "left": "float", "right": "smallint", "result": "float"} +{"name": "/", "left": "float", "right": "tinyint", "result": "float"} +{"name": "/", "left": "float", "right": "bit", "result": "float"} +{"name": "/", "left": "real", "right": "float", "result": "float"} +{"name": "/", "left": "real", "right": "decimal", "result": "real"} +{"name": "/", "left": "real", "right": "money", "result": "real"} +{"name": "/", "left": "real", "right": "smallmoney", "result": "real"} +{"name": "/", "left": "real", "right": "bigint", "result": "real"} +{"name": "/", "left": "real", "right": "int", "result": "real"} +{"name": "/", "left": "real", "right": "smallint", "result": "real"} +{"name": "/", "left": "real", "right": "tinyint", "result": "real"} +{"name": "/", "left": "real", "right": "bit", "result": "real"} +{"name": "/", "left": "decimal", "right": "float", "result": "float"} +{"name": "/", "left": "decimal", "right": "real", "result": "real"} +{"name": "/", "left": "decimal", "right": "money", "result": "decimal"} +{"name": "/", "left": "decimal", "right": "smallmoney", "result": "decimal"} +{"name": "/", "left": "decimal", "right": "bigint", "result": "decimal"} +{"name": "/", "left": "decimal", "right": "int", "result": "decimal"} +{"name": "/", "left": "decimal", "right": "smallint", "result": "decimal"} +{"name": "/", "left": "decimal", "right": "tinyint", "result": "decimal"} +{"name": "/", "left": "decimal", "right": "bit", "result": "decimal"} +{"name": "/", "left": "money", "right": "float", "result": "float"} +{"name": "/", "left": "money", "right": "real", "result": "real"} +{"name": "/", "left": "money", "right": "decimal", "result": "decimal"} +{"name": "/", "left": "money", "right": "smallmoney", "result": "money"} +{"name": "/", "left": "money", "right": "bigint", "result": "money"} +{"name": "/", "left": "money", "right": "int", "result": "money"} +{"name": "/", "left": "money", "right": "smallint", "result": "money"} +{"name": "/", "left": "money", "right": "tinyint", "result": "money"} +{"name": "/", "left": "money", "right": "bit", "result": "money"} +{"name": "/", "left": "smallmoney", "right": "float", "result": "float"} +{"name": "/", "left": "smallmoney", "right": "real", "result": "real"} +{"name": "/", "left": "smallmoney", "right": "decimal", "result": "decimal"} +{"name": "/", "left": "smallmoney", "right": "money", "result": "money"} +{"name": "/", "left": "smallmoney", "right": "bigint", "result": "smallmoney"} +{"name": "/", "left": "smallmoney", "right": "int", "result": "smallmoney"} +{"name": "/", "left": "smallmoney", "right": "smallint", "result": "smallmoney"} +{"name": "/", "left": "smallmoney", "right": "tinyint", "result": "smallmoney"} +{"name": "/", "left": "smallmoney", "right": "bit", "result": "smallmoney"} +{"name": "/", "left": "bigint", "right": "float", "result": "float"} +{"name": "/", "left": "bigint", "right": "real", "result": "real"} +{"name": "/", "left": "bigint", "right": "decimal", "result": "decimal"} +{"name": "/", "left": "bigint", "right": "money", "result": "money"} +{"name": "/", "left": "bigint", "right": "smallmoney", "result": "smallmoney"} +{"name": "/", "left": "bigint", "right": "int", "result": "bigint"} +{"name": "/", "left": "bigint", "right": "smallint", "result": "bigint"} +{"name": "/", "left": "bigint", "right": "tinyint", "result": "bigint"} +{"name": "/", "left": "bigint", "right": "bit", "result": "bigint"} +{"name": "/", "left": "int", "right": "float", "result": "float"} +{"name": "/", "left": "int", "right": "real", "result": "real"} +{"name": "/", "left": "int", "right": "decimal", "result": "decimal"} +{"name": "/", "left": "int", "right": "money", "result": "money"} +{"name": "/", "left": "int", "right": "smallmoney", "result": "smallmoney"} +{"name": "/", "left": "int", "right": "bigint", "result": "bigint"} +{"name": "/", "left": "int", "right": "smallint", "result": "int"} +{"name": "/", "left": "int", "right": "tinyint", "result": "int"} +{"name": "/", "left": "int", "right": "bit", "result": "int"} +{"name": "/", "left": "smallint", "right": "float", "result": "float"} +{"name": "/", "left": "smallint", "right": "real", "result": "real"} +{"name": "/", "left": "smallint", "right": "decimal", "result": "decimal"} +{"name": "/", "left": "smallint", "right": "money", "result": "money"} +{"name": "/", "left": "smallint", "right": "smallmoney", "result": "smallmoney"} +{"name": "/", "left": "smallint", "right": "bigint", "result": "bigint"} +{"name": "/", "left": "smallint", "right": "int", "result": "int"} +{"name": "/", "left": "smallint", "right": "tinyint", "result": "smallint"} +{"name": "/", "left": "smallint", "right": "bit", "result": "smallint"} +{"name": "/", "left": "tinyint", "right": "float", "result": "float"} +{"name": "/", "left": "tinyint", "right": "real", "result": "real"} +{"name": "/", "left": "tinyint", "right": "decimal", "result": "decimal"} +{"name": "/", "left": "tinyint", "right": "money", "result": "money"} +{"name": "/", "left": "tinyint", "right": "smallmoney", "result": "smallmoney"} +{"name": "/", "left": "tinyint", "right": "bigint", "result": "bigint"} +{"name": "/", "left": "tinyint", "right": "int", "result": "int"} +{"name": "/", "left": "tinyint", "right": "smallint", "result": "smallint"} +{"name": "/", "left": "tinyint", "right": "bit", "result": "tinyint"} +{"name": "/", "left": "bit", "right": "float", "result": "float"} +{"name": "/", "left": "bit", "right": "real", "result": "real"} +{"name": "/", "left": "bit", "right": "decimal", "result": "decimal"} +{"name": "/", "left": "bit", "right": "money", "result": "money"} +{"name": "/", "left": "bit", "right": "smallmoney", "result": "smallmoney"} +{"name": "/", "left": "bit", "right": "bigint", "result": "bigint"} +{"name": "/", "left": "bit", "right": "int", "result": "int"} +{"name": "/", "left": "bit", "right": "smallint", "result": "smallint"} +{"name": "/", "left": "bit", "right": "tinyint", "result": "tinyint"} +{"name": "%", "left": "decimal", "right": "money", "result": "decimal"} +{"name": "%", "left": "decimal", "right": "smallmoney", "result": "decimal"} +{"name": "%", "left": "decimal", "right": "bigint", "result": "decimal"} +{"name": "%", "left": "decimal", "right": "int", "result": "decimal"} +{"name": "%", "left": "decimal", "right": "smallint", "result": "decimal"} +{"name": "%", "left": "decimal", "right": "tinyint", "result": "decimal"} +{"name": "%", "left": "decimal", "right": "bit", "result": "decimal"} +{"name": "%", "left": "money", "right": "decimal", "result": "decimal"} +{"name": "%", "left": "money", "right": "smallmoney", "result": "money"} +{"name": "%", "left": "money", "right": "bigint", "result": "money"} +{"name": "%", "left": "money", "right": "int", "result": "money"} +{"name": "%", "left": "money", "right": "smallint", "result": "money"} +{"name": "%", "left": "money", "right": "tinyint", "result": "money"} +{"name": "%", "left": "money", "right": "bit", "result": "money"} +{"name": "%", "left": "smallmoney", "right": "decimal", "result": "decimal"} +{"name": "%", "left": "smallmoney", "right": "money", "result": "money"} +{"name": "%", "left": "smallmoney", "right": "bigint", "result": "smallmoney"} +{"name": "%", "left": "smallmoney", "right": "int", "result": "smallmoney"} +{"name": "%", "left": "smallmoney", "right": "smallint", "result": "smallmoney"} +{"name": "%", "left": "smallmoney", "right": "tinyint", "result": "smallmoney"} +{"name": "%", "left": "smallmoney", "right": "bit", "result": "smallmoney"} +{"name": "%", "left": "bigint", "right": "decimal", "result": "decimal"} +{"name": "%", "left": "bigint", "right": "money", "result": "money"} +{"name": "%", "left": "bigint", "right": "smallmoney", "result": "smallmoney"} +{"name": "%", "left": "bigint", "right": "int", "result": "bigint"} +{"name": "%", "left": "bigint", "right": "smallint", "result": "bigint"} +{"name": "%", "left": "bigint", "right": "tinyint", "result": "bigint"} +{"name": "%", "left": "bigint", "right": "bit", "result": "bigint"} +{"name": "%", "left": "int", "right": "decimal", "result": "decimal"} +{"name": "%", "left": "int", "right": "money", "result": "money"} +{"name": "%", "left": "int", "right": "smallmoney", "result": "smallmoney"} +{"name": "%", "left": "int", "right": "bigint", "result": "bigint"} +{"name": "%", "left": "int", "right": "smallint", "result": "int"} +{"name": "%", "left": "int", "right": "tinyint", "result": "int"} +{"name": "%", "left": "int", "right": "bit", "result": "int"} +{"name": "%", "left": "smallint", "right": "decimal", "result": "decimal"} +{"name": "%", "left": "smallint", "right": "money", "result": "money"} +{"name": "%", "left": "smallint", "right": "smallmoney", "result": "smallmoney"} +{"name": "%", "left": "smallint", "right": "bigint", "result": "bigint"} +{"name": "%", "left": "smallint", "right": "int", "result": "int"} +{"name": "%", "left": "smallint", "right": "tinyint", "result": "smallint"} +{"name": "%", "left": "smallint", "right": "bit", "result": "smallint"} +{"name": "%", "left": "tinyint", "right": "decimal", "result": "decimal"} +{"name": "%", "left": "tinyint", "right": "money", "result": "money"} +{"name": "%", "left": "tinyint", "right": "smallmoney", "result": "smallmoney"} +{"name": "%", "left": "tinyint", "right": "bigint", "result": "bigint"} +{"name": "%", "left": "tinyint", "right": "int", "result": "int"} +{"name": "%", "left": "tinyint", "right": "smallint", "result": "smallint"} +{"name": "%", "left": "tinyint", "right": "bit", "result": "tinyint"} +{"name": "%", "left": "bit", "right": "decimal", "result": "decimal"} +{"name": "%", "left": "bit", "right": "money", "result": "money"} +{"name": "%", "left": "bit", "right": "smallmoney", "result": "smallmoney"} +{"name": "%", "left": "bit", "right": "bigint", "result": "bigint"} +{"name": "%", "left": "bit", "right": "int", "result": "int"} +{"name": "%", "left": "bit", "right": "smallint", "result": "smallint"} +{"name": "%", "left": "bit", "right": "tinyint", "result": "tinyint"} +{"name": "+", "left": "char", "right": "char", "result": "char"} +{"name": "+", "left": "char", "right": "varchar", "result": "varchar"} +{"name": "+", "left": "char", "right": "nchar", "result": "nchar"} +{"name": "+", "left": "char", "right": "nvarchar", "result": "nvarchar"} +{"name": "+", "left": "varchar", "right": "char", "result": "varchar"} +{"name": "+", "left": "varchar", "right": "varchar", "result": "varchar"} +{"name": "+", "left": "varchar", "right": "nchar", "result": "nvarchar"} +{"name": "+", "left": "varchar", "right": "nvarchar", "result": "nvarchar"} +{"name": "+", "left": "nchar", "right": "char", "result": "nchar"} +{"name": "+", "left": "nchar", "right": "varchar", "result": "nvarchar"} +{"name": "+", "left": "nchar", "right": "nchar", "result": "nchar"} +{"name": "+", "left": "nchar", "right": "nvarchar", "result": "nvarchar"} +{"name": "+", "left": "nvarchar", "right": "char", "result": "nvarchar"} +{"name": "+", "left": "nvarchar", "right": "varchar", "result": "nvarchar"} +{"name": "+", "left": "nvarchar", "right": "nchar", "result": "nvarchar"} +{"name": "+", "left": "nvarchar", "right": "nvarchar", "result": "nvarchar"} diff --git a/internal/engine/mssql/parse_test.go b/internal/engine/mssql/parse_test.go new file mode 100644 index 0000000000..a9ae707c14 --- /dev/null +++ b/internal/engine/mssql/parse_test.go @@ -0,0 +1,29 @@ +package mssql + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// The fmt CLI does not support MSSQL, so exercise its AST printer directly. +func TestLoweredFunctionFormatting(t *testing.T) { + parser := NewParser() + for _, name := range []string{"isnull", "coalesce"} { + t.Run(name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader("SELECT " + name + "(NULL, 1);")) + if err != nil { + t.Fatal(err) + } + // This literal-only expression needs no dialect formatting hooks. + formatted := ast.Format(stmts[0].Raw, nil) + if !strings.Contains(formatted, name+"(") { + t.Fatalf("function identity changed: %s", formatted) + } + if _, err := parser.Parse(strings.NewReader(formatted)); err != nil { + t.Fatalf("formatted SQL cannot be parsed: %v", err) + } + }) + } +} diff --git a/internal/engine/mssql/typepolicy/combine.go b/internal/engine/mssql/typepolicy/combine.go new file mode 100644 index 0000000000..f727707a11 --- /dev/null +++ b/internal/engine/mssql/typepolicy/combine.go @@ -0,0 +1,173 @@ +// Package typepolicy implements SQL Server's type-combination rules without +// depending on its parser or the shared analyzer. +package typepolicy + +import ( + "bytes" + "encoding/json" + "fmt" + "slices" + + "github.com/sqlc-dev/sqlc/internal/core" +) + +type rules struct { + Precedence []string `json:"precedence"` + Varying map[string]string `json:"varying,omitempty"` + Decimal string `json:"decimal,omitempty"` + DecimalMaxPrecision int64 `json:"decimal_max_precision,omitempty"` + AsDecimal map[string][2]int64 `json:"as_decimal,omitempty"` +} + +type combiner struct { + cat *core.Catalog + rules rules +} + +// New binds SQL Server's policy to a freshly seeded or restored catalog. +func New(cat *core.Catalog) (core.TypeCombiner, error) { + data, err := cat.TypeCombinationData() + if err != nil { + return nil, err + } + rules, err := decodeRules(data) + if err != nil { + return nil, fmt.Errorf("mssql: type-combination rules: %w", err) + } + return &combiner{cat: cat, rules: rules}, nil +} + +func decodeRules(data []byte) (rules, error) { + var out rules + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(&out); err != nil { + return rules{}, err + } + if len(out.Precedence) == 0 { + return rules{}, fmt.Errorf("precedence must not be empty") + } + return out, nil +} + +func (p *combiner) Combine(req core.TypeCombinationRequest) (core.TypeCombinationResult, error) { + if req.Context == core.CombineCoalesce && len(req.FunctionName) > 0 { + if len(req.FunctionName) != 1 || req.FunctionName[0] != "isnull" { + return core.TypeCombinationResult{}, nil + } + // ISNULL retains the first typed argument, including when a literal + // NULL or an unconstrained placeholder comes before it. + for _, candidate := range req.Candidates { + if candidate != nil { + return core.TypeCombinationResult{Handled: true, Type: candidate.WithNullable(false)}, nil + } + } + return core.TypeCombinationResult{Handled: true}, nil + } + switch req.Context { + case core.CombineUnion, core.CombineIntersect, core.CombineExcept, + core.CombineCase, core.CombineCoalesce, core.CombineGreatest, core.CombineLeast: + default: + return core.TypeCombinationResult{}, nil + } + var out *core.TypeExpr + for _, candidate := range req.Candidates { + if candidate == nil { + continue + } + if out == nil { + out = candidate + } else { + out = p.combine(out, candidate) + } + } + return core.TypeCombinationResult{Handled: true, Type: out.WithNullable(false)}, nil +} + +// combine applies SQL Server's precedence and set-operation type modifiers. +// An unlisted family retains the left type, as the existing inference did. +func (p *combiner) combine(left, right *core.TypeExpr) *core.TypeExpr { + rules := &p.rules + li, ri := slices.Index(rules.Precedence, left.Name), slices.Index(rules.Precedence, right.Name) + if li < 0 || ri < 0 { + return left + } + winner, loser := left, right + if ri < li { + winner, loser = right, left + } + out := winner.WithNullable(false) + if varying, ok := rules.Varying[out.Name]; ok { + for _, name := range rules.Varying { + if loser.Name == name { + out.Name = varying + break + } + } + } + switch { + case out.Name == rules.Decimal && rules.Decimal != "": + combineDecimal(rules, out, loser) + case len(out.Args) == 1 && len(loser.Args) == 1 && p.sameCategory(left.Name, right.Name): + out.Args[0] = widerArg(out.Args[0], loser.Args[0]) + } + return out +} + +func (p *combiner) sameCategory(x, y string) bool { + category := func(name string) string { + oid, err := p.cat.TypeOID(name) + if err != nil { + return "" + } + chain := p.cat.ResolutionChain(oid) + t, err := p.cat.LookupType(chain[len(chain)-1]) + if err != nil { + return "" + } + return t.Category + } + cx := category(x) + return cx != "" && cx == category(y) +} + +// widerArg is the larger of two lengths or precisions, MAX above any. +func widerArg(a, b core.TypeArg) core.TypeArg { + switch { + case a.Ident != nil: + return a + case b.Ident != nil: + return b + case a.Int != nil && b.Int != nil && *b.Int > *a.Int: + return b + } + return a +} + +// combineDecimal applies SQL Server's UNION/INTERSECT/EXCEPT formula: +// retain the larger scale and integer-digit count, reducing scale at the +// precision limit. This is not a cross-engine decimal-coercion rule. +func combineDecimal(rules *rules, out, other *core.TypeExpr) { + p1, s1, ok1 := decimalForm(rules, out) + p2, s2, ok2 := decimalForm(rules, other) + if !ok1 || !ok2 { + return + } + scale := max(s1, s2) + precision := max(p1-s1, p2-s2) + scale + if limit := rules.DecimalMaxPrecision; limit > 0 && precision > limit { + scale = max(scale-(precision-limit), 0) + precision = limit + } + out.Args = []core.TypeArg{{Int: &precision}, {Int: &scale}} +} + +func decimalForm(rules *rules, t *core.TypeExpr) (int64, int64, bool) { + if t.Name == rules.Decimal && len(t.Args) == 2 && t.Args[0].Int != nil && t.Args[1].Int != nil { + return *t.Args[0].Int, *t.Args[1].Int, true + } + if form, ok := rules.AsDecimal[t.Name]; ok { + return form[0], form[1], true + } + return 0, 0, false +} diff --git a/internal/engine/mssql/typepolicy/combine_test.go b/internal/engine/mssql/typepolicy/combine_test.go new file mode 100644 index 0000000000..9820315b9f --- /dev/null +++ b/internal/engine/mssql/typepolicy/combine_test.go @@ -0,0 +1,34 @@ +package typepolicy + +import ( + "strings" + "testing" +) + +// Embedded dialect metadata cannot be replaced through the CLI. +func TestDecodeRules(t *testing.T) { + tests := []struct { + name string + data string + want string + }{ + {"missing", "", "EOF"}, + {"empty", "{}", "precedence must not be empty"}, + {"null", "null", "precedence must not be empty"}, + {"unknown field", `{"strategy":"mssql","precedence":["int"]}`, `unknown field "strategy"`}, + {"invalid shape", `{"precedence":"int"}`, "cannot unmarshal"}, + {"valid", `{"precedence":["int"]}`, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := decodeRules([]byte(tt.data)) + if tt.want == "" { + if err != nil { + t.Fatal(err) + } + } else if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("got %v, want error containing %q", err, tt.want) + } + }) + } +} diff --git a/internal/sql/ast/coalesce_expr.go b/internal/sql/ast/coalesce_expr.go index f51c2c5c8e..18224834a4 100644 --- a/internal/sql/ast/coalesce_expr.go +++ b/internal/sql/ast/coalesce_expr.go @@ -10,6 +10,9 @@ type CoalesceExpr struct { Coalescecollid Oid `json:"coalescecollid"` Args *List `json:"args,omitempty"` Location int `json:"location"` + // Func preserves the source identity of a function lowered to these + // semantics. Its dialect decides any function-specific type rules. + Func *FuncName `json:"func,omitempty"` } func (n *CoalesceExpr) Pos() int { @@ -22,7 +25,12 @@ func (n *CoalesceExpr) Format(buf *TrackedBuffer, d format.Dialect) { } // Lower case, like every other function name: the printer upper-cases // keywords, and function names are identifiers, which fold lower. - buf.WriteString("coalesce(") + if n.Func != nil { + buf.astFormat(n.Func, d) + } else { + buf.WriteString("coalesce") + } + buf.WriteString("(") buf.astFormat(n.Args, d) buf.WriteString(")") } diff --git a/internal/sql/astutils/rewrite.go b/internal/sql/astutils/rewrite.go index 25b274a16c..73b2641fe6 100644 --- a/internal/sql/astutils/rewrite.go +++ b/internal/sql/astutils/rewrite.go @@ -449,6 +449,7 @@ func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast. a.apply(n, "Relation", nil, n.Relation) case *ast.CoalesceExpr: + a.apply(n, "Func", nil, n.Func) a.apply(n, "Xpr", nil, n.Xpr) a.apply(n, "Args", nil, n.Args) diff --git a/internal/sql/astutils/walk.go b/internal/sql/astutils/walk.go index 6d5e80bdc3..c2535f8397 100644 --- a/internal/sql/astutils/walk.go +++ b/internal/sql/astutils/walk.go @@ -571,6 +571,9 @@ func Walk(f Visitor, node ast.Node) { } case *ast.CoalesceExpr: + if n.Func != nil { + Walk(f, n.Func) + } if n.Xpr != nil { Walk(f, n.Xpr) }