diff --git a/docs/howto/analyze.md b/docs/howto/analyze.md index f7ba400042..ea1063c310 100644 --- a/docs/howto/analyze.md +++ b/docs/howto/analyze.md @@ -26,7 +26,7 @@ provided. The schema is always read from the `--schema` file. ## Flags - `--dialect`, `-d` - The SQL dialect to use. One of `postgresql`, `mysql`, - `sqlite`, `clickhouse`, or `googlesql`. Required. + `sqlite`, `clickhouse`, `googlesql`, or `mssql`. Required. - `--schema`, `-s` - Path to the schema (DDL) file. Required. - `--ast` - Include each statement's AST in the output. Defaults to `false`. diff --git a/docs/howto/parse.md b/docs/howto/parse.md index 28ba9a3d9a..cf34e417be 100644 --- a/docs/howto/parse.md +++ b/docs/howto/parse.md @@ -20,7 +20,7 @@ provided. ## Flags - `--dialect`, `-d` - The SQL dialect to use. One of `postgresql`, `mysql`, - `sqlite`, `clickhouse`, or `googlesql`. Required. + `sqlite`, `clickhouse`, `googlesql`, or `mssql`. Required. ## Examples diff --git a/go.mod b/go.mod index c9db1b3408..acd10af37c 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/sqlc-dev/marino v0.1.0 github.com/sqlc-dev/meyer v0.1.1 github.com/sqlc-dev/oliphant v0.1.0 + github.com/sqlc-dev/teesql v1.1.0 github.com/sqlc-dev/zetajones v0.1.0 github.com/tetratelabs/wazero v1.12.0 github.com/xeipuuv/gojsonschema v1.2.0 diff --git a/go.sum b/go.sum index e22b4e345b..99d3140970 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/sqlc-dev/meyer v0.1.1 h1:BAeZcfgLyTnk9f90DyGEKXPrHxtgvVD/DTM6awq2kUY= github.com/sqlc-dev/meyer v0.1.1/go.mod h1:pS4USCRf/SLjWtaMcnTo4YrEEFKBj8CyyqlxcVUJQH8= github.com/sqlc-dev/oliphant v0.1.0 h1:RAsO6BMitIzB2+swx/qzUR5nf6w4cQ1abgHIu+Fgppo= github.com/sqlc-dev/oliphant v0.1.0/go.mod h1:fRM/t4FutRddTIq2YCuS4O9o+2rRwSwELRvLMqtPloo= +github.com/sqlc-dev/teesql v1.1.0 h1:3sVYQ9FGxQVcqrqQOQ27bk0aF4c4yN1H1zLL79uaSxQ= +github.com/sqlc-dev/teesql v1.1.0/go.mod h1:WwOp9UtnxG17+eNFT5KXu/AGBQ8ucdGc8wIOpnj4XCI= github.com/sqlc-dev/zetajones v0.1.0 h1:VeG0atx6lNABr9V2bSI5vL9DvOKTHX0XjMqWUE/rv40= github.com/sqlc-dev/zetajones v0.1.0/go.mod h1:dU1DxwqC6Cahbpnw16KpH1J2waWRDMdwyDSvovMZR4I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/cmd/analyze.go b/internal/cmd/analyze.go index a05129d57b..347a5949c7 100644 --- a/internal/cmd/analyze.go +++ b/internal/cmd/analyze.go @@ -43,6 +43,9 @@ Examples: # Analyze a GoogleSQL (BigQuery, Spanner) query sqlc analyze --dialect googlesql --schema schema.sql query.sql + # Analyze a SQL Server (T-SQL) query + sqlc analyze --dialect mssql --schema schema.sql query.sql + # Analyze a query piped via stdin echo "-- name: GetAuthor :one SELECT * FROM authors WHERE id = $1;" | sqlc analyze --dialect postgresql --schema schema.sql @@ -56,7 +59,7 @@ Examples: return err } if dialect == "" { - return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, or googlesql)") + return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, googlesql, or mssql)") } schemaPath, err := cmd.Flags().GetString("schema") @@ -117,8 +120,10 @@ Examples: engine = config.EngineClickHouse case "googlesql": engine = config.EngineGoogleSQL + case "mssql", "sqlserver": + engine = config.EngineMSSQL default: - return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, or googlesql)", dialect) + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, googlesql, or mssql)", dialect) } sql := config.SQL{ @@ -160,7 +165,7 @@ Examples: return nil }, } - cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, or googlesql)") + cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, googlesql, or mssql)") cmd.Flags().StringP("schema", "s", "", "path to the schema file") cmd.Flags().BoolP("ast", "", false, "include the statement AST in the output") return cmd diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go index 2fc4c763e8..1f92b595a9 100644 --- a/internal/cmd/parse.go +++ b/internal/cmd/parse.go @@ -12,6 +12,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" "github.com/sqlc-dev/sqlc/internal/engine/dolphin" "github.com/sqlc-dev/sqlc/internal/engine/googlesql" + "github.com/sqlc-dev/sqlc/internal/engine/mssql" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" "github.com/sqlc-dev/sqlc/internal/engine/sqlite" "github.com/sqlc-dev/sqlc/internal/metadata" @@ -59,7 +60,10 @@ Examples: sqlc parse --dialect clickhouse queries.sql # Parse GoogleSQL (BigQuery, Spanner) - sqlc parse --dialect googlesql queries.sql`, + sqlc parse --dialect googlesql queries.sql + + # Parse SQL Server (T-SQL) SQL + sqlc parse --dialect mssql queries.sql`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { dialect, err := cmd.Flags().GetString("dialect") @@ -67,7 +71,7 @@ Examples: return err } if dialect == "" { - return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, or googlesql)") + return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, googlesql, or mssql)") } // Determine input source @@ -104,8 +108,10 @@ Examples: parser = clickhouse.NewParser() case "googlesql": parser = googlesql.NewParser() + case "mssql", "sqlserver": + parser = mssql.NewParser() default: - return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, or googlesql)", dialect) + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, googlesql, or mssql)", dialect) } // Read the full source so each statement's name and command can be @@ -149,6 +155,6 @@ Examples: return nil }, } - cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, or googlesql)") + cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, googlesql, or mssql)") return cmd } diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index b830faaa19..06e6bc40db 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -11,6 +11,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" "github.com/sqlc-dev/sqlc/internal/engine/dolphin" "github.com/sqlc-dev/sqlc/internal/engine/googlesql" + "github.com/sqlc-dev/sqlc/internal/engine/mssql" "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" @@ -57,9 +58,10 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts o(c) } - // ClickHouse and GoogleSQL have no legacy analysis path to fall back to. + // ClickHouse, GoogleSQL and SQL Server have no legacy analysis path to + // fall back to. switch conf.Engine { - case config.EngineClickHouse, config.EngineGoogleSQL: + case config.EngineClickHouse, config.EngineGoogleSQL, config.EngineMSSQL: c.coreAnalysis = true } if c.coreAnalysis { @@ -138,6 +140,10 @@ func (c *Compiler) initCore() error { c.parser = googlesql.NewParser() c.selector = newDefaultSelector() dialect = googlesql.Dialect() + case config.EngineMSSQL: + c.parser = mssql.NewParser() + c.selector = newDefaultSelector() + dialect = mssql.Dialect() default: return fmt.Errorf("unknown engine: %s", c.conf.Engine) } diff --git a/internal/config/config.go b/internal/config/config.go index 4bbf323476..b96053ebb9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -56,6 +56,7 @@ const ( EngineSQLite Engine = "sqlite" EngineClickHouse Engine = "clickhouse" EngineGoogleSQL Engine = "googlesql" + EngineMSSQL Engine = "mssql" ) type Config struct { diff --git a/internal/endtoend/testdata/analyze_basic/mssql/exec.json b/internal/endtoend/testdata/analyze_basic/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/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_basic/mssql/query.sql b/internal/endtoend/testdata/analyze_basic/mssql/query.sql new file mode 100644 index 0000000000..b694368ed1 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/mssql/query.sql @@ -0,0 +1,2 @@ +-- name: ListAuthors :many +SELECT id, name, bio, royalties, created FROM authors; diff --git a/internal/endtoend/testdata/analyze_basic/mssql/schema.sql b/internal/endtoend/testdata/analyze_basic/mssql/schema.sql new file mode 100644 index 0000000000..3629635d2e --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/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_basic/mssql/stdout.txt b/internal/endtoend/testdata/analyze_basic/mssql/stdout.txt new file mode 100644 index 0000000000..7226354b09 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/mssql/stdout.txt @@ -0,0 +1,44 @@ +[ + { + "name": "ListAuthors", + "cmd": ":many", + "columns": [ + { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "bio", + "data_type": "nvarchar", + "not_null": false, + "is_array": false, + "table": "authors" + }, + { + "name": "royalties", + "data_type": "decimal", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "created", + "data_type": "datetime2", + "not_null": true, + "is_array": false, + "table": "authors" + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_dml/mssql/exec.json b/internal/endtoend/testdata/analyze_dml/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/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_dml/mssql/query.sql b/internal/endtoend/testdata/analyze_dml/mssql/query.sql new file mode 100644 index 0000000000..cdf7d9fd77 --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/mssql/query.sql @@ -0,0 +1,11 @@ +-- name: CreateAuthor :one +INSERT INTO authors (name, bio) OUTPUT INSERTED.id VALUES (@name, @bio); + +-- name: UpdateAuthorAlias :exec +UPDATE a SET name = @name FROM authors a WHERE a.id = @id; + +-- name: UpdateBookPrices :exec +UPDATE b SET price = @price FROM books b JOIN authors a ON b.author_id = a.id WHERE a.name = @author; + +-- name: DeleteBooksByAuthor :exec +DELETE b FROM books b INNER JOIN authors a ON b.author_id = a.id WHERE a.name = @name; diff --git a/internal/endtoend/testdata/analyze_dml/mssql/schema.sql b/internal/endtoend/testdata/analyze_dml/mssql/schema.sql new file mode 100644 index 0000000000..5a53ca742a --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/mssql/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE authors ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(100) NOT NULL, + bio NVARCHAR(MAX) +); + +CREATE TABLE books ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + author_id BIGINT NOT NULL, + title NVARCHAR(200) NOT NULL, + price DECIMAL(10,2) +); diff --git a/internal/endtoend/testdata/analyze_dml/mssql/stdout.txt b/internal/endtoend/testdata/analyze_dml/mssql/stdout.txt new file mode 100644 index 0000000000..d2ed5f27e0 --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/mssql/stdout.txt @@ -0,0 +1,108 @@ +[ + { + "name": "CreateAuthor", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "authors" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + } + }, + { + "number": 2, + "column": { + "name": "bio", + "data_type": "nvarchar", + "not_null": false, + "is_array": false, + "table": "authors" + } + } + ] + }, + { + "name": "UpdateAuthorAlias", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + } + }, + { + "number": 2, + "column": { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "authors" + } + } + ] + }, + { + "name": "UpdateBookPrices", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "price", + "data_type": "decimal", + "not_null": false, + "is_array": false, + "table": "books" + } + }, + { + "number": 2, + "column": { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + } + } + ] + }, + { + "name": "DeleteBooksByAuthor", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_params/mssql/exec.json b/internal/endtoend/testdata/analyze_params/mssql/exec.json new file mode 100644 index 0000000000..253b05abbf --- /dev/null +++ b/internal/endtoend/testdata/analyze_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_params/mssql/query.sql b/internal/endtoend/testdata/analyze_params/mssql/query.sql new file mode 100644 index 0000000000..c410ec569a --- /dev/null +++ b/internal/endtoend/testdata/analyze_params/mssql/query.sql @@ -0,0 +1,5 @@ +-- name: GetAuthor :one +SELECT id, name, bio FROM authors WHERE id = @id; + +-- name: FilterAuthors :many +SELECT id, name FROM authors WHERE name = @name AND royalties > @royalties; diff --git a/internal/endtoend/testdata/analyze_params/mssql/schema.sql b/internal/endtoend/testdata/analyze_params/mssql/schema.sql new file mode 100644 index 0000000000..3629635d2e --- /dev/null +++ b/internal/endtoend/testdata/analyze_params/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_params/mssql/stdout.txt b/internal/endtoend/testdata/analyze_params/mssql/stdout.txt new file mode 100644 index 0000000000..c5765c94c3 --- /dev/null +++ b/internal/endtoend/testdata/analyze_params/mssql/stdout.txt @@ -0,0 +1,83 @@ +[ + { + "name": "GetAuthor", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "bio", + "data_type": "nvarchar", + "not_null": false, + "is_array": false, + "table": "authors" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "authors" + } + } + ] + }, + { + "name": "FilterAuthors", + "cmd": ":many", + "columns": [ + { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "data_type": "nvarchar", + "not_null": true, + "is_array": false, + "table": "authors" + } + }, + { + "number": 2, + "column": { + "name": "royalties", + "data_type": "decimal", + "not_null": true, + "is_array": false, + "table": "authors" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/parse_basic/mssql/exec.json b/internal/endtoend/testdata/parse_basic/mssql/exec.json new file mode 100644 index 0000000000..fb497540ad --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/mssql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "parse", + "args": ["--dialect", "mssql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/parse_basic/mssql/query.sql b/internal/endtoend/testdata/parse_basic/mssql/query.sql new file mode 100644 index 0000000000..11dff59f08 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/mssql/query.sql @@ -0,0 +1,2 @@ +-- name: GetValue :one +SELECT 1; diff --git a/internal/endtoend/testdata/parse_basic/mssql/stdout.txt b/internal/endtoend/testdata/parse_basic/mssql/stdout.txt new file mode 100644 index 0000000000..b20fbdcee5 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/mssql/stdout.txt @@ -0,0 +1,44 @@ +[ + { + "name": "GetValue", + "cmd": ":one", + "ast": { + "Stmt": { + "DistinctClause": null, + "IntoClause": null, + "TargetList": { + "Items": [ + { + "Name": null, + "Indirection": null, + "Val": { + "Val": { + "Ival": 1 + }, + "Location": 30 + }, + "Location": 30 + } + ] + }, + "FromClause": null, + "WhereClause": null, + "GroupClause": null, + "HavingClause": null, + "WindowClause": null, + "ValuesLists": null, + "SortClause": null, + "LimitOffset": null, + "LimitCount": null, + "LockingClause": null, + "WithClause": null, + "Op": 0, + "All": false, + "Larg": null, + "Rarg": null + }, + "StmtLocation": 0, + "StmtLen": 32 + } + } +] diff --git a/internal/engine/mssql/convert.go b/internal/engine/mssql/convert.go new file mode 100644 index 0000000000..6401b7bb88 --- /dev/null +++ b/internal/engine/mssql/convert.go @@ -0,0 +1,1143 @@ +package mssql + +import ( + "strconv" + "strings" + + tsql "github.com/sqlc-dev/teesql/ast" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +type cc struct { + paramCount int + // namedParams tracks the number assigned to each "@name" so repeated + // uses share a single parameter. + namedParams map[string]int + // toByte maps a teesql UTF-16 code-unit offset to a byte offset in the + // source, so Location fields agree with StmtLocation/StmtLen. + toByte func(int) int +} + +// loc returns a node's start offset in bytes, for the Location fields of the +// sqlc AST. +func (c *cc) loc(n any) int { + f, ok := n.(fragmented) + if !ok { + return 0 + } + off := f.Frag().StartOffset + if c.toByte != nil { + return c.toByte(off) + } + return off +} + +func (c *cc) parseRangeVar(n *tsql.SchemaObjectName) *ast.RangeVar { + name := parseTableName(n) + rv := &ast.RangeVar{ + Relname: &name.Name, + Location: c.loc(n), + } + if name.Schema != "" { + rv.Schemaname = &name.Schema + } + return rv +} + +func (c *cc) convert(node tsql.Node) ast.Node { + switch n := node.(type) { + case *tsql.SelectStatement: + return c.convertSelectStatement(n) + case *tsql.InsertStatement: + return c.convertInsertStatement(n) + case *tsql.UpdateStatement: + return c.convertUpdateStatement(n) + case *tsql.DeleteStatement: + return c.convertDeleteStatement(n) + case *tsql.CreateTableStatement: + return c.convertCreateTableStatement(n) + case *tsql.DropTableStatement: + return c.convertDropTableStatement(n) + case *tsql.AlterTableAddTableElementStatement: + return c.convertAlterTableAddTableElementStatement(n) + case *tsql.AlterTableDropTableElementStatement: + return c.convertAlterTableDropTableElementStatement(n) + default: + return todo(n) + } +} + +func (c *cc) convertSelectStatement(n *tsql.SelectStatement) ast.Node { + stmt := c.convertQueryExpression(n.QueryExpression) + sel, ok := stmt.(*ast.SelectStmt) + if !ok { + return stmt + } + if with := c.convertWithClause(n.WithCtesAndXmlNamespaces); with != nil { + sel.WithClause = with + } + return sel +} + +func (c *cc) convertWithClause(n *tsql.WithCtesAndXmlNamespaces) *ast.WithClause { + if n == nil || len(n.CommonTableExpressions) == 0 { + return nil + } + with := &ast.WithClause{ + Ctes: &ast.List{}, + Location: c.loc(n), + } + for _, cte := range n.CommonTableExpressions { + name := identifierValue(cte.ExpressionName) + item := &ast.CommonTableExpr{ + Ctename: &name, + Ctequery: c.convertQueryExpression(cte.QueryExpression), + Location: c.loc(cte), + } + if len(cte.Columns) > 0 { + item.Aliascolnames = &ast.List{} + for _, col := range cte.Columns { + item.Aliascolnames.Items = append(item.Aliascolnames.Items, NewIdentifier(col.Value)) + } + } + with.Ctes.Items = append(with.Ctes.Items, item) + } + return with +} + +func (c *cc) convertQueryExpression(q tsql.QueryExpression) ast.Node { + switch n := q.(type) { + case *tsql.QuerySpecification: + return c.convertQuerySpecification(n) + case *tsql.BinaryQueryExpression: + return c.convertBinaryQueryExpression(n) + case *tsql.QueryParenthesisExpression: + return c.convertQueryExpression(n.QueryExpression) + default: + return todo(q) + } +} + +func (c *cc) convertBinaryQueryExpression(n *tsql.BinaryQueryExpression) ast.Node { + var op ast.SetOperation + switch n.BinaryQueryExpressionType { + case "Union": + op = ast.Union + case "Except": + op = ast.Except + case "Intersect": + op = ast.Intersect + default: + return todo(n) + } + larg, ok := c.convertQueryExpression(n.FirstQueryExpression).(*ast.SelectStmt) + if !ok { + return todo(n) + } + rarg, ok := c.convertQueryExpression(n.SecondQueryExpression).(*ast.SelectStmt) + if !ok { + return todo(n) + } + stmt := &ast.SelectStmt{ + Op: op, + All: n.All, + Larg: larg, + Rarg: rarg, + } + if n.OrderByClause != nil { + stmt.SortClause = c.convertOrderByClause(n.OrderByClause) + } + return stmt +} + +func (c *cc) convertQuerySpecification(n *tsql.QuerySpecification) *ast.SelectStmt { + stmt := &ast.SelectStmt{} + + if len(n.SelectElements) > 0 { + stmt.TargetList = &ast.List{} + for _, elem := range n.SelectElements { + if target := c.convertSelectElement(elem); target != nil { + stmt.TargetList.Items = append(stmt.TargetList.Items, target) + } + } + } + + if n.FromClause != nil { + stmt.FromClause = &ast.List{} + for _, ref := range n.FromClause.TableReferences { + stmt.FromClause.Items = append(stmt.FromClause.Items, c.convertTableReference(ref)) + } + } + + if n.WhereClause != nil { + stmt.WhereClause = c.convertBooleanExpression(n.WhereClause.SearchCondition) + } + + if n.GroupByClause != nil { + stmt.GroupClause = &ast.List{} + for _, spec := range n.GroupByClause.GroupingSpecifications { + if g, ok := spec.(*tsql.ExpressionGroupingSpecification); ok { + stmt.GroupClause.Items = append(stmt.GroupClause.Items, c.convertScalarExpression(g.Expression)) + } + } + } + + if n.HavingClause != nil { + stmt.HavingClause = c.convertBooleanExpression(n.HavingClause.SearchCondition) + } + + if n.OrderByClause != nil { + stmt.SortClause = c.convertOrderByClause(n.OrderByClause) + } + + // OFFSET n ROWS FETCH NEXT m ROWS ONLY + if n.OffsetClause != nil { + if n.OffsetClause.OffsetExpression != nil { + stmt.LimitOffset = c.convertScalarExpression(n.OffsetClause.OffsetExpression) + } + if n.OffsetClause.FetchExpression != nil { + stmt.LimitCount = c.convertScalarExpression(n.OffsetClause.FetchExpression) + } + } + + // TOP n; TOP n PERCENT does not translate to a row count. + if n.TopRowFilter != nil && !n.TopRowFilter.Percent { + stmt.LimitCount = c.convertScalarExpression(n.TopRowFilter.Expression) + } + + if n.UniqueRowFilter == "Distinct" { + stmt.DistinctClause = &ast.List{} + } + + return stmt +} + +func (c *cc) convertSelectElement(elem tsql.SelectElement) ast.Node { + switch e := elem.(type) { + case *tsql.SelectScalarExpression: + res := &ast.ResTarget{ + Val: c.convertScalarExpression(e.Expression), + Location: c.loc(e), + } + if name := columnAlias(e.ColumnName); name != "" { + res.Name = &name + } + return res + case *tsql.SelectStarExpression: + fields := &ast.List{} + if e.Qualifier != nil { + for _, id := range e.Qualifier.Identifiers { + fields.Items = append(fields.Items, NewIdentifier(id.Value)) + } + } + fields.Items = append(fields.Items, &ast.A_Star{}) + return &ast.ResTarget{ + Val: &ast.ColumnRef{ + Fields: fields, + Location: c.loc(e), + }, + Location: c.loc(e), + } + default: + return &ast.ResTarget{ + Val: todo(elem), + Location: c.loc(elem), + } + } +} + +func columnAlias(n *tsql.IdentifierOrValueExpression) string { + if n == nil { + return "" + } + if n.Identifier != nil { + return identifierValue(n.Identifier) + } + if n.Value != "" { + return identifier(n.Value) + } + return "" +} + +func (c *cc) convertOrderByClause(n *tsql.OrderByClause) *ast.List { + list := &ast.List{} + for _, elem := range n.OrderByElements { + sortBy := &ast.SortBy{ + Node: c.convertScalarExpression(elem.Expression), + Location: c.loc(elem), + } + switch elem.SortOrder { + case "Descending": + sortBy.SortbyDir = ast.SortByDirDesc + case "Ascending": + sortBy.SortbyDir = ast.SortByDirAsc + default: + sortBy.SortbyDir = ast.SortByDirDefault + } + list.Items = append(list.Items, sortBy) + } + return list +} + +func (c *cc) convertTableReference(ref tsql.TableReference) ast.Node { + switch t := ref.(type) { + case *tsql.NamedTableReference: + rv := c.parseRangeVar(t.SchemaObject) + if t.Alias != nil { + alias := identifierValue(t.Alias) + rv.Alias = &ast.Alias{Aliasname: &alias} + } + return rv + case *tsql.QueryDerivedTable: + sub := &ast.RangeSubselect{ + Subquery: c.convertQueryExpression(t.QueryExpression), + } + if t.Alias != nil { + alias := identifierValue(t.Alias) + sub.Alias = &ast.Alias{Aliasname: &alias} + } + return sub + case *tsql.QualifiedJoin: + join := &ast.JoinExpr{ + Larg: c.convertTableReference(t.FirstTableReference), + Rarg: c.convertTableReference(t.SecondTableReference), + } + switch t.QualifiedJoinType { + case "LeftOuter": + join.Jointype = ast.JoinTypeLeft + case "RightOuter": + join.Jointype = ast.JoinTypeRight + case "FullOuter": + join.Jointype = ast.JoinTypeFull + default: + join.Jointype = ast.JoinTypeInner + } + if t.SearchCondition != nil { + join.Quals = c.convertBooleanExpression(t.SearchCondition) + } + return join + case *tsql.UnqualifiedJoin: + if t.UnqualifiedJoinType != "CrossJoin" { + return todo(ref) + } + return &ast.JoinExpr{ + Jointype: ast.JoinTypeInner, + Larg: c.convertTableReference(t.FirstTableReference), + Rarg: c.convertTableReference(t.SecondTableReference), + } + default: + return todo(ref) + } +} + +func (c *cc) convertBooleanExpression(expr tsql.BooleanExpression) ast.Node { + if expr == nil { + return nil + } + switch e := expr.(type) { + case *tsql.BooleanComparisonExpression: + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: comparisonOperator(e.ComparisonType)}}, + }, + Lexpr: c.convertScalarExpression(e.FirstExpression), + Rexpr: c.convertScalarExpression(e.SecondExpression), + Location: c.loc(e), + } + case *tsql.BooleanBinaryExpression: + boolop := ast.BoolExprTypeAnd + if e.BinaryExpressionType == "Or" { + boolop = ast.BoolExprTypeOr + } + return &ast.BoolExpr{ + Boolop: boolop, + Args: &ast.List{ + Items: []ast.Node{ + c.convertBooleanExpression(e.FirstExpression), + c.convertBooleanExpression(e.SecondExpression), + }, + }, + Location: c.loc(e), + } + case *tsql.BooleanNotExpression: + return &ast.BoolExpr{ + Boolop: ast.BoolExprTypeNot, + Args: &ast.List{ + Items: []ast.Node{c.convertBooleanExpression(e.Expression)}, + }, + Location: c.loc(e), + } + case *tsql.BooleanParenthesisExpression: + return c.convertBooleanExpression(e.Expression) + case *tsql.BooleanIsNullExpression: + test := ast.NullTestTypeIsNull + if e.IsNot { + test = ast.NullTestTypeIsNotNull + } + return &ast.NullTest{ + Arg: c.convertScalarExpression(e.Expression), + Nulltesttype: test, + Location: c.loc(e), + } + case *tsql.BooleanTernaryExpression: + return &ast.BetweenExpr{ + Expr: c.convertScalarExpression(e.FirstExpression), + Left: c.convertScalarExpression(e.SecondExpression), + Right: c.convertScalarExpression(e.ThirdExpression), + Not: e.TernaryExpressionType == "NotBetween", + Location: c.loc(e), + } + case *tsql.BooleanInExpression: + in := &ast.In{ + Expr: c.convertScalarExpression(e.Expression), + Not: e.NotDefined, + Location: c.loc(e), + } + for _, item := range e.Values { + in.List = append(in.List, c.convertScalarExpression(item)) + } + if e.Subquery != nil { + in.Sel = c.convertQueryExpression(e.Subquery) + } + return in + case *tsql.BooleanLikeExpression: + op := "~~" + if e.NotDefined { + op = "!~~" + } + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: op}}, + }, + Lexpr: c.convertScalarExpression(e.FirstExpression), + Rexpr: c.convertScalarExpression(e.SecondExpression), + Location: c.loc(e), + } + case *tsql.ExistsPredicate: + return &ast.SubLink{ + SubLinkType: ast.EXISTS_SUBLINK, + Subselect: c.convertQueryExpression(e.Subquery), + Location: c.loc(e), + } + case *tsql.BooleanScalarPlaceholder: + return c.convertScalarExpression(e.Scalar) + default: + return todo(expr) + } +} + +func comparisonOperator(comparisonType string) string { + switch comparisonType { + case "Equals": + return "=" + case "NotEqualToBrackets", "NotEqualToExclamation": + return "<>" + case "GreaterThan": + return ">" + case "LessThan": + return "<" + case "GreaterThanOrEqualTo", "NotLessThan": + return ">=" + case "LessThanOrEqualTo", "NotGreaterThan": + return "<=" + default: + return "=" + } +} + +func (c *cc) convertScalarExpression(expr tsql.ScalarExpression) ast.Node { + if expr == nil { + return nil + } + switch e := expr.(type) { + case *tsql.ColumnReferenceExpression: + return c.convertColumnReference(e) + case *tsql.VariableReference: + return c.convertVariableReference(e) + case *tsql.IntegerLiteral: + ival, _ := strconv.ParseInt(e.Value, 10, 64) + return &ast.A_Const{ + Val: &ast.Integer{Ival: ival}, + Location: c.loc(e), + } + case *tsql.NumericLiteral: + return &ast.A_Const{ + Val: &ast.Float{Str: e.Value}, + Location: c.loc(e), + } + case *tsql.RealLiteral: + return &ast.A_Const{ + Val: &ast.Float{Str: e.Value}, + Location: c.loc(e), + } + case *tsql.MoneyLiteral: + return &ast.A_Const{ + Val: &ast.Float{Str: e.Value}, + Location: c.loc(e), + } + case *tsql.StringLiteral: + return &ast.A_Const{ + Val: &ast.String{Str: e.Value}, + Location: c.loc(e), + } + case *tsql.NullLiteral: + return &ast.A_Const{ + Val: &ast.Null{}, + Location: c.loc(e), + } + case *tsql.BinaryExpression: + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: binaryOperator(e.BinaryExpressionType)}}, + }, + Lexpr: c.convertScalarExpression(e.FirstExpression), + Rexpr: c.convertScalarExpression(e.SecondExpression), + Location: c.loc(e), + } + case *tsql.UnaryExpression: + switch e.UnaryExpressionType { + case "Positive": + return c.convertScalarExpression(e.Expression) + case "BitwiseNot": + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: "~"}}, + }, + Rexpr: c.convertScalarExpression(e.Expression), + Location: c.loc(e), + } + default: // Negative + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: "-"}}, + }, + Rexpr: c.convertScalarExpression(e.Expression), + Location: c.loc(e), + } + } + case *tsql.ParenthesisExpression: + return c.convertScalarExpression(e.Expression) + case *tsql.FunctionCall: + return c.convertFunctionCall(e) + case *tsql.CastCall: + return &ast.TypeCast{ + Arg: c.convertScalarExpression(e.Parameter), + TypeName: &ast.TypeName{Name: dataTypeName(e.DataType)}, + Location: c.loc(e), + } + case *tsql.TryCastCall: + return &ast.TypeCast{ + Arg: c.convertScalarExpression(e.Parameter), + TypeName: &ast.TypeName{Name: dataTypeName(e.DataType)}, + Location: c.loc(e), + } + case *tsql.ConvertCall: + return &ast.TypeCast{ + Arg: c.convertScalarExpression(e.Parameter), + TypeName: &ast.TypeName{Name: dataTypeName(e.DataType)}, + Location: c.loc(e), + } + case *tsql.TryConvertCall: + return &ast.TypeCast{ + Arg: c.convertScalarExpression(e.Parameter), + TypeName: &ast.TypeName{Name: dataTypeName(e.DataType)}, + Location: c.loc(e), + } + case *tsql.CoalesceExpression: + coalesce := &ast.CoalesceExpr{ + Args: &ast.List{}, + Location: c.loc(e), + } + for _, arg := range e.Expressions { + coalesce.Args.Items = append(coalesce.Args.Items, c.convertScalarExpression(arg)) + } + return coalesce + case *tsql.NullIfExpression: + return &ast.FuncCall{ + Funcname: &ast.List{ + Items: []ast.Node{&ast.String{Str: "nullif"}}, + }, + Args: &ast.List{ + Items: []ast.Node{ + c.convertScalarExpression(e.FirstExpression), + c.convertScalarExpression(e.SecondExpression), + }, + }, + Location: c.loc(e), + } + case *tsql.IIfCall: + return &ast.CaseExpr{ + Args: &ast.List{ + Items: []ast.Node{ + &ast.CaseWhen{ + Expr: c.convertBooleanExpression(e.Predicate), + Result: c.convertScalarExpression(e.ThenExpression), + }, + }, + }, + Defresult: c.convertScalarExpression(e.ElseExpression), + Location: c.loc(e), + } + case *tsql.SearchedCaseExpression: + caseExpr := &ast.CaseExpr{ + Args: &ast.List{}, + Location: c.loc(e), + } + for _, when := range e.WhenClauses { + caseExpr.Args.Items = append(caseExpr.Args.Items, &ast.CaseWhen{ + Expr: c.convertBooleanExpression(when.WhenExpression), + Result: c.convertScalarExpression(when.ThenExpression), + }) + } + if e.ElseExpression != nil { + caseExpr.Defresult = c.convertScalarExpression(e.ElseExpression) + } + return caseExpr + case *tsql.SimpleCaseExpression: + caseExpr := &ast.CaseExpr{ + Arg: c.convertScalarExpression(e.InputExpression), + Args: &ast.List{}, + Location: c.loc(e), + } + for _, when := range e.WhenClauses { + caseExpr.Args.Items = append(caseExpr.Args.Items, &ast.CaseWhen{ + Expr: c.convertScalarExpression(when.WhenExpression), + Result: c.convertScalarExpression(when.ThenExpression), + }) + } + if e.ElseExpression != nil { + caseExpr.Defresult = c.convertScalarExpression(e.ElseExpression) + } + return caseExpr + case *tsql.ScalarSubquery: + return &ast.SubLink{ + SubLinkType: ast.EXPR_SUBLINK, + Subselect: c.convertQueryExpression(e.QueryExpression), + Location: c.loc(e), + } + case *tsql.ParameterlessCall: + return &ast.FuncCall{ + Funcname: &ast.List{ + Items: []ast.Node{&ast.String{Str: identifier(e.ParameterlessCallType)}}, + }, + Location: c.loc(e), + } + default: + return todo(expr) + } +} + +func binaryOperator(binaryExpressionType string) string { + switch binaryExpressionType { + case "Add": + return "+" + case "Subtract": + return "-" + case "Multiply": + return "*" + case "Divide": + return "/" + case "Modulo": + return "%" + case "BitwiseAnd": + return "&" + case "BitwiseOr": + return "|" + case "BitwiseXor": + return "^" + default: + return "+" + } +} + +func (c *cc) convertColumnReference(n *tsql.ColumnReferenceExpression) *ast.ColumnRef { + fields := &ast.List{} + if n.MultiPartIdentifier != nil { + for _, id := range n.MultiPartIdentifier.Identifiers { + fields.Items = append(fields.Items, NewIdentifier(id.Value)) + } + } + if n.ColumnType == "Wildcard" { + fields.Items = append(fields.Items, &ast.A_Star{}) + } + return &ast.ColumnRef{ + Fields: fields, + Location: c.loc(n), + } +} + +// convertVariableReference converts an "@name" query parameter. Repeated uses +// of a name share a single parameter number. +func (c *cc) convertVariableReference(n *tsql.VariableReference) ast.Node { + name := strings.TrimPrefix(n.Name, "@") + number, ok := c.namedParams[name] + if !ok { + c.paramCount++ + number = c.paramCount + if c.namedParams == nil { + c.namedParams = map[string]int{} + } + c.namedParams[name] = number + } + return &ast.ParamRef{ + Number: number, + Location: c.loc(n), + } +} + +func (c *cc) convertFunctionCall(n *tsql.FunctionCall) *ast.FuncCall { + fc := &ast.FuncCall{ + Funcname: &ast.List{ + Items: []ast.Node{&ast.String{Str: identifierValue(n.FunctionName)}}, + }, + Location: c.loc(n), + AggDistinct: n.UniqueRowFilter == "Distinct", + } + + for _, param := range n.Parameters { + // 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 + continue + } + if fc.Args == nil { + fc.Args = &ast.List{} + } + fc.Args.Items = append(fc.Args.Items, c.convertScalarExpression(param)) + } + + if n.OverClause != nil { + fc.Over = &ast.WindowDef{Location: c.loc(n.OverClause)} + if len(n.OverClause.Partitions) > 0 { + fc.Over.PartitionClause = &ast.List{} + for _, p := range n.OverClause.Partitions { + fc.Over.PartitionClause.Items = append(fc.Over.PartitionClause.Items, c.convertScalarExpression(p)) + } + } + if n.OverClause.OrderByClause != nil { + fc.Over.OrderClause = c.convertOrderByClause(n.OverClause.OrderByClause) + } + } + + return fc +} + +// 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. +func (c *cc) convertOutputClause(n *tsql.OutputClause) *ast.List { + if n == nil || len(n.SelectColumns) == 0 { + return nil + } + list := &ast.List{} + for _, elem := range n.SelectColumns { + target := c.convertSelectElement(elem) + if res, ok := target.(*ast.ResTarget); ok { + stripOutputQualifier(res.Val) + } + list.Items = append(list.Items, target) + } + return list +} + +func stripOutputQualifier(node ast.Node) { + ref, ok := node.(*ast.ColumnRef) + if !ok || ref.Fields == nil || len(ref.Fields.Items) < 2 { + return + } + if first, ok := ref.Fields.Items[0].(*ast.String); ok { + switch first.Str { + case "inserted", "deleted": + ref.Fields.Items = ref.Fields.Items[1:] + } + } +} + +func (c *cc) convertInsertStatement(n *tsql.InsertStatement) ast.Node { + if n.InsertSpecification == nil { + return todo(n) + } + spec := n.InsertSpecification + + target, ok := spec.Target.(*tsql.NamedTableReference) + if !ok { + return todo(n) + } + + stmt := &ast.InsertStmt{ + Relation: c.parseRangeVar(target.SchemaObject), + } + + if len(spec.Columns) > 0 { + stmt.Cols = &ast.List{} + for _, col := range spec.Columns { + if col.MultiPartIdentifier == nil || len(col.MultiPartIdentifier.Identifiers) == 0 { + continue + } + ids := col.MultiPartIdentifier.Identifiers + name := identifierValue(ids[len(ids)-1]) + stmt.Cols.Items = append(stmt.Cols.Items, &ast.ResTarget{ + Name: &name, + Location: c.loc(col), + }) + } + } + + switch src := spec.InsertSource.(type) { + case *tsql.ValuesInsertSource: + if !src.IsDefaultValues { + sel := &ast.SelectStmt{ + ValuesLists: &ast.List{}, + } + for _, row := range src.RowValues { + rowList := &ast.List{} + for _, val := range row.ColumnValues { + rowList.Items = append(rowList.Items, c.convertScalarExpression(val)) + } + sel.ValuesLists.Items = append(sel.ValuesLists.Items, rowList) + } + stmt.SelectStmt = sel + } + case *tsql.SelectInsertSource: + stmt.SelectStmt = c.convertQueryExpression(src.Select) + } + + if returning := c.convertOutputClause(spec.OutputClause); returning != nil { + stmt.ReturningList = returning + } + + return stmt +} + +func (c *cc) convertUpdateStatement(n *tsql.UpdateStatement) ast.Node { + if n.UpdateSpecification == nil { + return todo(n) + } + spec := n.UpdateSpecification + + target, ok := spec.Target.(*tsql.NamedTableReference) + if !ok { + return todo(n) + } + + rv, fromItems, quals := c.dmlTargetAndFrom(target, spec.FromClause) + stmt := &ast.UpdateStmt{ + Relations: &ast.List{ + Items: []ast.Node{rv}, + }, + TargetList: &ast.List{}, + FromClause: &ast.List{Items: fromItems}, + ReturningList: &ast.List{}, + WithClause: c.convertWithClause(n.WithCtesAndXmlNamespaces), + } + + for _, sc := range spec.SetClauses { + assign, ok := sc.(*tsql.AssignmentSetClause) + if !ok || assign.Column == nil { + continue + } + ids := assign.Column.MultiPartIdentifier + if ids == nil || len(ids.Identifiers) == 0 { + continue + } + name := identifierValue(ids.Identifiers[len(ids.Identifiers)-1]) + stmt.TargetList.Items = append(stmt.TargetList.Items, &ast.ResTarget{ + Name: &name, + Val: c.convertScalarExpression(assign.NewValue), + Location: c.loc(assign), + }) + } + + if spec.WhereClause != nil { + stmt.WhereClause = c.convertBooleanExpression(spec.WhereClause.SearchCondition) + } + stmt.WhereClause = andQuals(stmt.WhereClause, quals) + + if returning := c.convertOutputClause(spec.OutputClause); returning != nil { + stmt.ReturningList = returning + } + + return stmt +} + +func (c *cc) convertDeleteStatement(n *tsql.DeleteStatement) ast.Node { + if n.DeleteSpecification == nil { + return todo(n) + } + spec := n.DeleteSpecification + + target, ok := spec.Target.(*tsql.NamedTableReference) + if !ok { + return todo(n) + } + + rv, fromItems, quals := c.dmlTargetAndFrom(target, spec.FromClause) + stmt := &ast.DeleteStmt{ + Relations: &ast.List{ + Items: []ast.Node{rv}, + }, + ReturningList: &ast.List{}, + WithClause: c.convertWithClause(n.WithCtesAndXmlNamespaces), + } + if len(fromItems) > 0 { + stmt.FromClause = &ast.List{Items: fromItems} + } + + if spec.WhereClause != nil { + stmt.WhereClause = c.convertBooleanExpression(spec.WhereClause.SearchCondition) + } + stmt.WhereClause = andQuals(stmt.WhereClause, quals) + + if returning := c.convertOutputClause(spec.OutputClause); returning != nil { + stmt.ReturningList = returning + } + + return stmt +} + +// dmlTargetAndFrom resolves an UPDATE or DELETE statement's target against +// its FROM clause. T-SQL names the target by the alias the FROM clause gives +// it — "UPDATE a SET ... FROM authors a" — so a target matching a FROM +// relation becomes that relation, removed from the returned FROM items. The +// ON conditions of inner joins dissolved by the removal are returned as +// extra WHERE conditions. +func (c *cc) dmlTargetAndFrom(target *tsql.NamedTableReference, from *tsql.FromClause) (*ast.RangeVar, []ast.Node, []ast.Node) { + rv := c.parseRangeVar(target.SchemaObject) + if target.Alias != nil { + alias := identifierValue(target.Alias) + rv.Alias = &ast.Alias{Aliasname: &alias} + } + var items []ast.Node + if from != nil { + for _, ref := range from.TableReferences { + items = append(items, c.convertTableReference(ref)) + } + } + if rv.Schemaname == nil && rv.Alias == nil && rv.Relname != nil { + for i, item := range items { + found, remaining, quals := extractTarget(item, *rv.Relname) + if found == nil { + continue + } + if remaining == nil { + items = append(items[:i], items[i+1:]...) + } else { + items[i] = remaining + } + return found, items, quals + } + } + return rv, items, nil +} + +// extractTarget removes the relation named name — by alias, or by table name +// when unaliased — from a FROM item. It returns the extracted relation, what +// remains of the item (nil when the relation was the whole item), and the ON +// conditions of any inner join dissolved by the removal. Relations under an +// outer join are left alone: pulling one out would change the join's meaning. +func extractTarget(item ast.Node, name string) (*ast.RangeVar, ast.Node, []ast.Node) { + switch v := item.(type) { + case *ast.RangeVar: + if rangeVarNamed(v, name) { + return v, nil, nil + } + case *ast.JoinExpr: + if v.Jointype != ast.JoinTypeInner { + return nil, item, nil + } + if found, remaining, quals := extractTarget(v.Larg, name); found != nil { + if remaining == nil { + if v.Quals != nil { + quals = append(quals, v.Quals) + } + return found, v.Rarg, quals + } + v.Larg = remaining + return found, v, quals + } + if found, remaining, quals := extractTarget(v.Rarg, name); found != nil { + if remaining == nil { + if v.Quals != nil { + quals = append(quals, v.Quals) + } + return found, v.Larg, quals + } + v.Rarg = remaining + return found, v, quals + } + } + return nil, item, nil +} + +func rangeVarNamed(rv *ast.RangeVar, name string) bool { + if rv.Alias != nil && rv.Alias.Aliasname != nil { + return *rv.Alias.Aliasname == name + } + return rv.Schemaname == nil && rv.Relname != nil && *rv.Relname == name +} + +// andQuals conjoins the ON conditions of dissolved inner joins onto a WHERE +// clause; for inner joins the two forms are equivalent. +func andQuals(where ast.Node, quals []ast.Node) ast.Node { + for _, q := range quals { + if where == nil { + where = q + continue + } + where = &ast.BoolExpr{ + Boolop: ast.BoolExprTypeAnd, + Args: &ast.List{Items: []ast.Node{where, q}}, + } + } + return where +} + +func (c *cc) convertCreateTableStatement(n *tsql.CreateTableStatement) ast.Node { + if n.SchemaObjectName == nil || n.Definition == nil { + return todo(n) + } + + stmt := &ast.CreateTableStmt{ + Name: parseTableName(n.SchemaObjectName), + } + + // Column names in table-level PRIMARY KEY constraints are NOT NULL. + primaryKey := map[string]bool{} + for _, constraint := range n.Definition.TableConstraints { + unique, ok := constraint.(*tsql.UniqueConstraintDefinition) + if !ok || !unique.IsPrimaryKey { + continue + } + for _, col := range unique.Columns { + if col.Column == nil || col.Column.MultiPartIdentifier == nil { + continue + } + ids := col.Column.MultiPartIdentifier.Identifiers + if len(ids) == 0 { + continue + } + primaryKey[identifierValue(ids[len(ids)-1])] = true + } + } + + for _, col := range n.Definition.ColumnDefinitions { + stmt.Cols = append(stmt.Cols, c.convertColumnDefinition(col, primaryKey)) + } + + return stmt +} + +func (c *cc) convertColumnDefinition(n *tsql.ColumnDefinition, tablePrimaryKey map[string]bool) *ast.ColumnDef { + name := identifierValue(n.ColumnIdentifier) + colDef := &ast.ColumnDef{ + Colname: name, + TypeName: &ast.TypeName{Name: dataTypeName(n.DataType)}, + Location: c.loc(n), + } + + // T-SQL columns are nullable unless declared otherwise. + if n.Nullable != nil && !n.Nullable.Nullable { + colDef.IsNotNull = true + } + // IDENTITY columns are always NOT NULL. + if n.IdentityOptions != nil { + colDef.IsNotNull = true + } + for _, constraint := range n.Constraints { + switch cons := constraint.(type) { + case *tsql.NullableConstraintDefinition: + colDef.IsNotNull = !cons.Nullable + case *tsql.UniqueConstraintDefinition: + if cons.IsPrimaryKey { + colDef.PrimaryKey = true + colDef.IsNotNull = true + } + } + } + if tablePrimaryKey[name] { + colDef.PrimaryKey = true + colDef.IsNotNull = true + } + + return colDef +} + +// dataTypeName returns the lowercased base name of a column's declared type, +// e.g. "nvarchar" for NVARCHAR(100). Length and precision arguments do not +// name distinct types. +func dataTypeName(ref tsql.DataTypeReference) string { + switch t := ref.(type) { + case *tsql.SqlDataTypeReference: + if t.Name != nil && t.Name.BaseIdentifier != nil { + return identifierValue(t.Name.BaseIdentifier) + } + return identifier(t.SqlDataTypeOption) + case *tsql.XmlDataTypeReference: + return "xml" + case *tsql.UserDataTypeReference: + if t.Name != nil && t.Name.BaseIdentifier != nil { + return identifierValue(t.Name.BaseIdentifier) + } + } + return "" +} + +func (c *cc) convertDropTableStatement(n *tsql.DropTableStatement) ast.Node { + stmt := &ast.DropTableStmt{ + IfExists: n.IsIfExists, + } + for _, obj := range n.Objects { + stmt.Tables = append(stmt.Tables, parseTableName(obj)) + } + return stmt +} + +func (c *cc) convertAlterTableAddTableElementStatement(n *tsql.AlterTableAddTableElementStatement) ast.Node { + if n.SchemaObjectName == nil || n.Definition == nil { + return todo(n) + } + stmt := &ast.AlterTableStmt{ + Table: parseTableName(n.SchemaObjectName), + Cmds: &ast.List{}, + } + for _, col := range n.Definition.ColumnDefinitions { + def := c.convertColumnDefinition(col, nil) + stmt.Cmds.Items = append(stmt.Cmds.Items, &ast.AlterTableCmd{ + Name: &def.Colname, + Subtype: ast.AT_AddColumn, + Def: def, + }) + } + return stmt +} + +func (c *cc) convertAlterTableDropTableElementStatement(n *tsql.AlterTableDropTableElementStatement) ast.Node { + if n.SchemaObjectName == nil { + return todo(n) + } + stmt := &ast.AlterTableStmt{ + Table: parseTableName(n.SchemaObjectName), + Cmds: &ast.List{}, + } + for _, elem := range n.AlterTableDropTableElements { + if elem.TableElementType != "Column" || elem.Name == nil { + continue + } + name := identifierValue(elem.Name) + stmt.Cmds.Items = append(stmt.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_DropColumn, + MissingOk: elem.IsIfExists, + }) + } + if len(stmt.Cmds.Items) == 0 { + return todo(n) + } + return stmt +} diff --git a/internal/engine/mssql/dialect/dialect.json b/internal/engine/mssql/dialect/dialect.json new file mode 100644 index 0000000000..88d4088df1 --- /dev/null +++ b/internal/engine/mssql/dialect/dialect.json @@ -0,0 +1,15 @@ +{ + "dialect": "mssql", + "const": { + "integer": "int", + "float": "float", + "string": "varchar", + "bool": "bit" + }, + "bool": "bit", + "comparison": ["=", "<>", "!=", "<", "<=", ">", ">="], + "comparison_categories": "NBSD", + "arithmetic": ["+", "-", "*", "/", "%"], + "arithmetic_categories": "N", + "cast_categories": "NSD" +} diff --git a/internal/engine/mssql/dialect/functions.jsonl b/internal/engine/mssql/dialect/functions.jsonl new file mode 100644 index 0000000000..e0cd45408c --- /dev/null +++ b/internal/engine/mssql/dialect/functions.jsonl @@ -0,0 +1,76 @@ +{"name": "count", "kind": "a", "returns": "int"} +{"name": "count_big", "kind": "a", "returns": "bigint"} +{"name": "sum", "kind": "a", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "sum", "kind": "a", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "sum", "kind": "a", "args": [{"type": "smallint"}], "returns": "int", "nullable": true} +{"name": "sum", "kind": "a", "args": [{"type": "tinyint"}], "returns": "int", "nullable": true} +{"name": "sum", "kind": "a", "args": [{"type": "decimal"}], "returns": "decimal", "nullable": true} +{"name": "sum", "kind": "a", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "sum", "kind": "a", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "sum", "kind": "a", "args": [{"type": "real"}], "returns": "float", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "smallint"}], "returns": "int", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "tinyint"}], "returns": "int", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "decimal"}], "returns": "decimal", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "avg", "kind": "a", "args": [{"type": "real"}], "returns": "float", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "smallint"}], "returns": "smallint", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "tinyint"}], "returns": "tinyint", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "decimal"}], "returns": "decimal", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "real"}], "returns": "real", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "date"}], "returns": "date", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "datetime"}], "returns": "datetime", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "datetime2"}], "returns": "datetime2", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "datetimeoffset"}], "returns": "datetimeoffset", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "time"}], "returns": "time", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "varchar"}], "returns": "varchar", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "nvarchar"}], "returns": "nvarchar", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "char"}], "returns": "char", "nullable": true} +{"name": "min", "kind": "a", "args": [{"type": "nchar"}], "returns": "nchar", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "bigint"}], "returns": "bigint", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "int"}], "returns": "int", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "smallint"}], "returns": "smallint", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "tinyint"}], "returns": "tinyint", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "decimal"}], "returns": "decimal", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "money"}], "returns": "money", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "float"}], "returns": "float", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "real"}], "returns": "real", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "date"}], "returns": "date", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "datetime"}], "returns": "datetime", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "datetime2"}], "returns": "datetime2", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "datetimeoffset"}], "returns": "datetimeoffset", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "time"}], "returns": "time", "nullable": true} +{"name": "max", "kind": "a", "args": [{"type": "varchar"}], "returns": "varchar", "nullable": true} +{"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": "getdate", "returns": "datetime"} +{"name": "getutcdate", "returns": "datetime"} +{"name": "sysdatetime", "returns": "datetime2"} +{"name": "sysutcdatetime", "returns": "datetime2"} +{"name": "sysdatetimeoffset", "returns": "datetimeoffset"} +{"name": "current_timestamp", "returns": "datetime"} +{"name": "newid", "returns": "uniqueidentifier"} +{"name": "newsequentialid", "returns": "uniqueidentifier"} +{"name": "scope_identity", "returns": "decimal"} +{"name": "len", "args": [{"type": "varchar"}], "returns": "int"} +{"name": "len", "args": [{"type": "nvarchar"}], "returns": "int"} +{"name": "datalength", "args": [{"type": "varchar"}], "returns": "int"} +{"name": "datalength", "args": [{"type": "nvarchar"}], "returns": "int"} +{"name": "datalength", "args": [{"type": "varbinary"}], "returns": "int"} +{"name": "lower", "args": [{"type": "varchar"}], "returns": "varchar"} +{"name": "lower", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "upper", "args": [{"type": "varchar"}], "returns": "varchar"} +{"name": "upper", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "ltrim", "args": [{"type": "varchar"}], "returns": "varchar"} +{"name": "ltrim", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "rtrim", "args": [{"type": "varchar"}], "returns": "varchar"} +{"name": "rtrim", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} +{"name": "trim", "args": [{"type": "varchar"}], "returns": "varchar"} +{"name": "trim", "args": [{"type": "nvarchar"}], "returns": "nvarchar"} diff --git a/internal/engine/mssql/dialect/types.jsonl b/internal/engine/mssql/dialect/types.jsonl new file mode 100644 index 0000000000..e730686aa7 --- /dev/null +++ b/internal/engine/mssql/dialect/types.jsonl @@ -0,0 +1,34 @@ +{"name": "bigint", "category": "N"} +{"name": "int", "category": "N", "aliases": ["integer"]} +{"name": "smallint", "category": "N"} +{"name": "tinyint", "category": "N"} +{"name": "bit", "category": "B"} +{"name": "decimal", "category": "N", "aliases": ["dec", "numeric"]} +{"name": "money", "category": "N"} +{"name": "smallmoney", "category": "N"} +{"name": "float", "category": "N", "aliases": ["double precision"]} +{"name": "real", "category": "N"} +{"name": "date", "category": "D"} +{"name": "datetime", "category": "D"} +{"name": "datetime2", "category": "D"} +{"name": "smalldatetime", "category": "D"} +{"name": "datetimeoffset", "category": "D"} +{"name": "time", "category": "D"} +{"name": "char", "category": "S", "aliases": ["character"]} +{"name": "varchar", "category": "S"} +{"name": "text", "category": "S"} +{"name": "nchar", "category": "S"} +{"name": "nvarchar", "category": "S"} +{"name": "ntext", "category": "S"} +{"name": "binary", "category": "S"} +{"name": "varbinary", "category": "S"} +{"name": "image", "category": "S"} +{"name": "uniqueidentifier", "category": "S"} +{"name": "xml", "category": "U"} +{"name": "json", "category": "U"} +{"name": "sql_variant", "category": "U"} +{"name": "rowversion", "category": "U", "aliases": ["timestamp"]} +{"name": "hierarchyid", "category": "U"} +{"name": "geography", "category": "U"} +{"name": "geometry", "category": "U"} +{"name": "vector", "category": "U"} diff --git a/internal/engine/mssql/parse.go b/internal/engine/mssql/parse.go new file mode 100644 index 0000000000..e222708ece --- /dev/null +++ b/internal/engine/mssql/parse.go @@ -0,0 +1,187 @@ +package mssql + +import ( + "bytes" + "context" + "io" + "unicode/utf8" + + "github.com/sqlc-dev/teesql/parser" + + "github.com/sqlc-dev/sqlc/internal/source" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func NewParser() *Parser { + return &Parser{} +} + +type Parser struct{} + +func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { + blob, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + ctx := context.Background() + script, err := parser.Parse(ctx, bytes.NewReader(blob)) + if err != nil { + return nil, err + } + + toByte := byteOffsets(blob) + + var stmts []ast.Statement + loc := 0 + for _, batch := range script.Batches { + for _, stmt := range batch.Statements { + start := loc + if frag, ok := stmt.(fragmented); ok && frag.Frag().HasSpan() { + if s := toByte(frag.Frag().StartOffset); s > start { + start = s + } + } + end := statementEnd(blob, start) + + converter := &cc{toByte: toByte} + out := converter.convert(stmt) + if _, ok := out.(*ast.TODO); ok { + loc = end + continue + } + + stmts = append(stmts, ast.Statement{ + Raw: &ast.RawStmt{ + Stmt: out, + StmtLocation: loc, + StmtLen: end - loc, + }, + }) + loc = end + } + } + + return stmts, nil +} + +// byteOffsets returns a function mapping a UTF-16 code-unit offset — how +// teesql records source spans, mirroring ScriptDom — to a byte offset in +// blob. For ASCII input the two are identical and the identity is returned. +func byteOffsets(blob []byte) func(int) int { + ascii := true + for _, b := range blob { + if b >= utf8.RuneSelf { + ascii = false + break + } + } + if ascii { + return func(off int) int { return off } + } + // Walk the blob once, recording the byte index at which each UTF-16 + // offset begins. + byteAt := make([]int, 0, len(blob)+1) + for i := 0; i < len(blob); { + r, size := utf8.DecodeRune(blob[i:]) + units := 1 + if r > 0xFFFF { + units = 2 + } + for u := 0; u < units; u++ { + byteAt = append(byteAt, i) + } + i += size + } + byteAt = append(byteAt, len(blob)) + return func(off int) int { + if off < 0 { + return 0 + } + if off >= len(byteAt) { + return len(blob) + } + return byteAt[off] + } +} + +// statementEnd scans from start for the semicolon ending the statement, +// skipping string literals, quoted and bracketed identifiers, and comments. +func statementEnd(blob []byte, start int) int { + for i := start; i < len(blob); i++ { + switch blob[i] { + case '\'', '"': + i = skipQuoted(blob, i) + case '[': + i = skipBracketed(blob, i) + case '-': + if i+1 < len(blob) && blob[i+1] == '-' { + i = skipLineComment(blob, i) + } + case '/': + if i+1 < len(blob) && blob[i+1] == '*' { + i = skipBlockComment(blob, i) + } + case ';': + return i + 1 + } + } + return len(blob) +} + +// skipQuoted skips a string literal or quoted identifier; T-SQL escapes the +// quote character by doubling it. +func skipQuoted(blob []byte, i int) int { + q := blob[i] + for j := i + 1; j < len(blob); j++ { + if blob[j] == q { + if j+1 < len(blob) && blob[j+1] == q { + j++ + continue + } + return j + } + } + return len(blob) - 1 +} + +// skipBracketed skips a [bracketed identifier]; a closing bracket is escaped +// by doubling it. +func skipBracketed(blob []byte, i int) int { + for j := i + 1; j < len(blob); j++ { + if blob[j] == ']' { + if j+1 < len(blob) && blob[j+1] == ']' { + j++ + continue + } + return j + } + } + return len(blob) - 1 +} + +func skipLineComment(blob []byte, i int) int { + for j := i; j < len(blob); j++ { + if blob[j] == '\n' { + return j + } + } + return len(blob) - 1 +} + +func skipBlockComment(blob []byte, i int) int { + for j := i + 2; j < len(blob); j++ { + if blob[j] == '*' && j+1 < len(blob) && blob[j+1] == '/' { + return j + 1 + } + } + return len(blob) - 1 +} + +// https://learn.microsoft.com/en-us/sql/t-sql/language-elements/comment-transact-sql +func (p *Parser) CommentSyntax() source.CommentSyntax { + return source.CommentSyntax{ + Dash: true, // -- comment + SlashStar: true, // /* comment */ + } +} diff --git a/internal/engine/mssql/reserved.go b/internal/engine/mssql/reserved.go new file mode 100644 index 0000000000..e1a697a1ca --- /dev/null +++ b/internal/engine/mssql/reserved.go @@ -0,0 +1,196 @@ +package mssql + +import "strings" + +// https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql +func (p *Parser) IsReservedKeyword(s string) bool { + switch strings.ToLower(s) { + case "add": + case "all": + case "alter": + case "and": + case "any": + case "as": + case "asc": + case "authorization": + case "backup": + case "begin": + case "between": + case "break": + case "browse": + case "bulk": + case "by": + case "cascade": + case "case": + case "check": + case "checkpoint": + case "close": + case "clustered": + case "coalesce": + case "collate": + case "column": + case "commit": + case "compute": + case "constraint": + case "contains": + case "containstable": + case "continue": + case "convert": + case "create": + case "cross": + case "current": + case "current_date": + case "current_time": + case "current_timestamp": + case "current_user": + case "cursor": + case "database": + case "dbcc": + case "deallocate": + case "declare": + case "default": + case "delete": + case "deny": + case "desc": + case "disk": + case "distinct": + case "distributed": + case "double": + case "drop": + case "dump": + case "else": + case "end": + case "errlvl": + case "escape": + case "except": + case "exec": + case "execute": + case "exists": + case "exit": + case "external": + case "fetch": + case "file": + case "fillfactor": + case "for": + case "foreign": + case "freetext": + case "freetexttable": + case "from": + case "full": + case "function": + case "goto": + case "grant": + case "group": + case "having": + case "holdlock": + case "identity": + case "identity_insert": + case "identitycol": + case "if": + case "in": + case "index": + case "inner": + case "insert": + case "intersect": + case "into": + case "is": + case "join": + case "key": + case "kill": + case "left": + case "like": + case "lineno": + case "load": + case "merge": + case "national": + case "nocheck": + case "nonclustered": + case "not": + case "null": + case "nullif": + case "of": + case "off": + case "offsets": + case "on": + case "open": + case "opendatasource": + case "openquery": + case "openrowset": + case "openxml": + case "option": + case "or": + case "order": + case "outer": + case "over": + case "percent": + case "pivot": + case "plan": + case "precision": + case "primary": + case "print": + case "proc": + case "procedure": + case "public": + case "raiserror": + case "read": + case "readtext": + case "reconfigure": + case "references": + case "replication": + case "restore": + case "restrict": + case "return": + case "revert": + case "revoke": + case "right": + case "rollback": + case "rowcount": + case "rowguidcol": + case "rule": + case "save": + case "schema": + case "securityaudit": + case "select": + case "semantickeyphrasetable": + case "semanticsimilaritydetailstable": + case "semanticsimilaritytable": + case "session_user": + case "set": + case "setuser": + case "shutdown": + case "some": + case "statistics": + case "system_user": + case "table": + case "tablesample": + case "textsize": + case "then": + case "to": + case "top": + case "tran": + case "transaction": + case "trigger": + case "truncate": + case "try_convert": + case "tsequal": + case "union": + case "unique": + case "unpivot": + case "update": + case "updatetext": + case "use": + case "user": + case "values": + case "varying": + case "view": + case "waitfor": + case "when": + case "where": + case "while": + case "with": + case "writetext": + default: + return false + } + return true +} diff --git a/internal/engine/mssql/seed.go b/internal/engine/mssql/seed.go new file mode 100644 index 0000000000..7ce732b129 --- /dev/null +++ b/internal/engine/mssql/seed.go @@ -0,0 +1,16 @@ +package mssql + +import ( + "embed" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/core/seed" +) + +//go:embed dialect +var dialectFS embed.FS + +// Dialect returns the catalog option that seeds SQL Server's type system. +func Dialect() core.Option { + return seed.Dialect(dialectFS, "dialect") +} diff --git a/internal/engine/mssql/utils.go b/internal/engine/mssql/utils.go new file mode 100644 index 0000000000..067566912d --- /dev/null +++ b/internal/engine/mssql/utils.go @@ -0,0 +1,65 @@ +package mssql + +import ( + "log" + "strings" + + tsql "github.com/sqlc-dev/teesql/ast" + + "github.com/sqlc-dev/sqlc/internal/debug" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// fragmented is satisfied by every teesql node via its embedded Fragment. +type fragmented interface { + Frag() *tsql.Fragment +} + +func todo(n tsql.Node) *ast.TODO { + if debug.Active { + log.Printf("mssql.convert: Unknown node type %T\n", n) + } + return &ast.TODO{} +} + +// identifier normalizes an identifier. T-SQL identifiers are +// case-insensitive under the default collations. +func identifier(id string) string { + return strings.ToLower(id) +} + +func NewIdentifier(t string) *ast.String { + return &ast.String{Str: identifier(t)} +} + +func identifierValue(id *tsql.Identifier) string { + if id == nil { + return "" + } + return identifier(id.Value) +} + +// schemaName returns the schema an object name qualifies it with. The "dbo" +// default schema maps to the catalog's default namespace, so it is treated +// as unqualified. +func schemaName(n *tsql.SchemaObjectName) string { + if n == nil || n.SchemaIdentifier == nil { + return "" + } + s := identifier(n.SchemaIdentifier.Value) + if s == "dbo" { + return "" + } + return s +} + +func parseTableName(n *tsql.SchemaObjectName) *ast.TableName { + if n == nil { + return &ast.TableName{} + } + return &ast.TableName{ + Schema: schemaName(n), + Name: identifierValue(n.BaseIdentifier), + } +} +