More Roundtrip tests
Some checks are pending
CI / Test (1.23) (push) Waiting to run
CI / Test (1.24) (push) Waiting to run
CI / Test (1.25) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build (push) Waiting to run

This commit is contained in:
2025-12-17 22:52:24 +02:00
parent 5e1448dcdb
commit a427aa5537
23 changed files with 22897 additions and 1319 deletions

View File

@@ -33,7 +33,7 @@ func (r *Reader) ReadDatabase() (*models.Database, error) {
return nil, fmt.Errorf("failed to read file: %w", err)
}
var dctx DCTXDictionary
var dctx models.DCTXDictionary
if err := xml.Unmarshal(data, &dctx); err != nil {
return nil, fmt.Errorf("failed to parse DCTX XML: %w", err)
}
@@ -70,7 +70,7 @@ func (r *Reader) ReadTable() (*models.Table, error) {
}
// convertToDatabase converts a DCTX dictionary to a Database model
func (r *Reader) convertToDatabase(dctx *DCTXDictionary) (*models.Database, error) {
func (r *Reader) convertToDatabase(dctx *models.DCTXDictionary) (*models.Database, error) {
dbName := dctx.Name
if dbName == "" {
dbName = "database"
@@ -81,7 +81,7 @@ func (r *Reader) convertToDatabase(dctx *DCTXDictionary) (*models.Database, erro
// Create GUID mappings for tables and keys
tableGuidMap := make(map[string]string) // GUID -> table name
keyGuidMap := make(map[string]*DCTXKey) // GUID -> key definition
keyGuidMap := make(map[string]*models.DCTXKey) // GUID -> key definition
keyTableMap := make(map[string]string) // key GUID -> table name
fieldGuidMaps := make(map[string]map[string]string) // table name -> field GUID -> field name
@@ -135,7 +135,7 @@ func (r *Reader) convertToDatabase(dctx *DCTXDictionary) (*models.Database, erro
}
// hasSQLOption checks if a DCTX table has the SQL option set to "1"
func (r *Reader) hasSQLOption(dctxTable *DCTXTable) bool {
func (r *Reader) hasSQLOption(dctxTable *models.DCTXTable) bool {
for _, option := range dctxTable.Options {
if option.Property == "SQL" && option.PropertyValue == "1" {
return true
@@ -144,8 +144,21 @@ func (r *Reader) hasSQLOption(dctxTable *DCTXTable) bool {
return false
}
// collectFieldGuids recursively collects all field GUIDs from a field and its nested fields
func (r *Reader) collectFieldGuids(dctxField *models.DCTXField, guidMap map[string]string) {
// Store the current field's GUID if available
if dctxField.Guid != "" && dctxField.Name != "" {
guidMap[dctxField.Guid] = r.sanitizeName(dctxField.Name)
}
// Recursively process nested fields (for GROUP types)
for i := range dctxField.Fields {
r.collectFieldGuids(&dctxField.Fields[i], guidMap)
}
}
// convertTable converts a DCTX table to a Table model
func (r *Reader) convertTable(dctxTable *DCTXTable) (*models.Table, map[string]string, error) {
func (r *Reader) convertTable(dctxTable *models.DCTXTable) (*models.Table, map[string]string, error) {
tableName := r.sanitizeName(dctxTable.Name)
table := models.InitTable(tableName, "public")
table.Description = dctxTable.Description
@@ -154,10 +167,8 @@ func (r *Reader) convertTable(dctxTable *DCTXTable) (*models.Table, map[string]s
// Process fields
for _, dctxField := range dctxTable.Fields {
// Store GUID to name mapping
if dctxField.Guid != "" && dctxField.Name != "" {
fieldGuidMap[dctxField.Guid] = r.sanitizeName(dctxField.Name)
}
// Recursively collect all field GUIDs (including nested fields in GROUP types)
r.collectFieldGuids(&dctxField, fieldGuidMap)
columns, err := r.convertField(&dctxField, table.Name)
if err != nil {
@@ -174,7 +185,7 @@ func (r *Reader) convertTable(dctxTable *DCTXTable) (*models.Table, map[string]s
}
// convertField converts a DCTX field to Column(s)
func (r *Reader) convertField(dctxField *DCTXField, tableName string) ([]*models.Column, error) {
func (r *Reader) convertField(dctxField *models.DCTXField, tableName string) ([]*models.Column, error) {
var columns []*models.Column
// Handle GROUP fields (nested structures)
@@ -286,7 +297,7 @@ func (r *Reader) mapDataType(clarionType string, size int) (sqlType string, prec
}
// processKeys processes DCTX keys and converts them to indexes and primary keys
func (r *Reader) processKeys(dctxTable *DCTXTable, table *models.Table, fieldGuidMap map[string]string) error {
func (r *Reader) processKeys(dctxTable *models.DCTXTable, table *models.Table, fieldGuidMap map[string]string) error {
for _, dctxKey := range dctxTable.Keys {
err := r.convertKey(&dctxKey, table, fieldGuidMap)
if err != nil {
@@ -297,7 +308,7 @@ func (r *Reader) processKeys(dctxTable *DCTXTable, table *models.Table, fieldGui
}
// convertKey converts a DCTX key to appropriate constraint/index
func (r *Reader) convertKey(dctxKey *DCTXKey, table *models.Table, fieldGuidMap map[string]string) error {
func (r *Reader) convertKey(dctxKey *models.DCTXKey, table *models.Table, fieldGuidMap map[string]string) error {
var columns []string
// Extract column names from key components
@@ -349,7 +360,7 @@ func (r *Reader) convertKey(dctxKey *DCTXKey, table *models.Table, fieldGuidMap
}
// Handle regular index
index := models.InitIndex(r.sanitizeName(dctxKey.Name))
index := models.InitIndex(r.sanitizeName(dctxKey.Name), table.Name, table.Schema)
index.Table = table.Name
index.Schema = table.Schema
index.Columns = columns
@@ -361,7 +372,7 @@ func (r *Reader) convertKey(dctxKey *DCTXKey, table *models.Table, fieldGuidMap
}
// processRelations processes DCTX relations and creates foreign keys
func (r *Reader) processRelations(dctx *DCTXDictionary, schema *models.Schema, tableGuidMap map[string]string, keyGuidMap map[string]*DCTXKey, fieldGuidMaps map[string]map[string]string) error {
func (r *Reader) processRelations(dctx *models.DCTXDictionary, schema *models.Schema, tableGuidMap map[string]string, keyGuidMap map[string]*models.DCTXKey, fieldGuidMaps map[string]map[string]string) error {
for i := range dctx.Relations {
relation := &dctx.Relations[i]
// Get table names from GUIDs
@@ -390,19 +401,23 @@ func (r *Reader) processRelations(dctx *DCTXDictionary, schema *models.Schema, t
var fkColumns, pkColumns []string
// Try to use explicit field mappings
// NOTE: DCTX format has backwards naming - ForeignMapping contains primary table fields,
// and PrimaryMapping contains foreign table fields
if len(relation.ForeignMappings) > 0 && len(relation.PrimaryMappings) > 0 {
foreignFieldMap := fieldGuidMaps[foreignTableName]
primaryFieldMap := fieldGuidMaps[primaryTableName]
// ForeignMapping actually contains fields from the PRIMARY table
for _, mapping := range relation.ForeignMappings {
if fieldName, exists := foreignFieldMap[mapping.Field]; exists {
fkColumns = append(fkColumns, fieldName)
if fieldName, exists := primaryFieldMap[mapping.Field]; exists {
pkColumns = append(pkColumns, fieldName)
}
}
// PrimaryMapping actually contains fields from the FOREIGN table
for _, mapping := range relation.PrimaryMappings {
if fieldName, exists := primaryFieldMap[mapping.Field]; exists {
pkColumns = append(pkColumns, fieldName)
if fieldName, exists := foreignFieldMap[mapping.Field]; exists {
fkColumns = append(fkColumns, fieldName)
}
}
}

View File

@@ -445,3 +445,51 @@ func TestColumnProperties(t *testing.T) {
t.Log("Note: No columns with default values found (this may be valid for the test data)")
}
}
func TestRelationships(t *testing.T) {
opts := &readers.ReaderOptions{
FilePath: filepath.Join("..", "..", "..", "examples", "dctx", "example.dctx"),
}
reader := NewReader(opts)
db, err := reader.ReadDatabase()
if err != nil {
t.Fatalf("ReadDatabase() error = %v", err)
}
// Count total relationships across all tables
relationshipCount := 0
for _, schema := range db.Schemas {
for _, table := range schema.Tables {
relationshipCount += len(table.Relationships)
}
}
// The example.dctx file should have a significant number of relationships
// With the fix for nested field GUID mapping, we expect around 100+ relationships
if relationshipCount < 50 {
t.Errorf("Expected at least 50 relationships, got %d. This may indicate relationships are not being parsed correctly", relationshipCount)
}
t.Logf("Successfully parsed %d relationships", relationshipCount)
// Verify relationship properties
for _, schema := range db.Schemas {
for _, table := range schema.Tables {
for _, rel := range table.Relationships {
if rel.Name == "" {
t.Errorf("Relationship in table '%s' should have a name", table.Name)
}
if rel.FromTable == "" {
t.Errorf("Relationship '%s' should have a from table", rel.Name)
}
if rel.ToTable == "" {
t.Errorf("Relationship '%s' should have a to table", rel.Name)
}
if rel.ForeignKey == "" {
t.Errorf("Relationship '%s' should reference a foreign key", rel.Name)
}
}
}
}
}

View File

@@ -1,84 +0,0 @@
package dctx
import "encoding/xml"
// DCTXDictionary represents the root element of a DCTX file
type DCTXDictionary struct {
XMLName xml.Name `xml:"Dictionary"`
Name string `xml:"Name,attr"`
Version string `xml:"Version,attr"`
Tables []DCTXTable `xml:"Table"`
Relations []DCTXRelation `xml:"Relation"`
}
// DCTXTable represents a table definition in DCTX
type DCTXTable struct {
Guid string `xml:"Guid,attr"`
Name string `xml:"Name,attr"`
Prefix string `xml:"Prefix,attr"`
Driver string `xml:"Driver,attr"`
Owner string `xml:"Owner,attr"`
Path string `xml:"Path,attr"`
Description string `xml:"Description,attr"`
Fields []DCTXField `xml:"Field"`
Keys []DCTXKey `xml:"Key"`
Options []DCTXOption `xml:"Option"`
}
// DCTXField represents a field/column definition in DCTX
type DCTXField struct {
Guid string `xml:"Guid,attr"`
Name string `xml:"Name,attr"`
DataType string `xml:"DataType,attr"`
Size int `xml:"Size,attr"`
NoPopulate bool `xml:"NoPopulate,attr"`
Thread bool `xml:"Thread,attr"`
Fields []DCTXField `xml:"Field"` // For GROUP fields (nested structures)
Options []DCTXOption `xml:"Option"`
}
// DCTXKey represents an index or key definition in DCTX
type DCTXKey struct {
Guid string `xml:"Guid,attr"`
Name string `xml:"Name,attr"`
KeyType string `xml:"KeyType,attr"`
Primary bool `xml:"Primary,attr"`
Unique bool `xml:"Unique,attr"`
Order int `xml:"Order,attr"`
Description string `xml:"Description,attr"`
Components []DCTXComponent `xml:"Component"`
}
// DCTXComponent represents a component of a key (field reference)
type DCTXComponent struct {
Guid string `xml:"Guid,attr"`
FieldId string `xml:"FieldId,attr"`
Order int `xml:"Order,attr"`
Ascend bool `xml:"Ascend,attr"`
}
// DCTXOption represents a property option in DCTX
type DCTXOption struct {
Property string `xml:"Property,attr"`
PropertyType string `xml:"PropertyType,attr"`
PropertyValue string `xml:"PropertyValue,attr"`
}
// DCTXRelation represents a relationship/foreign key in DCTX
type DCTXRelation struct {
Guid string `xml:"Guid,attr"`
PrimaryTable string `xml:"PrimaryTable,attr"`
ForeignTable string `xml:"ForeignTable,attr"`
PrimaryKey string `xml:"PrimaryKey,attr"`
ForeignKey string `xml:"ForeignKey,attr"`
Delete string `xml:"Delete,attr"`
Update string `xml:"Update,attr"`
ForeignMappings []DCTXFieldMapping `xml:"ForeignMapping"`
PrimaryMappings []DCTXFieldMapping `xml:"PrimaryMapping"`
}
// DCTXFieldMapping represents a field mapping in a relation
type DCTXFieldMapping struct {
Guid string `xml:"Guid,attr"`
Field string `xml:"Field,attr"`
}