All checks were successful
- 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.
267 lines
6.9 KiB
Go
267 lines
6.9 KiB
Go
package mssql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
_ "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/readers"
|
|
)
|
|
|
|
// Reader implements the readers.Reader interface for MSSQL databases
|
|
type Reader struct {
|
|
options *readers.ReaderOptions
|
|
db *sql.DB
|
|
ctx context.Context
|
|
}
|
|
|
|
// NewReader creates a new MSSQL reader
|
|
func NewReader(options *readers.ReaderOptions) *Reader {
|
|
return &Reader{
|
|
options: options,
|
|
ctx: context.Background(),
|
|
}
|
|
}
|
|
|
|
// ReadDatabase reads the entire database schema from MSSQL
|
|
func (r *Reader) ReadDatabase() (*models.Database, error) {
|
|
// Validate connection string
|
|
if r.options.ConnectionString == "" {
|
|
return nil, fmt.Errorf("connection string is required")
|
|
}
|
|
|
|
// Connect to the database
|
|
if err := r.connect(); err != nil {
|
|
return nil, fmt.Errorf("failed to connect: %w", err)
|
|
}
|
|
defer r.close()
|
|
|
|
// Get database name
|
|
var dbName string
|
|
err := r.db.QueryRowContext(r.ctx, "SELECT DB_NAME()").Scan(&dbName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get database name: %w", err)
|
|
}
|
|
|
|
// Initialize database model
|
|
db := models.InitDatabase(dbName)
|
|
db.DatabaseType = models.MSSQLDatabaseType
|
|
db.SourceFormat = "mssql"
|
|
|
|
// Get MSSQL version
|
|
var version string
|
|
err = r.db.QueryRowContext(r.ctx, "SELECT @@VERSION").Scan(&version)
|
|
if err == nil {
|
|
db.DatabaseVersion = version
|
|
}
|
|
|
|
// Query all schemas
|
|
schemas, err := r.querySchemas()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query schemas: %w", err)
|
|
}
|
|
|
|
// Process each schema
|
|
for _, schema := range schemas {
|
|
// Query tables for this schema
|
|
tables, err := r.queryTables(schema.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query tables for schema %s: %w", schema.Name, err)
|
|
}
|
|
schema.Tables = tables
|
|
|
|
// Query columns for tables
|
|
columnsMap, err := r.queryColumns(schema.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query columns for schema %s: %w", schema.Name, err)
|
|
}
|
|
|
|
// Populate table columns
|
|
for _, table := range schema.Tables {
|
|
tableKey := schema.Name + "." + table.Name
|
|
if cols, exists := columnsMap[tableKey]; exists {
|
|
table.Columns = cols
|
|
}
|
|
}
|
|
|
|
// Query primary keys
|
|
primaryKeys, err := r.queryPrimaryKeys(schema.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query primary keys for schema %s: %w", schema.Name, err)
|
|
}
|
|
|
|
// Apply primary keys to tables
|
|
for _, table := range schema.Tables {
|
|
tableKey := schema.Name + "." + table.Name
|
|
if pk, exists := primaryKeys[tableKey]; exists {
|
|
table.Constraints[pk.Name] = pk
|
|
// Mark columns as primary key and not null
|
|
for _, colName := range pk.Columns {
|
|
if col, colExists := table.Columns[colName]; colExists {
|
|
col.IsPrimaryKey = true
|
|
col.NotNull = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Query foreign keys
|
|
foreignKeys, err := r.queryForeignKeys(schema.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query foreign keys for schema %s: %w", schema.Name, err)
|
|
}
|
|
|
|
// Apply foreign keys to tables
|
|
for _, table := range schema.Tables {
|
|
tableKey := schema.Name + "." + table.Name
|
|
if fks, exists := foreignKeys[tableKey]; exists {
|
|
for _, fk := range fks {
|
|
table.Constraints[fk.Name] = fk
|
|
// Derive relationship from foreign key
|
|
r.deriveRelationship(table, fk)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Query unique constraints
|
|
uniqueConstraints, err := r.queryUniqueConstraints(schema.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query unique constraints for schema %s: %w", schema.Name, err)
|
|
}
|
|
|
|
// Apply unique constraints to tables
|
|
for _, table := range schema.Tables {
|
|
tableKey := schema.Name + "." + table.Name
|
|
if ucs, exists := uniqueConstraints[tableKey]; exists {
|
|
for _, uc := range ucs {
|
|
table.Constraints[uc.Name] = uc
|
|
}
|
|
}
|
|
}
|
|
|
|
// Query check constraints
|
|
checkConstraints, err := r.queryCheckConstraints(schema.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query check constraints for schema %s: %w", schema.Name, err)
|
|
}
|
|
|
|
// Apply check constraints to tables
|
|
for _, table := range schema.Tables {
|
|
tableKey := schema.Name + "." + table.Name
|
|
if ccs, exists := checkConstraints[tableKey]; exists {
|
|
for _, cc := range ccs {
|
|
table.Constraints[cc.Name] = cc
|
|
}
|
|
}
|
|
}
|
|
|
|
// Query indexes
|
|
indexes, err := r.queryIndexes(schema.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query indexes for schema %s: %w", schema.Name, err)
|
|
}
|
|
|
|
// Apply indexes to tables
|
|
for _, table := range schema.Tables {
|
|
tableKey := schema.Name + "." + table.Name
|
|
if idxs, exists := indexes[tableKey]; exists {
|
|
for _, idx := range idxs {
|
|
table.Indexes[idx.Name] = idx
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set RefDatabase for schema
|
|
schema.RefDatabase = db
|
|
|
|
// Set RefSchema for tables
|
|
for _, table := range schema.Tables {
|
|
table.RefSchema = schema
|
|
}
|
|
|
|
// Add schema to database
|
|
db.Schemas = append(db.Schemas, schema)
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// ReadSchema reads a single schema (returns the first schema from the database)
|
|
func (r *Reader) ReadSchema() (*models.Schema, error) {
|
|
db, err := r.ReadDatabase()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(db.Schemas) == 0 {
|
|
return nil, fmt.Errorf("no schemas found in database")
|
|
}
|
|
return db.Schemas[0], nil
|
|
}
|
|
|
|
// ReadTable reads a single table (returns the first table from the first schema)
|
|
func (r *Reader) ReadTable() (*models.Table, error) {
|
|
schema, err := r.ReadSchema()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(schema.Tables) == 0 {
|
|
return nil, fmt.Errorf("no tables found in schema")
|
|
}
|
|
return schema.Tables[0], nil
|
|
}
|
|
|
|
// connect establishes a connection to the MSSQL database
|
|
func (r *Reader) connect() error {
|
|
db, err := sql.Open("mssql", r.options.ConnectionString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Test connection
|
|
if err = db.PingContext(r.ctx); err != nil {
|
|
db.Close()
|
|
return err
|
|
}
|
|
|
|
r.db = db
|
|
return nil
|
|
}
|
|
|
|
// close closes the database connection
|
|
func (r *Reader) close() {
|
|
if r.db != nil {
|
|
r.db.Close()
|
|
}
|
|
}
|
|
|
|
// mapDataType maps MSSQL data types to canonical types
|
|
func (r *Reader) mapDataType(mssqlType string) string {
|
|
return mssql.ConvertMSSQLToCanonical(mssqlType)
|
|
}
|
|
|
|
// deriveRelationship creates a relationship from a foreign key constraint
|
|
func (r *Reader) deriveRelationship(table *models.Table, fk *models.Constraint) {
|
|
relationshipName := fmt.Sprintf("%s_to_%s", table.Name, fk.ReferencedTable)
|
|
|
|
relationship := models.InitRelationship(relationshipName, models.OneToMany)
|
|
relationship.FromTable = table.Name
|
|
relationship.FromSchema = table.Schema
|
|
relationship.ToTable = fk.ReferencedTable
|
|
relationship.ToSchema = fk.ReferencedSchema
|
|
relationship.ForeignKey = fk.Name
|
|
|
|
// Store constraint actions in properties
|
|
if fk.OnDelete != "" {
|
|
relationship.Properties["on_delete"] = fk.OnDelete
|
|
}
|
|
if fk.OnUpdate != "" {
|
|
relationship.Properties["on_update"] = fk.OnUpdate
|
|
}
|
|
|
|
table.Relationships[relationshipName] = relationship
|
|
}
|