feat(mssql): add MSSQL writer for generating DDL from database schema
All checks were successful
CI / Test (1.24) (push) Successful in -23m27s
CI / Test (1.25) (push) Successful in -23m4s
CI / Lint (push) Successful in -24m57s
CI / Build (push) Successful in -25m15s
Integration Tests / Integration Tests (push) Successful in -25m42s

- Implement MSSQL writer to generate SQL scripts for creating schemas, tables, and constraints.
- Support for identity columns, indexes, and extended properties.
- Add tests for column definitions, table creation, primary keys, foreign keys, and comments.
- Include testing guide and sample schema for integration tests.
This commit is contained in:
2026-02-07 16:09:27 +02:00
parent 47bf748fd5
commit 466d657ea7
14 changed files with 2489 additions and 2 deletions

130
pkg/writers/mssql/README.md Normal file
View File

@@ -0,0 +1,130 @@
# MSSQL Writer
Generates Microsoft SQL Server DDL (Data Definition Language) from database schema models.
## Features
- **DDL Generation**: Generates complete SQL scripts for creating MSSQL schema
- **Schema Support**: Creates multiple schemas with proper naming
- **Bracket Notation**: Uses [schema].[table] bracket notation for identifiers
- **Identity Columns**: Generates IDENTITY(1,1) for auto-increment columns
- **Constraints**: Generates primary keys, foreign keys, unique, and check constraints
- **Indexes**: Creates indexes with unique support
- **Extended Properties**: Uses sp_addextendedproperty for comments
- **Direct Execution**: Can directly execute DDL on MSSQL database
- **Schema Flattening**: Optional schema flattening for compatibility
## Features by Phase
1. **Phase 1**: Create schemas
2. **Phase 2**: Create tables with columns, identity, and defaults
3. **Phase 3**: Add primary key constraints
4. **Phase 4**: Create indexes
5. **Phase 5**: Add unique constraints
6. **Phase 6**: Add check constraints
7. **Phase 7**: Add foreign key constraints
8. **Phase 8**: Add extended properties (comments)
## Type Mappings
| Canonical Type | MSSQL Type |
|----------------|-----------|
| int | INT |
| int64 | BIGINT |
| int16 | SMALLINT |
| int8 | TINYINT |
| bool | BIT |
| float32 | REAL |
| float64 | FLOAT |
| decimal | NUMERIC |
| string | NVARCHAR(255) |
| text | NVARCHAR(MAX) |
| timestamp | DATETIME2 |
| timestamptz | DATETIMEOFFSET |
| uuid | UNIQUEIDENTIFIER |
| bytea | VARBINARY(MAX) |
| date | DATE |
| time | TIME |
## Usage
### Generate SQL File
```go
import "git.warky.dev/wdevs/relspecgo/pkg/writers/mssql"
import "git.warky.dev/wdevs/relspecgo/pkg/writers"
writer := mssql.NewWriter(&writers.WriterOptions{
OutputPath: "schema.sql",
FlattenSchema: false,
})
err := writer.WriteDatabase(db)
if err != nil {
panic(err)
}
```
### Direct Database Execution
```go
writer := mssql.NewWriter(&writers.WriterOptions{
OutputPath: "",
Metadata: map[string]interface{}{
"connection_string": "sqlserver://sa:password@localhost/newdb",
},
})
err := writer.WriteDatabase(db)
if err != nil {
panic(err)
}
```
### CLI Usage
Generate SQL file:
```bash
relspec convert --from json --from-path schema.json \
--to mssql --to-path schema.sql
```
Execute directly to database:
```bash
relspec convert --from json --from-path schema.json \
--to mssql \
--metadata '{"connection_string":"sqlserver://sa:password@localhost/mydb"}'
```
## Default Values
The writer supports several default value patterns:
- Functions: `GETDATE()`, `CURRENT_TIMESTAMP()`
- Literals: strings wrapped in quotes, numbers, booleans (0/1 for BIT)
- CAST expressions
## Comments/Extended Properties
Table and column descriptions are stored as MS_Description extended properties:
```sql
EXEC sp_addextendedproperty
@name = 'MS_Description',
@value = 'Table description here',
@level0type = 'SCHEMA', @level0name = 'dbo',
@level1type = 'TABLE', @level1name = 'my_table';
```
## Testing
Run tests with:
```bash
go test ./pkg/writers/mssql/...
```
## Limitations
- Views are not currently supported in the writer
- Sequences are not supported (MSSQL uses IDENTITY instead)
- Partitioning and advanced features are not supported
- Generated DDL assumes no triggers or computed columns

579
pkg/writers/mssql/writer.go Normal file
View File

@@ -0,0 +1,579 @@
package mssql
import (
"context"
"database/sql"
"fmt"
"io"
"os"
"sort"
"strings"
_ "github.com/microsoft/go-mssqldb" // MSSQL driver
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/mssql"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
// Writer implements the Writer interface for MSSQL SQL output
type Writer struct {
options *writers.WriterOptions
writer io.Writer
}
// NewWriter creates a new MSSQL SQL writer
func NewWriter(options *writers.WriterOptions) *Writer {
return &Writer{
options: options,
}
}
// qualTable returns a schema-qualified name using bracket notation
func (w *Writer) qualTable(schema, name string) string {
if w.options.FlattenSchema {
return fmt.Sprintf("[%s]", name)
}
return fmt.Sprintf("[%s].[%s]", schema, name)
}
// WriteDatabase writes the entire database schema as SQL
func (w *Writer) WriteDatabase(db *models.Database) error {
// Check if we should execute SQL directly on a database
if connString, ok := w.options.Metadata["connection_string"].(string); ok && connString != "" {
return w.executeDatabaseSQL(db, connString)
}
var writer io.Writer
var file *os.File
var err error
// Use existing writer if already set (for testing)
if w.writer != nil {
writer = w.writer
} else if w.options.OutputPath != "" {
// Determine output destination
file, err = os.Create(w.options.OutputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer file.Close()
writer = file
} else {
writer = os.Stdout
}
w.writer = writer
// Write header comment
fmt.Fprintf(w.writer, "-- MSSQL Database Schema\n")
fmt.Fprintf(w.writer, "-- Database: %s\n", db.Name)
fmt.Fprintf(w.writer, "-- Generated by RelSpec\n\n")
// Process each schema in the database
for _, schema := range db.Schemas {
if err := w.WriteSchema(schema); err != nil {
return fmt.Errorf("failed to write schema %s: %w", schema.Name, err)
}
}
return nil
}
// WriteSchema writes a single schema and all its tables
func (w *Writer) WriteSchema(schema *models.Schema) error {
if w.writer == nil {
w.writer = os.Stdout
}
// Phase 1: Create schema (skip dbo schema and when flattening)
if schema.Name != "dbo" && !w.options.FlattenSchema {
fmt.Fprintf(w.writer, "-- Schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "CREATE SCHEMA [%s];\n\n", schema.Name)
}
// Phase 2: Create tables with columns
fmt.Fprintf(w.writer, "-- Tables for schema: %s\n", schema.Name)
for _, table := range schema.Tables {
if err := w.writeCreateTable(schema, table); err != nil {
return err
}
}
// Phase 3: Primary keys
fmt.Fprintf(w.writer, "-- Primary keys for schema: %s\n", schema.Name)
for _, table := range schema.Tables {
if err := w.writePrimaryKey(schema, table); err != nil {
return err
}
}
// Phase 4: Indexes
fmt.Fprintf(w.writer, "-- Indexes for schema: %s\n", schema.Name)
for _, table := range schema.Tables {
if err := w.writeIndexes(schema, table); err != nil {
return err
}
}
// Phase 5: Unique constraints
fmt.Fprintf(w.writer, "-- Unique constraints for schema: %s\n", schema.Name)
for _, table := range schema.Tables {
if err := w.writeUniqueConstraints(schema, table); err != nil {
return err
}
}
// Phase 6: Check constraints
fmt.Fprintf(w.writer, "-- Check constraints for schema: %s\n", schema.Name)
for _, table := range schema.Tables {
if err := w.writeCheckConstraints(schema, table); err != nil {
return err
}
}
// Phase 7: Foreign keys
fmt.Fprintf(w.writer, "-- Foreign keys for schema: %s\n", schema.Name)
for _, table := range schema.Tables {
if err := w.writeForeignKeys(schema, table); err != nil {
return err
}
}
// Phase 8: Comments
fmt.Fprintf(w.writer, "-- Comments for schema: %s\n", schema.Name)
for _, table := range schema.Tables {
if err := w.writeComments(schema, table); err != nil {
return err
}
}
return nil
}
// WriteTable writes a single table with all its elements
func (w *Writer) WriteTable(table *models.Table) error {
if w.writer == nil {
w.writer = os.Stdout
}
// Create a temporary schema with just this table
schema := models.InitSchema(table.Schema)
schema.Tables = append(schema.Tables, table)
return w.WriteSchema(schema)
}
// writeCreateTable generates CREATE TABLE statement
func (w *Writer) writeCreateTable(schema *models.Schema, table *models.Table) error {
fmt.Fprintf(w.writer, "CREATE TABLE %s (\n", w.qualTable(schema.Name, table.Name))
// Sort columns by sequence
columns := getSortedColumns(table.Columns)
columnDefs := make([]string, 0, len(columns))
for _, col := range columns {
def := w.generateColumnDefinition(col)
columnDefs = append(columnDefs, " "+def)
}
fmt.Fprintf(w.writer, "%s\n", strings.Join(columnDefs, ",\n"))
fmt.Fprintf(w.writer, ");\n\n")
return nil
}
// generateColumnDefinition generates MSSQL column definition
func (w *Writer) generateColumnDefinition(col *models.Column) string {
parts := []string{fmt.Sprintf("[%s]", col.Name)}
// Type with length/precision
baseType := mssql.ConvertCanonicalToMSSQL(col.Type)
typeStr := baseType
// Handle specific type parameters for MSSQL
if col.Length > 0 && col.Precision == 0 {
// String types with length - override the default length from baseType
if strings.HasPrefix(baseType, "NVARCHAR") || strings.HasPrefix(baseType, "VARCHAR") ||
strings.HasPrefix(baseType, "CHAR") || strings.HasPrefix(baseType, "NCHAR") {
if col.Length > 0 && col.Length < 8000 {
// Extract base type without length specification
baseName := strings.Split(baseType, "(")[0]
typeStr = fmt.Sprintf("%s(%d)", baseName, col.Length)
}
}
} else if col.Precision > 0 {
// Numeric types with precision/scale
baseName := strings.Split(baseType, "(")[0]
if col.Scale > 0 {
typeStr = fmt.Sprintf("%s(%d,%d)", baseName, col.Precision, col.Scale)
} else {
typeStr = fmt.Sprintf("%s(%d)", baseName, col.Precision)
}
}
parts = append(parts, typeStr)
// IDENTITY for auto-increment
if col.AutoIncrement {
parts = append(parts, "IDENTITY(1,1)")
}
// NOT NULL
if col.NotNull {
parts = append(parts, "NOT NULL")
}
// DEFAULT
if col.Default != nil {
switch v := col.Default.(type) {
case string:
cleanDefault := stripBackticks(v)
if strings.HasPrefix(strings.ToUpper(cleanDefault), "GETDATE") ||
strings.HasPrefix(strings.ToUpper(cleanDefault), "CURRENT_") {
parts = append(parts, fmt.Sprintf("DEFAULT %s", cleanDefault))
} else if cleanDefault == "true" || cleanDefault == "false" {
if cleanDefault == "true" {
parts = append(parts, "DEFAULT 1")
} else {
parts = append(parts, "DEFAULT 0")
}
} else {
parts = append(parts, fmt.Sprintf("DEFAULT '%s'", escapeQuote(cleanDefault)))
}
case bool:
if v {
parts = append(parts, "DEFAULT 1")
} else {
parts = append(parts, "DEFAULT 0")
}
case int, int64:
parts = append(parts, fmt.Sprintf("DEFAULT %v", v))
}
}
return strings.Join(parts, " ")
}
// writePrimaryKey generates ALTER TABLE statement for primary key
func (w *Writer) writePrimaryKey(schema *models.Schema, table *models.Table) error {
// Find primary key constraint
var pkConstraint *models.Constraint
for _, constraint := range table.Constraints {
if constraint.Type == models.PrimaryKeyConstraint {
pkConstraint = constraint
break
}
}
var columnNames []string
pkName := fmt.Sprintf("PK_%s_%s", schema.Name, table.Name)
if pkConstraint != nil {
pkName = pkConstraint.Name
columnNames = make([]string, 0, len(pkConstraint.Columns))
for _, colName := range pkConstraint.Columns {
columnNames = append(columnNames, fmt.Sprintf("[%s]", colName))
}
} else {
// Check for columns with IsPrimaryKey = true
for _, col := range table.Columns {
if col.IsPrimaryKey {
columnNames = append(columnNames, fmt.Sprintf("[%s]", col.Name))
}
}
sort.Strings(columnNames)
}
if len(columnNames) == 0 {
return nil
}
fmt.Fprintf(w.writer, "ALTER TABLE %s ADD CONSTRAINT [%s] PRIMARY KEY (%s);\n\n",
w.qualTable(schema.Name, table.Name), pkName, strings.Join(columnNames, ", "))
return nil
}
// writeIndexes generates CREATE INDEX statements
func (w *Writer) writeIndexes(schema *models.Schema, table *models.Table) error {
// Sort indexes by name
indexNames := make([]string, 0, len(table.Indexes))
for name := range table.Indexes {
indexNames = append(indexNames, name)
}
sort.Strings(indexNames)
for _, name := range indexNames {
index := table.Indexes[name]
// Skip if it's a primary key index
if strings.HasPrefix(strings.ToLower(index.Name), "pk_") {
continue
}
// Build column list
columnExprs := make([]string, 0, len(index.Columns))
for _, colName := range index.Columns {
columnExprs = append(columnExprs, fmt.Sprintf("[%s]", colName))
}
if len(columnExprs) == 0 {
continue
}
unique := ""
if index.Unique {
unique = "UNIQUE "
}
fmt.Fprintf(w.writer, "CREATE %sINDEX [%s] ON %s (%s);\n\n",
unique, index.Name, w.qualTable(schema.Name, table.Name), strings.Join(columnExprs, ", "))
}
return nil
}
// writeUniqueConstraints generates ALTER TABLE statements for unique constraints
func (w *Writer) writeUniqueConstraints(schema *models.Schema, table *models.Table) error {
// Sort constraints by name
constraintNames := make([]string, 0)
for name, constraint := range table.Constraints {
if constraint.Type == models.UniqueConstraint {
constraintNames = append(constraintNames, name)
}
}
sort.Strings(constraintNames)
for _, name := range constraintNames {
constraint := table.Constraints[name]
// Build column list
columnExprs := make([]string, 0, len(constraint.Columns))
for _, colName := range constraint.Columns {
columnExprs = append(columnExprs, fmt.Sprintf("[%s]", colName))
}
if len(columnExprs) == 0 {
continue
}
fmt.Fprintf(w.writer, "ALTER TABLE %s ADD CONSTRAINT [%s] UNIQUE (%s);\n\n",
w.qualTable(schema.Name, table.Name), constraint.Name, strings.Join(columnExprs, ", "))
}
return nil
}
// writeCheckConstraints generates ALTER TABLE statements for check constraints
func (w *Writer) writeCheckConstraints(schema *models.Schema, table *models.Table) error {
// Sort constraints by name
constraintNames := make([]string, 0)
for name, constraint := range table.Constraints {
if constraint.Type == models.CheckConstraint {
constraintNames = append(constraintNames, name)
}
}
sort.Strings(constraintNames)
for _, name := range constraintNames {
constraint := table.Constraints[name]
if constraint.Expression == "" {
continue
}
fmt.Fprintf(w.writer, "ALTER TABLE %s ADD CONSTRAINT [%s] CHECK (%s);\n\n",
w.qualTable(schema.Name, table.Name), constraint.Name, constraint.Expression)
}
return nil
}
// writeForeignKeys generates ALTER TABLE statements for foreign keys
func (w *Writer) writeForeignKeys(schema *models.Schema, table *models.Table) error {
// Process foreign key constraints
constraintNames := make([]string, 0)
for name, constraint := range table.Constraints {
if constraint.Type == models.ForeignKeyConstraint {
constraintNames = append(constraintNames, name)
}
}
sort.Strings(constraintNames)
for _, name := range constraintNames {
constraint := table.Constraints[name]
// Build column lists
sourceColumns := make([]string, 0, len(constraint.Columns))
for _, colName := range constraint.Columns {
sourceColumns = append(sourceColumns, fmt.Sprintf("[%s]", colName))
}
targetColumns := make([]string, 0, len(constraint.ReferencedColumns))
for _, colName := range constraint.ReferencedColumns {
targetColumns = append(targetColumns, fmt.Sprintf("[%s]", colName))
}
if len(sourceColumns) == 0 || len(targetColumns) == 0 {
continue
}
refSchema := constraint.ReferencedSchema
if refSchema == "" {
refSchema = schema.Name
}
onDelete := "NO ACTION"
if constraint.OnDelete != "" {
onDelete = strings.ToUpper(constraint.OnDelete)
}
onUpdate := "NO ACTION"
if constraint.OnUpdate != "" {
onUpdate = strings.ToUpper(constraint.OnUpdate)
}
fmt.Fprintf(w.writer, "ALTER TABLE %s ADD CONSTRAINT [%s] FOREIGN KEY (%s)\n",
w.qualTable(schema.Name, table.Name), constraint.Name, strings.Join(sourceColumns, ", "))
fmt.Fprintf(w.writer, " REFERENCES %s (%s)\n",
w.qualTable(refSchema, constraint.ReferencedTable), strings.Join(targetColumns, ", "))
fmt.Fprintf(w.writer, " ON DELETE %s ON UPDATE %s;\n\n",
onDelete, onUpdate)
}
return nil
}
// writeComments generates EXEC sp_addextendedproperty statements for table and column descriptions
func (w *Writer) writeComments(schema *models.Schema, table *models.Table) error {
// Table comment
if table.Description != "" {
fmt.Fprintf(w.writer, "EXEC sp_addextendedproperty\n")
fmt.Fprintf(w.writer, " @name = 'MS_Description',\n")
fmt.Fprintf(w.writer, " @value = '%s',\n", escapeQuote(table.Description))
fmt.Fprintf(w.writer, " @level0type = 'SCHEMA', @level0name = '%s',\n", schema.Name)
fmt.Fprintf(w.writer, " @level1type = 'TABLE', @level1name = '%s';\n\n", table.Name)
}
// Column comments
for _, col := range getSortedColumns(table.Columns) {
if col.Description != "" {
fmt.Fprintf(w.writer, "EXEC sp_addextendedproperty\n")
fmt.Fprintf(w.writer, " @name = 'MS_Description',\n")
fmt.Fprintf(w.writer, " @value = '%s',\n", escapeQuote(col.Description))
fmt.Fprintf(w.writer, " @level0type = 'SCHEMA', @level0name = '%s',\n", schema.Name)
fmt.Fprintf(w.writer, " @level1type = 'TABLE', @level1name = '%s',\n", table.Name)
fmt.Fprintf(w.writer, " @level2type = 'COLUMN', @level2name = '%s';\n\n", col.Name)
}
}
return nil
}
// executeDatabaseSQL executes SQL statements directly on an MSSQL database
func (w *Writer) executeDatabaseSQL(db *models.Database, connString string) error {
// Generate SQL statements
statements := []string{}
statements = append(statements, "-- MSSQL Database Schema")
statements = append(statements, fmt.Sprintf("-- Database: %s", db.Name))
statements = append(statements, "-- Generated by RelSpec")
for _, schema := range db.Schemas {
if err := w.generateSchemaStatements(schema, &statements); err != nil {
return fmt.Errorf("failed to generate statements for schema %s: %w", schema.Name, err)
}
}
// Connect to database
dbConn, err := sql.Open("mssql", connString)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer dbConn.Close()
ctx := context.Background()
if err = dbConn.PingContext(ctx); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
// Execute statements
executedCount := 0
for i, stmt := range statements {
stmtTrimmed := strings.TrimSpace(stmt)
// Skip comments and empty statements
if strings.HasPrefix(stmtTrimmed, "--") || stmtTrimmed == "" {
continue
}
fmt.Fprintf(os.Stderr, "Executing statement %d/%d...\n", i+1, len(statements))
_, execErr := dbConn.ExecContext(ctx, stmt)
if execErr != nil {
fmt.Fprintf(os.Stderr, "⚠ Warning: Statement failed: %v\n", execErr)
continue
}
executedCount++
}
fmt.Fprintf(os.Stderr, "✓ Successfully executed %d statements\n", executedCount)
return nil
}
// generateSchemaStatements generates SQL statements for a schema
func (w *Writer) generateSchemaStatements(schema *models.Schema, statements *[]string) error {
// Phase 1: Create schema
if schema.Name != "dbo" && !w.options.FlattenSchema {
*statements = append(*statements, fmt.Sprintf("-- Schema: %s", schema.Name))
*statements = append(*statements, fmt.Sprintf("CREATE SCHEMA [%s];", schema.Name))
}
// Phase 2: Create tables
*statements = append(*statements, fmt.Sprintf("-- Tables for schema: %s", schema.Name))
for _, table := range schema.Tables {
createTableSQL := fmt.Sprintf("CREATE TABLE %s (", w.qualTable(schema.Name, table.Name))
columnDefs := make([]string, 0)
columns := getSortedColumns(table.Columns)
for _, col := range columns {
def := w.generateColumnDefinition(col)
columnDefs = append(columnDefs, " "+def)
}
createTableSQL += "\n" + strings.Join(columnDefs, ",\n") + "\n)"
*statements = append(*statements, createTableSQL)
}
// Phase 3-7: Constraints and indexes will be added by WriteSchema logic
// For now, just create tables
return nil
}
// Helper functions
// getSortedColumns returns columns sorted by sequence
func getSortedColumns(columns map[string]*models.Column) []*models.Column {
names := make([]string, 0, len(columns))
for name := range columns {
names = append(names, name)
}
sort.Strings(names)
sorted := make([]*models.Column, 0, len(columns))
for _, name := range names {
sorted = append(sorted, columns[name])
}
return sorted
}
// escapeQuote escapes single quotes in strings for SQL
func escapeQuote(s string) string {
return strings.ReplaceAll(s, "'", "''")
}
// stripBackticks removes backticks from SQL expressions
func stripBackticks(s string) string {
return strings.ReplaceAll(s, "`", "")
}

View File

@@ -0,0 +1,205 @@
package mssql
import (
"bytes"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
"github.com/stretchr/testify/assert"
)
// TestGenerateColumnDefinition tests column definition generation
func TestGenerateColumnDefinition(t *testing.T) {
writer := NewWriter(&writers.WriterOptions{})
tests := []struct {
name string
column *models.Column
expected string
}{
{
name: "INT NOT NULL",
column: &models.Column{
Name: "id",
Type: "int",
NotNull: true,
Sequence: 1,
},
expected: "[id] INT NOT NULL",
},
{
name: "VARCHAR with length",
column: &models.Column{
Name: "name",
Type: "string",
Length: 100,
NotNull: true,
Sequence: 2,
},
expected: "[name] NVARCHAR(100) NOT NULL",
},
{
name: "DATETIME2 with default",
column: &models.Column{
Name: "created_at",
Type: "timestamp",
NotNull: true,
Default: "GETDATE()",
Sequence: 3,
},
expected: "[created_at] DATETIME2 NOT NULL DEFAULT GETDATE()",
},
{
name: "IDENTITY column",
column: &models.Column{
Name: "id",
Type: "int",
AutoIncrement: true,
NotNull: true,
Sequence: 1,
},
expected: "[id] INT IDENTITY(1,1) NOT NULL",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := writer.generateColumnDefinition(tt.column)
assert.Equal(t, tt.expected, result)
})
}
}
// TestWriteCreateTable tests CREATE TABLE statement generation
func TestWriteCreateTable(t *testing.T) {
writer := NewWriter(&writers.WriterOptions{})
// Create a test schema with a table
schema := models.InitSchema("dbo")
table := models.InitTable("users", "dbo")
col1 := models.InitColumn("id", "users", "dbo")
col1.Type = "int"
col1.AutoIncrement = true
col1.NotNull = true
col1.Sequence = 1
col2 := models.InitColumn("email", "users", "dbo")
col2.Type = "string"
col2.Length = 255
col2.NotNull = true
col2.Sequence = 2
table.Columns["id"] = col1
table.Columns["email"] = col2
// Write to buffer
buf := &bytes.Buffer{}
writer.writer = buf
err := writer.writeCreateTable(schema, table)
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "CREATE TABLE [dbo].[users]")
assert.Contains(t, output, "[id] INT IDENTITY(1,1) NOT NULL")
assert.Contains(t, output, "[email] NVARCHAR(255) NOT NULL")
}
// TestWritePrimaryKey tests PRIMARY KEY constraint generation
func TestWritePrimaryKey(t *testing.T) {
writer := NewWriter(&writers.WriterOptions{})
schema := models.InitSchema("dbo")
table := models.InitTable("users", "dbo")
// Add primary key constraint
pk := models.InitConstraint("PK_users_id", models.PrimaryKeyConstraint)
pk.Columns = []string{"id"}
table.Constraints[pk.Name] = pk
// Add column
col := models.InitColumn("id", "users", "dbo")
col.Type = "int"
col.Sequence = 1
table.Columns["id"] = col
// Write to buffer
buf := &bytes.Buffer{}
writer.writer = buf
err := writer.writePrimaryKey(schema, table)
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "ALTER TABLE [dbo].[users]")
assert.Contains(t, output, "PRIMARY KEY")
assert.Contains(t, output, "[id]")
}
// TestWriteForeignKey tests FOREIGN KEY constraint generation
func TestWriteForeignKey(t *testing.T) {
writer := NewWriter(&writers.WriterOptions{})
schema := models.InitSchema("dbo")
table := models.InitTable("orders", "dbo")
// Add foreign key constraint
fk := models.InitConstraint("FK_orders_users", models.ForeignKeyConstraint)
fk.Columns = []string{"user_id"}
fk.ReferencedSchema = "dbo"
fk.ReferencedTable = "users"
fk.ReferencedColumns = []string{"id"}
fk.OnDelete = "CASCADE"
fk.OnUpdate = "NO ACTION"
table.Constraints[fk.Name] = fk
// Add column
col := models.InitColumn("user_id", "orders", "dbo")
col.Type = "int"
col.Sequence = 1
table.Columns["user_id"] = col
// Write to buffer
buf := &bytes.Buffer{}
writer.writer = buf
err := writer.writeForeignKeys(schema, table)
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "ALTER TABLE [dbo].[orders]")
assert.Contains(t, output, "FK_orders_users")
assert.Contains(t, output, "FOREIGN KEY")
assert.Contains(t, output, "CASCADE")
assert.Contains(t, output, "NO ACTION")
}
// TestWriteComments tests extended property generation for comments
func TestWriteComments(t *testing.T) {
writer := NewWriter(&writers.WriterOptions{})
schema := models.InitSchema("dbo")
table := models.InitTable("users", "dbo")
table.Description = "User accounts table"
col := models.InitColumn("id", "users", "dbo")
col.Type = "int"
col.Description = "Primary key"
col.Sequence = 1
table.Columns["id"] = col
// Write to buffer
buf := &bytes.Buffer{}
writer.writer = buf
err := writer.writeComments(schema, table)
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "sp_addextendedproperty")
assert.Contains(t, output, "MS_Description")
assert.Contains(t, output, "User accounts table")
assert.Contains(t, output, "Primary key")
}