package drawdb import ( "encoding/json" "fmt" "os" "strconv" "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" "git.warky.dev/wdevs/relspecgo/pkg/writers/drawdb" ) // Reader implements the readers.Reader interface for DrawDB JSON format type Reader struct { options *readers.ReaderOptions } // NewReader creates a new DrawDB reader with the given options func NewReader(options *readers.ReaderOptions) *Reader { return &Reader{ options: options, } } // ReadDatabase reads and parses DrawDB JSON input, returning a Database model func (r *Reader) ReadDatabase() (*models.Database, error) { if r.options.FilePath == "" { return nil, fmt.Errorf("file path is required for DrawDB reader") } data, err := os.ReadFile(r.options.FilePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } var drawSchema drawdb.DrawDBSchema if err := json.Unmarshal(data, &drawSchema); err != nil { return nil, fmt.Errorf("failed to parse DrawDB JSON: %w", err) } return r.convertToDatabase(&drawSchema) } // ReadSchema reads and parses DrawDB JSON input, returning a Schema model func (r *Reader) ReadSchema() (*models.Schema, error) { if r.options.FilePath == "" { return nil, fmt.Errorf("file path is required for DrawDB reader") } data, err := os.ReadFile(r.options.FilePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } var drawSchema drawdb.DrawDBSchema if err := json.Unmarshal(data, &drawSchema); err != nil { return nil, fmt.Errorf("failed to parse DrawDB JSON: %w", err) } // Determine schema name from the first table, or use "public" as default schemaName := "public" if len(drawSchema.Tables) > 0 && drawSchema.Tables[0].Schema != "" { schemaName = drawSchema.Tables[0].Schema } return r.convertToSchema(&drawSchema, schemaName) } // ReadTable reads and parses DrawDB JSON input, returning a Table model func (r *Reader) ReadTable() (*models.Table, error) { if r.options.FilePath == "" { return nil, fmt.Errorf("file path is required for DrawDB reader") } data, err := os.ReadFile(r.options.FilePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } var drawSchema drawdb.DrawDBSchema if err := json.Unmarshal(data, &drawSchema); err != nil { return nil, fmt.Errorf("failed to parse DrawDB JSON: %w", err) } if len(drawSchema.Tables) == 0 { return nil, fmt.Errorf("no tables found in DrawDB JSON") } // Return the first table return r.convertToTable(drawSchema.Tables[0], &drawSchema) } // convertToDatabase converts a DrawDB schema to a Database model func (r *Reader) convertToDatabase(drawSchema *drawdb.DrawDBSchema) (*models.Database, error) { db := models.InitDatabase("database") if r.options.Metadata != nil { if name, ok := r.options.Metadata["name"].(string); ok { db.Name = name } } // Extract database info from notes for _, note := range drawSchema.Notes { if strings.HasPrefix(note.Content, "Database:") { parts := strings.SplitN(note.Content, "\n\n", 2) if len(parts) == 2 { db.Description = parts[1] } } } // Group tables by schema schemaMap := make(map[string]*models.Schema) for _, drawTable := range drawSchema.Tables { schemaName := drawTable.Schema if schemaName == "" { schemaName = "public" } schema, exists := schemaMap[schemaName] if !exists { schema = models.InitSchema(schemaName) schemaMap[schemaName] = schema } table, err := r.convertToTable(drawTable, drawSchema) if err != nil { return nil, fmt.Errorf("failed to convert table %s: %w", drawTable.Name, err) } schema.Tables = append(schema.Tables, table) } // Add schemas to database for _, schema := range schemaMap { db.Schemas = append(db.Schemas, schema) } return db, nil } // convertToSchema converts DrawDB tables to a Schema model func (r *Reader) convertToSchema(drawSchema *drawdb.DrawDBSchema, schemaName string) (*models.Schema, error) { schema := models.InitSchema(schemaName) for _, drawTable := range drawSchema.Tables { // Filter by schema if specified in the table if drawTable.Schema != "" && drawTable.Schema != schemaName { continue } table, err := r.convertToTable(drawTable, drawSchema) if err != nil { return nil, fmt.Errorf("failed to convert table %s: %w", drawTable.Name, err) } schema.Tables = append(schema.Tables, table) } return schema, nil } // convertToTable converts a DrawDB table to a Table model func (r *Reader) convertToTable(drawTable *drawdb.DrawDBTable, drawSchema *drawdb.DrawDBSchema) (*models.Table, error) { schemaName := drawTable.Schema if schemaName == "" { schemaName = "public" } table := models.InitTable(drawTable.Name, schemaName) table.Description = drawTable.Comment // Convert fields to columns for _, field := range drawTable.Fields { column := r.convertToColumn(field, drawTable.Name, schemaName) table.Columns[column.Name] = column } // Convert indexes for _, index := range drawTable.Indexes { idx := r.convertToIndex(index, drawTable, schemaName) table.Indexes[idx.Name] = idx } // Find and convert relationships/constraints for this table for _, rel := range drawSchema.Relationships { if rel.StartTableID == drawTable.ID { constraint := r.convertToConstraint(rel, drawSchema) if constraint != nil { table.Constraints[constraint.Name] = constraint } } } return table, nil } // convertToColumn converts a DrawDB field to a Column model func (r *Reader) convertToColumn(field *drawdb.DrawDBField, tableName, schemaName string) *models.Column { column := models.InitColumn(field.Name, tableName, schemaName) // Parse type and dimensions typeStr := field.Type column.Type = typeStr // Try to extract length/precision from type string like "varchar(255)" or "decimal(10,2)" if strings.Contains(typeStr, "(") { parts := strings.Split(typeStr, "(") column.Type = parts[0] if len(parts) > 1 { dimensions := strings.TrimSuffix(parts[1], ")") if strings.Contains(dimensions, ",") { // Precision and scale (e.g., decimal(10,2)) dims := strings.Split(dimensions, ",") if precision, err := strconv.Atoi(strings.TrimSpace(dims[0])); err == nil { column.Precision = precision } if len(dims) > 1 { if scale, err := strconv.Atoi(strings.TrimSpace(dims[1])); err == nil { column.Scale = scale } } } else { // Just length (e.g., varchar(255)) if length, err := strconv.Atoi(dimensions); err == nil { column.Length = length } } } } column.IsPrimaryKey = field.Primary column.NotNull = field.NotNull || field.Primary column.AutoIncrement = field.Increment column.Comment = field.Comment if field.Default != "" { column.Default = field.Default } return column } // convertToIndex converts a DrawDB index to an Index model func (r *Reader) convertToIndex(drawIndex *drawdb.DrawDBIndex, drawTable *drawdb.DrawDBTable, schemaName string) *models.Index { index := models.InitIndex(drawIndex.Name) index.Table = drawTable.Name index.Schema = schemaName index.Unique = drawIndex.Unique // Convert field IDs to column names for _, fieldID := range drawIndex.Fields { if fieldID >= 0 && fieldID < len(drawTable.Fields) { index.Columns = append(index.Columns, drawTable.Fields[fieldID].Name) } } return index } // convertToConstraint converts a DrawDB relationship to a Constraint model func (r *Reader) convertToConstraint(rel *drawdb.DrawDBRelationship, drawSchema *drawdb.DrawDBSchema) *models.Constraint { // Find the start and end tables var startTable, endTable *drawdb.DrawDBTable for _, table := range drawSchema.Tables { if table.ID == rel.StartTableID { startTable = table } if table.ID == rel.EndTableID { endTable = table } } if startTable == nil || endTable == nil { return nil } constraint := models.InitConstraint(rel.Name, models.ForeignKeyConstraint) // Get the column names from field IDs if rel.StartFieldID >= 0 && rel.StartFieldID < len(startTable.Fields) { constraint.Columns = append(constraint.Columns, startTable.Fields[rel.StartFieldID].Name) } if rel.EndFieldID >= 0 && rel.EndFieldID < len(endTable.Fields) { constraint.ReferencedColumns = append(constraint.ReferencedColumns, endTable.Fields[rel.EndFieldID].Name) } constraint.Table = startTable.Name constraint.Schema = startTable.Schema if constraint.Schema == "" { constraint.Schema = "public" } constraint.ReferencedTable = endTable.Name constraint.ReferencedSchema = endTable.Schema if constraint.ReferencedSchema == "" { constraint.ReferencedSchema = "public" } constraint.OnUpdate = rel.UpdateConstraint constraint.OnDelete = rel.DeleteConstraint return constraint }