Skip to content

Conversation

@kyleconroy
Copy link
Collaborator

Add MSSQL/SQL Server engine support using the sqlc-dev/teesql parser.
This implementation uses database-only mode (analyzer.database: only)
which analyzes queries against a live SQL Server database using
sp_describe_first_result_set for column metadata.

Features:

  • T-SQL parser integration using teesql
  • Database analyzer using go-mssqldb driver
  • MSSQL catalog with dbo default schema
  • Example configuration in examples/authors/sqlserver

The SQL Server engine requires a database connection and does not
support static catalog analysis.

🤖 Generated with Claude Code

Co-Authored-By: Claude [email protected]

Add MSSQL/SQL Server engine support using the sqlc-dev/teesql parser.
This implementation uses database-only mode (analyzer.database: only)
which analyzes queries against a live SQL Server database using
sp_describe_first_result_set for column metadata.

Features:
- T-SQL parser integration using teesql
- Database analyzer using go-mssqldb driver
- MSSQL catalog with dbo default schema
- Example configuration in examples/authors/sqlserver

The SQL Server engine requires a database connection and does not
support static catalog analysis.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Change the engine identifier from "sqlserver" to "mssql" for consistency
with the package naming and industry conventions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. 🔧 golang labels Dec 23, 2025
Run go mod tidy to properly categorize direct vs indirect dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Move the sqlserver configuration to its own sqlc.yaml file to prevent
test failures in CI. The MSSQL engine requires a database connection
which isn't available in the standard CI environment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add native and Docker support for starting MSSQL Server instances
in end-to-end tests, following the same pattern as PostgreSQL and MySQL.

- Add internal/sqltest/native/mssql.go for native MSSQL service
- Add internal/sqltest/docker/mssql.go for Docker-based MSSQL
- Update endtoend_test.go to initialize and use MSSQL connections

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Consolidate the sqlserver example configuration back into the main
examples/authors/sqlc.yaml file, using an environment variable for
the database URI like the other examples.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Add MSSQL variant of the column_as test to verify basic SELECT
with column aliases works correctly with the MSSQL engine.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Update MSSQL Docker startup code to create a new connection on each
ping attempt, matching the pattern used by PostgreSQL. This ensures
the connection is properly established even if MSSQL takes time to
become ready.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@kyleconroy kyleconroy force-pushed the claude/add-mssql-support-MrXFp branch from b2c8b18 to 1764695 Compare December 24, 2025 01:04
Add PostgreSQL, MySQL, and MSSQL as GitHub Actions services so they're
ready before tests run. This avoids the timeout issues from pulling the
large MSSQL image (~1.5GB) during test execution.

Set environment variables so tests use the pre-started service databases
instead of trying to start their own containers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove GitHub Actions services and let the tests start databases
via the sqltest/docker package, matching the existing pattern for
PostgreSQL and MySQL.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@diffray-bot

This comment was marked as spam.

}
}

uri := "sqlserver://sa:MySecretPassword1!@localhost:1433?database=master"

This comment was marked as spam.

case *ast.ColumnReferenceExpression:
return convertColumnReference(e)
case *ast.IntegerLiteral:
val, _ := strconv.ParseInt(e.Value, 10, 64)

This comment was marked as spam.

Comment on lines +140 to +141
col.Name = name.String
col.IsNullable = isNullable

This comment was marked as spam.

Comment on lines +611 to +619
switch src := spec.InsertSource.(type) {
case *ast.ValuesInsertSource:
// Handle VALUES clauses
stmt.SelectStmt = &sqast.TODO{}
case *ast.SelectInsertSource:
if src.Select != nil {
stmt.SelectStmt = convertQueryExpression(src.Select)
}
}

This comment was marked as spam.

Comment on lines +200 to +209
func countParameters(query string) int {
count := 0
for i := 1; i <= 100; i++ {
param := fmt.Sprintf("@p%d", i)
if strings.Contains(query, param) {
count = i
}
}
return count
}

This comment was marked as spam.

Comment on lines +422 to +428
scanArgs := make([]interface{}, 39)
scanArgs[0] = &isHidden
scanArgs[1] = &colOrdinal
scanArgs[2] = &name
for i := 3; i < 39; i++ {
scanArgs[i] = &dummy
}

This comment was marked as spam.

Comment on lines +58 to +190
func (a *Analyzer) analyzeQuery(ctx context.Context, n ast.Node, query string, ps *named.ParamSet) (*core.Analysis, error) {
var result core.Analysis

// Use sp_describe_first_result_set to get column metadata
// This is MSSQL's equivalent of PostgreSQL's PREPARE for getting result set metadata
rows, err := a.conn.QueryContext(ctx, "EXEC sp_describe_first_result_set @tsql = @p1", query)
if err != nil {
return nil, a.extractSqlErr(n, err)
}
defer rows.Close()

for rows.Next() {
var col columnInfo
// sp_describe_first_result_set returns many columns, we only need a few
// Columns: is_hidden, column_ordinal, name, is_nullable, system_type_id, system_type_name,
// max_length, precision, scale, collation_name, user_type_id, user_type_database,
// user_type_schema, user_type_name, assembly_qualified_type_name, xml_collection_id,
// xml_collection_database, xml_collection_schema, xml_collection_name, is_xml_document,
// is_case_sensitive, is_fixed_length_clr_type, source_server, source_database,
// source_schema, source_table, source_column, is_identity_column, is_part_of_unique_key,
// is_updateable, is_computed_column, is_sparse_column_set, ordinal_in_order_by_list,
// order_by_is_descending, order_by_list_length, tds_type_id, tds_length,
// tds_collation_id, tds_collation_sort_id

var isHidden bool
var colOrdinal int
var name sql.NullString
var isNullable bool
var sysTypeId int
var sysTypeName sql.NullString
var maxLength int
var precision int
var scale int
var collationName sql.NullString
var userTypeId sql.NullInt64
var userTypeDb sql.NullString
var userTypeSchema sql.NullString
var userTypeName sql.NullString
var assemblyQualTypeName sql.NullString
var xmlColId sql.NullInt64
var xmlColDb sql.NullString
var xmlColSchema sql.NullString
var xmlColName sql.NullString
var isXmlDoc bool
var isCaseSensitive bool
var isFixedLenClr bool
var sourceServer sql.NullString
var sourceDb sql.NullString
var sourceSchema sql.NullString
var sourceTable sql.NullString
var sourceColumn sql.NullString
var isIdentity bool
var isPartOfUniqueKey sql.NullBool
var isUpdateable bool
var isComputed bool
var isSparseColSet bool
var ordinalInOrderBy sql.NullInt64
var orderByDesc sql.NullBool
var orderByLen sql.NullInt64
var tdsTypeId sql.NullInt64
var tdsLength sql.NullInt64
var tdsCollationId sql.NullInt64
var tdsCollationSortId sql.NullInt64

err := rows.Scan(
&isHidden, &colOrdinal, &name, &isNullable, &sysTypeId, &sysTypeName,
&maxLength, &precision, &scale, &collationName, &userTypeId, &userTypeDb,
&userTypeSchema, &userTypeName, &assemblyQualTypeName, &xmlColId,
&xmlColDb, &xmlColSchema, &xmlColName, &isXmlDoc, &isCaseSensitive,
&isFixedLenClr, &sourceServer, &sourceDb, &sourceSchema, &sourceTable,
&sourceColumn, &isIdentity, &isPartOfUniqueKey, &isUpdateable,
&isComputed, &isSparseColSet, &ordinalInOrderBy, &orderByDesc,
&orderByLen, &tdsTypeId, &tdsLength, &tdsCollationId, &tdsCollationSortId,
)
if err != nil {
return nil, fmt.Errorf("scanning column info: %w", err)
}

if isHidden {
continue
}

col.Name = name.String
col.IsNullable = isNullable
col.DataType = normalizeTypeName(sysTypeName.String, maxLength, precision, scale)
col.Table = sourceTable.String
col.Schema = sourceSchema.String

coreCol := &core.Column{
Name: col.Name,
OriginalName: col.Name,
DataType: col.DataType,
NotNull: !col.IsNullable,
}

if col.Table != "" {
coreCol.Table = &core.Identifier{
Schema: col.Schema,
Name: col.Table,
}
}

result.Columns = append(result.Columns, coreCol)
}

if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating column info: %w", err)
}

// Get parameter information
// MSSQL doesn't have a built-in way to get parameter metadata from a query string
// We'll count the @pN placeholders in the query and create parameters
paramCount := countParameters(query)
for i := 1; i <= paramCount; i++ {
paramName := ""
if ps != nil {
if n, ok := ps.NameFor(i); ok {
paramName = n
}
}

result.Params = append(result.Params, &core.Parameter{
Number: int32(i),
Column: &core.Column{
Name: paramName,
DataType: "any", // MSSQL doesn't provide parameter type info
NotNull: false,
},
})
}

return &result, nil
}

This comment was marked as spam.

Comment on lines +122 to +131
err := rows.Scan(
&isHidden, &colOrdinal, &name, &isNullable, &sysTypeId, &sysTypeName,
&maxLength, &precision, &scale, &collationName, &userTypeId, &userTypeDb,
&userTypeSchema, &userTypeName, &assemblyQualTypeName, &xmlColId,
&xmlColDb, &xmlColSchema, &xmlColName, &isXmlDoc, &isCaseSensitive,
&isFixedLenClr, &sourceServer, &sourceDb, &sourceSchema, &sourceTable,
&sourceColumn, &isIdentity, &isPartOfUniqueKey, &isUpdateable,
&isComputed, &isSparseColSet, &ordinalInOrderBy, &orderByDesc,
&orderByLen, &tdsTypeId, &tdsLength, &tdsCollationId, &tdsCollationSortId,
)

This comment was marked as spam.

Comment on lines +350 to +356
if _, err := a.conn.ExecContext(ctx, batch); err != nil {
// Check if it's a "already exists" error and skip it
if !isObjectExistsError(err) {
a.conn.Close()
a.conn = nil
return fmt.Errorf("migration failed: %s: %w", batch, err)
}

This comment was marked as spam.

@diffray-bot

This comment was marked as spam.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files. 🔧 golang

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants