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 }