* Introduced GUID field to Database, Domain, DomainTable, Schema, Table, View, Sequence, Column, Index, Relationship, Constraint, Enum, and Script models. * Updated initialization functions to assign new GUIDs using uuid package. * Enhanced DCTX reader and writer to utilize GUIDs from models where available.
392 lines
10 KiB
Go
392 lines
10 KiB
Go
package dctx
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
)
|
|
|
|
// Writer implements the writers.Writer interface for DCTX format
|
|
type Writer struct {
|
|
options *writers.WriterOptions
|
|
fieldGuidMap map[string]string // key: "table.column", value: guid
|
|
keyGuidMap map[string]string // key: "table.index", value: guid
|
|
tableGuidMap map[string]string // key: "table", value: guid
|
|
}
|
|
|
|
// NewWriter creates a new DCTX writer with the given options
|
|
func NewWriter(options *writers.WriterOptions) *Writer {
|
|
return &Writer{
|
|
options: options,
|
|
fieldGuidMap: make(map[string]string),
|
|
keyGuidMap: make(map[string]string),
|
|
tableGuidMap: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// WriteDatabase is not implemented for DCTX
|
|
func (w *Writer) WriteDatabase(db *models.Database) error {
|
|
return fmt.Errorf("writing a full database is not supported for DCTX, please write a single schema")
|
|
}
|
|
|
|
// WriteSchema writes a schema to the writer in DCTX format
|
|
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
|
dctx := models.DCTXDictionary{
|
|
Name: schema.Name,
|
|
Version: "1",
|
|
Tables: make([]models.DCTXTable, len(schema.Tables)),
|
|
}
|
|
|
|
tableSlice := make([]*models.Table, 0, len(schema.Tables))
|
|
|
|
tableSlice = append(tableSlice, schema.Tables...)
|
|
|
|
// Pass 1: Create fields and populate fieldGuidMap
|
|
for i, table := range tableSlice {
|
|
dctx.Tables[i] = w.mapTableFields(table)
|
|
}
|
|
|
|
// Pass 2: Create keys and populate keyGuidMap
|
|
for i, table := range tableSlice {
|
|
dctx.Tables[i].Keys = w.mapTableKeys(table)
|
|
}
|
|
|
|
// Pass 3: Collect all relationships (from schema and tables)
|
|
var allRelations []*models.Relationship
|
|
|
|
// Add schema-level relations
|
|
allRelations = append(allRelations, schema.Relations...)
|
|
|
|
// Add table-level relationships
|
|
for _, table := range tableSlice {
|
|
for _, rel := range table.Relationships {
|
|
// Check if this relationship is already in the list (avoid duplicates)
|
|
isDuplicate := false
|
|
for _, existing := range allRelations {
|
|
if existing.Name == rel.Name &&
|
|
existing.FromTable == rel.FromTable &&
|
|
existing.ToTable == rel.ToTable {
|
|
isDuplicate = true
|
|
break
|
|
}
|
|
}
|
|
if !isDuplicate {
|
|
allRelations = append(allRelations, rel)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Map all relations to DCTX format
|
|
dctx.Relations = make([]models.DCTXRelation, len(allRelations))
|
|
for i, rel := range allRelations {
|
|
dctx.Relations[i] = w.mapRelation(rel, schema)
|
|
}
|
|
|
|
output, err := xml.MarshalIndent(dctx, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := os.Create(w.options.OutputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
if _, err := file.Write([]byte(xml.Header)); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = file.Write(output)
|
|
return err
|
|
}
|
|
|
|
// WriteTable writes a single table to the writer in DCTX format
|
|
func (w *Writer) WriteTable(table *models.Table) error {
|
|
dctxTable := w.mapTableFields(table)
|
|
dctxTable.Keys = w.mapTableKeys(table)
|
|
|
|
output, err := xml.MarshalIndent(dctxTable, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file, err := os.Create(w.options.OutputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
_, err = file.Write(output)
|
|
return err
|
|
}
|
|
|
|
func (w *Writer) mapTableFields(table *models.Table) models.DCTXTable {
|
|
// Generate prefix (first 3 chars, or full name if shorter)
|
|
prefix := table.Name
|
|
if len(table.Name) > 3 {
|
|
prefix = table.Name[:3]
|
|
}
|
|
|
|
// Use GUID from model if available, otherwise generate a new one
|
|
tableGuid := table.GUID
|
|
if tableGuid == "" {
|
|
tableGuid = w.newGUID()
|
|
}
|
|
w.tableGuidMap[table.Name] = tableGuid
|
|
|
|
dctxTable := models.DCTXTable{
|
|
Guid: tableGuid,
|
|
Name: table.Name,
|
|
Prefix: prefix,
|
|
Description: table.Comment,
|
|
Fields: make([]models.DCTXField, len(table.Columns)),
|
|
Options: []models.DCTXOption{
|
|
{
|
|
Property: "SQL",
|
|
PropertyType: "1",
|
|
PropertyValue: "1",
|
|
},
|
|
},
|
|
}
|
|
|
|
i := 0
|
|
for _, column := range table.Columns {
|
|
dctxTable.Fields[i] = w.mapField(column)
|
|
i++
|
|
}
|
|
|
|
return dctxTable
|
|
}
|
|
|
|
func (w *Writer) mapTableKeys(table *models.Table) []models.DCTXKey {
|
|
keys := make([]models.DCTXKey, len(table.Indexes))
|
|
i := 0
|
|
for _, index := range table.Indexes {
|
|
keys[i] = w.mapKey(index, table)
|
|
i++
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func (w *Writer) mapField(column *models.Column) models.DCTXField {
|
|
// Use GUID from model if available, otherwise generate a new one
|
|
guid := column.GUID
|
|
if guid == "" {
|
|
guid = w.newGUID()
|
|
}
|
|
fieldKey := fmt.Sprintf("%s.%s", column.Table, column.Name)
|
|
w.fieldGuidMap[fieldKey] = guid
|
|
|
|
return models.DCTXField{
|
|
Guid: guid,
|
|
Name: column.Name,
|
|
DataType: w.mapDataType(column.Type),
|
|
Size: column.Length,
|
|
}
|
|
}
|
|
|
|
func (w *Writer) mapDataType(dataType string) string {
|
|
switch dataType {
|
|
case "integer", "int", "int4", "serial":
|
|
return "LONG"
|
|
case "bigint", "int8", "bigserial":
|
|
return "DECIMAL"
|
|
case "smallint", "int2":
|
|
return "SHORT"
|
|
case "boolean", "bool":
|
|
return "BYTE"
|
|
case "text", "varchar", "char":
|
|
return "CSTRING"
|
|
case "date":
|
|
return "DATE"
|
|
case "time":
|
|
return "TIME"
|
|
case "timestamp", "timestamptz":
|
|
return "STRING"
|
|
case "decimal", "numeric":
|
|
return "DECIMAL"
|
|
default:
|
|
return "STRING"
|
|
}
|
|
}
|
|
|
|
func (w *Writer) mapKey(index *models.Index, table *models.Table) models.DCTXKey {
|
|
// Use GUID from model if available, otherwise generate a new one
|
|
guid := index.GUID
|
|
if guid == "" {
|
|
guid = w.newGUID()
|
|
}
|
|
keyKey := fmt.Sprintf("%s.%s", table.Name, index.Name)
|
|
w.keyGuidMap[keyKey] = guid
|
|
|
|
key := models.DCTXKey{
|
|
Guid: guid,
|
|
Name: index.Name,
|
|
Primary: strings.HasSuffix(index.Name, "_pkey"),
|
|
Unique: index.Unique,
|
|
Components: make([]models.DCTXComponent, len(index.Columns)),
|
|
Description: index.Comment,
|
|
}
|
|
|
|
for i, colName := range index.Columns {
|
|
fieldKey := fmt.Sprintf("%s.%s", table.Name, colName)
|
|
fieldID := w.fieldGuidMap[fieldKey]
|
|
key.Components[i] = models.DCTXComponent{
|
|
Guid: w.newGUID(),
|
|
FieldId: fieldID,
|
|
Order: i + 1,
|
|
Ascend: true,
|
|
}
|
|
}
|
|
|
|
return key
|
|
}
|
|
|
|
func (w *Writer) mapRelation(rel *models.Relationship, schema *models.Schema) models.DCTXRelation {
|
|
// Find the foreign key constraint from the 'from' table
|
|
var fromTable *models.Table
|
|
for _, t := range schema.Tables {
|
|
if t.Name == rel.FromTable {
|
|
fromTable = t
|
|
break
|
|
}
|
|
}
|
|
|
|
var constraint *models.Constraint
|
|
if fromTable != nil {
|
|
for _, c := range fromTable.Constraints {
|
|
if c.Name == rel.ForeignKey {
|
|
constraint = c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var foreignKeyGUID string
|
|
var fkColumns []string
|
|
if constraint != nil {
|
|
fkColumns = constraint.Columns
|
|
// In DCTX, a relation is often linked by a foreign key which is an index.
|
|
// We'll look for an index that matches the constraint columns.
|
|
for _, index := range fromTable.Indexes {
|
|
if strings.Join(index.Columns, ",") == strings.Join(constraint.Columns, ",") {
|
|
keyKey := fmt.Sprintf("%s.%s", fromTable.Name, index.Name)
|
|
foreignKeyGUID = w.keyGuidMap[keyKey]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the primary key of the 'to' table
|
|
var toTable *models.Table
|
|
for _, t := range schema.Tables {
|
|
if t.Name == rel.ToTable {
|
|
toTable = t
|
|
break
|
|
}
|
|
}
|
|
var primaryKeyGUID string
|
|
var pkColumns []string
|
|
|
|
// Use referenced columns from the constraint if available
|
|
if constraint != nil && len(constraint.ReferencedColumns) > 0 {
|
|
pkColumns = constraint.ReferencedColumns
|
|
}
|
|
|
|
if toTable != nil {
|
|
// Find the matching primary key index
|
|
for _, index := range toTable.Indexes {
|
|
// If we have referenced columns, try to match them
|
|
if len(pkColumns) > 0 {
|
|
if strings.Join(index.Columns, ",") == strings.Join(pkColumns, ",") {
|
|
keyKey := fmt.Sprintf("%s.%s", toTable.Name, index.Name)
|
|
primaryKeyGUID = w.keyGuidMap[keyKey]
|
|
break
|
|
}
|
|
} else if strings.HasSuffix(index.Name, "_pkey") {
|
|
// Fall back to finding primary key by naming convention
|
|
keyKey := fmt.Sprintf("%s.%s", toTable.Name, index.Name)
|
|
primaryKeyGUID = w.keyGuidMap[keyKey]
|
|
pkColumns = index.Columns
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create field mappings
|
|
// NOTE: DCTX has backwards naming - ForeignMapping contains PRIMARY table fields,
|
|
// and PrimaryMapping contains FOREIGN table fields
|
|
var foreignMappings []models.DCTXFieldMapping // Will contain primary table fields
|
|
var primaryMappings []models.DCTXFieldMapping // Will contain foreign table fields
|
|
|
|
// Map foreign key columns (from foreign table) to PrimaryMapping
|
|
for _, colName := range fkColumns {
|
|
fieldKey := fmt.Sprintf("%s.%s", rel.FromTable, colName)
|
|
if fieldGUID, exists := w.fieldGuidMap[fieldKey]; exists {
|
|
primaryMappings = append(primaryMappings, models.DCTXFieldMapping{
|
|
Guid: w.newGUID(),
|
|
Field: fieldGUID,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Map primary key columns (from primary table) to ForeignMapping
|
|
for _, colName := range pkColumns {
|
|
fieldKey := fmt.Sprintf("%s.%s", rel.ToTable, colName)
|
|
if fieldGUID, exists := w.fieldGuidMap[fieldKey]; exists {
|
|
foreignMappings = append(foreignMappings, models.DCTXFieldMapping{
|
|
Guid: w.newGUID(),
|
|
Field: fieldGUID,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Get OnDelete and OnUpdate actions from the constraint
|
|
onDelete := ""
|
|
onUpdate := ""
|
|
if constraint != nil {
|
|
onDelete = w.mapReferentialAction(constraint.OnDelete)
|
|
onUpdate = w.mapReferentialAction(constraint.OnUpdate)
|
|
}
|
|
|
|
return models.DCTXRelation{
|
|
Guid: rel.GUID, // Use GUID from relationship model
|
|
PrimaryTable: w.tableGuidMap[rel.ToTable], // GUID of the 'to' table (e.g., users)
|
|
ForeignTable: w.tableGuidMap[rel.FromTable], // GUID of the 'from' table (e.g., posts)
|
|
PrimaryKey: primaryKeyGUID,
|
|
ForeignKey: foreignKeyGUID,
|
|
Delete: onDelete,
|
|
Update: onUpdate,
|
|
ForeignMappings: foreignMappings,
|
|
PrimaryMappings: primaryMappings,
|
|
}
|
|
}
|
|
|
|
// mapReferentialAction maps SQL referential actions to DCTX format
|
|
func (w *Writer) mapReferentialAction(action string) string {
|
|
switch strings.ToUpper(action) {
|
|
case "RESTRICT":
|
|
return "RESTRICT_SERVER"
|
|
case "CASCADE":
|
|
return "CASCADE_SERVER"
|
|
case "SET NULL":
|
|
return "SET_NULL_SERVER"
|
|
case "SET DEFAULT":
|
|
return "SET_DEFAULT_SERVER"
|
|
case "NO ACTION":
|
|
return "NO_ACTION_SERVER"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (w *Writer) newGUID() string {
|
|
return "{" + uuid.New().String() + "}"
|
|
}
|