More Roundtrip tests
This commit is contained in:
194
pkg/writers/dctx/roundtrip_test.go
Normal file
194
pkg/writers/dctx/roundtrip_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package dctx
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
dctxreader "git.warky.dev/wdevs/relspecgo/pkg/readers/dctx"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRoundTrip_WriteAndRead(t *testing.T) {
|
||||
// 1. Create a sample schema with relationships
|
||||
schema := models.InitSchema("public")
|
||||
schema.Name = "TestDB"
|
||||
|
||||
// Table 1: users
|
||||
usersTable := models.InitTable("users", "public")
|
||||
usersTable.Comment = "Stores user information"
|
||||
idCol := models.InitColumn("id", "users", "public")
|
||||
idCol.Type = "serial"
|
||||
idCol.IsPrimaryKey = true
|
||||
idCol.NotNull = true
|
||||
usersTable.Columns["id"] = idCol
|
||||
nameCol := models.InitColumn("name", "users", "public")
|
||||
nameCol.Type = "varchar"
|
||||
nameCol.Length = 100
|
||||
usersTable.Columns["name"] = nameCol
|
||||
pkIndex := models.InitIndex("users_pkey", "users", "public")
|
||||
pkIndex.Unique = true
|
||||
pkIndex.Columns = []string{"id"}
|
||||
usersTable.Indexes["users_pkey"] = pkIndex
|
||||
|
||||
pkConstraint := models.InitConstraint("users_pkey", models.PrimaryKeyConstraint)
|
||||
pkConstraint.Table = "users"
|
||||
pkConstraint.Schema = "public"
|
||||
pkConstraint.Columns = []string{"id"}
|
||||
usersTable.Constraints["users_pkey"] = pkConstraint
|
||||
|
||||
schema.Tables = append(schema.Tables, usersTable)
|
||||
|
||||
// Table 2: posts
|
||||
postsTable := models.InitTable("posts", "public")
|
||||
postsTable.Comment = "Stores blog posts"
|
||||
postIDCol := models.InitColumn("id", "posts", "public")
|
||||
postIDCol.Type = "serial"
|
||||
postIDCol.IsPrimaryKey = true
|
||||
postIDCol.NotNull = true
|
||||
postsTable.Columns["id"] = postIDCol
|
||||
titleCol := models.InitColumn("title", "posts", "public")
|
||||
titleCol.Type = "varchar"
|
||||
titleCol.Length = 255
|
||||
postsTable.Columns["title"] = titleCol
|
||||
userIDCol := models.InitColumn("user_id", "posts", "public")
|
||||
userIDCol.Type = "integer"
|
||||
postsTable.Columns["user_id"] = userIDCol
|
||||
postsPKIndex := models.InitIndex("posts_pkey", "posts", "public")
|
||||
postsPKIndex.Unique = true
|
||||
postsPKIndex.Columns = []string{"id"}
|
||||
postsTable.Indexes["posts_pkey"] = postsPKIndex
|
||||
|
||||
fkIndex := models.InitIndex("posts_user_id_idx", "posts", "public")
|
||||
fkIndex.Columns = []string{"user_id"}
|
||||
postsTable.Indexes["posts_user_id_idx"] = fkIndex
|
||||
|
||||
postsPKConstraint := models.InitConstraint("posts_pkey", models.PrimaryKeyConstraint)
|
||||
postsPKConstraint.Table = "posts"
|
||||
postsPKConstraint.Schema = "public"
|
||||
postsPKConstraint.Columns = []string{"id"}
|
||||
postsTable.Constraints["posts_pkey"] = postsPKConstraint
|
||||
|
||||
// Foreign key constraint
|
||||
fkConstraint := models.InitConstraint("fk_posts_users", models.ForeignKeyConstraint)
|
||||
fkConstraint.Table = "posts"
|
||||
fkConstraint.Schema = "public"
|
||||
fkConstraint.Columns = []string{"user_id"}
|
||||
fkConstraint.ReferencedTable = "users"
|
||||
fkConstraint.ReferencedSchema = "public"
|
||||
fkConstraint.ReferencedColumns = []string{"id"}
|
||||
fkConstraint.OnDelete = "CASCADE"
|
||||
fkConstraint.OnUpdate = "NO ACTION"
|
||||
postsTable.Constraints["fk_posts_users"] = fkConstraint
|
||||
|
||||
schema.Tables = append(schema.Tables, postsTable)
|
||||
|
||||
// Relation
|
||||
relation := models.InitRelationship("posts_to_users", models.OneToMany)
|
||||
relation.FromTable = "posts"
|
||||
relation.FromSchema = "public"
|
||||
relation.ToTable = "users"
|
||||
relation.ToSchema = "public"
|
||||
relation.ForeignKey = "fk_posts_users"
|
||||
schema.Relations = append(schema.Relations, relation)
|
||||
|
||||
// 2. Write the schema to DCTX
|
||||
outputPath := filepath.Join(t.TempDir(), "roundtrip.dctx")
|
||||
writerOpts := &writers.WriterOptions{
|
||||
OutputPath: outputPath,
|
||||
}
|
||||
writer := NewWriter(writerOpts)
|
||||
|
||||
err := writer.WriteSchema(schema)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file was created
|
||||
_, err = os.Stat(outputPath)
|
||||
assert.NoError(t, err, "Output file should exist")
|
||||
|
||||
// 3. Read the schema back from DCTX
|
||||
readerOpts := &readers.ReaderOptions{
|
||||
FilePath: outputPath,
|
||||
}
|
||||
reader := dctxreader.NewReader(readerOpts)
|
||||
|
||||
db, err := reader.ReadDatabase()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, db)
|
||||
|
||||
// 4. Verify the schema was read correctly
|
||||
assert.Len(t, db.Schemas, 1, "Should have one schema")
|
||||
readSchema := db.Schemas[0]
|
||||
|
||||
// Verify tables
|
||||
assert.Len(t, readSchema.Tables, 2, "Should have two tables")
|
||||
|
||||
// Find users and posts tables
|
||||
var readUsersTable, readPostsTable *models.Table
|
||||
for _, table := range readSchema.Tables {
|
||||
switch table.Name {
|
||||
case "users":
|
||||
readUsersTable = table
|
||||
case "posts":
|
||||
readPostsTable = table
|
||||
}
|
||||
}
|
||||
|
||||
assert.NotNil(t, readUsersTable, "Users table should exist")
|
||||
assert.NotNil(t, readPostsTable, "Posts table should exist")
|
||||
|
||||
// Verify columns
|
||||
assert.Len(t, readUsersTable.Columns, 2, "Users table should have 2 columns")
|
||||
assert.NotNil(t, readUsersTable.Columns["id"])
|
||||
assert.NotNil(t, readUsersTable.Columns["name"])
|
||||
|
||||
assert.Len(t, readPostsTable.Columns, 3, "Posts table should have 3 columns")
|
||||
assert.NotNil(t, readPostsTable.Columns["id"])
|
||||
assert.NotNil(t, readPostsTable.Columns["title"])
|
||||
assert.NotNil(t, readPostsTable.Columns["user_id"])
|
||||
|
||||
// Verify relationships were preserved
|
||||
// The DCTX reader stores relationships on the foreign table (posts)
|
||||
assert.NotEmpty(t, readPostsTable.Relationships, "Posts table should have relationships")
|
||||
|
||||
// Debug: print all relationships
|
||||
t.Logf("Posts table has %d relationships:", len(readPostsTable.Relationships))
|
||||
for name, rel := range readPostsTable.Relationships {
|
||||
t.Logf(" - %s: from=%s to=%s fk=%s", name, rel.FromTable, rel.ToTable, rel.ForeignKey)
|
||||
}
|
||||
|
||||
// Find the relationship - the reader creates it with FromTable as primary and ToTable as foreign
|
||||
var postsToUsersRel *models.Relationship
|
||||
for _, rel := range readPostsTable.Relationships {
|
||||
// The relationship should have posts as ToTable (foreign) and users as FromTable (primary)
|
||||
if rel.FromTable == "users" && rel.ToTable == "posts" {
|
||||
postsToUsersRel = rel
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.NotNil(t, postsToUsersRel, "Should have relationship from users to posts")
|
||||
if postsToUsersRel != nil {
|
||||
assert.Equal(t, "users", postsToUsersRel.FromTable, "Relationship should come from users (primary) table")
|
||||
assert.Equal(t, "posts", postsToUsersRel.ToTable, "Relationship should point to posts (foreign) table")
|
||||
assert.NotEmpty(t, postsToUsersRel.ForeignKey, "Relationship should have a foreign key")
|
||||
}
|
||||
|
||||
// Verify foreign key constraint
|
||||
fks := readPostsTable.GetForeignKeys()
|
||||
assert.NotEmpty(t, fks, "Posts table should have foreign keys")
|
||||
|
||||
if len(fks) > 0 {
|
||||
fk := fks[0]
|
||||
assert.Equal(t, models.ForeignKeyConstraint, fk.Type)
|
||||
assert.Contains(t, fk.Columns, "user_id")
|
||||
assert.Equal(t, "users", fk.ReferencedTable)
|
||||
assert.Contains(t, fk.ReferencedColumns, "id")
|
||||
assert.Equal(t, "CASCADE", fk.OnDelete)
|
||||
}
|
||||
|
||||
t.Logf("Round-trip test successful: wrote and read back %d tables with relationships", len(readSchema.Tables))
|
||||
}
|
||||
@@ -1,36 +1,379 @@
|
||||
package dctx
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Writer implements the writers.Writer interface for DCTX format
|
||||
// Note: DCTX is a read-only format used for loading Clarion dictionary files
|
||||
type Writer struct {
|
||||
options *writers.WriterOptions
|
||||
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,
|
||||
options: options,
|
||||
fieldGuidMap: make(map[string]string),
|
||||
keyGuidMap: make(map[string]string),
|
||||
tableGuidMap: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// WriteDatabase returns an error as DCTX format is read-only
|
||||
// WriteDatabase is not implemented for DCTX
|
||||
func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
return fmt.Errorf("DCTX format is read-only and does not support writing - it is used for loading Clarion dictionary files only")
|
||||
return fmt.Errorf("writing a full database is not supported for DCTX, please write a single schema")
|
||||
}
|
||||
|
||||
// WriteSchema returns an error as DCTX format is read-only
|
||||
// WriteSchema writes a schema to the writer in DCTX format
|
||||
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
||||
return fmt.Errorf("DCTX format is read-only and does not support writing - it is used for loading Clarion dictionary files only")
|
||||
dctx := models.DCTXDictionary{
|
||||
Name: schema.Name,
|
||||
Version: "1",
|
||||
Tables: make([]models.DCTXTable, len(schema.Tables)),
|
||||
}
|
||||
|
||||
tableSlice := make([]*models.Table, 0, len(schema.Tables))
|
||||
for _, t := range schema.Tables {
|
||||
tableSlice = append(tableSlice, t)
|
||||
}
|
||||
|
||||
// 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 returns an error as DCTX format is read-only
|
||||
// WriteTable writes a single table to the writer in DCTX format
|
||||
func (w *Writer) WriteTable(table *models.Table) error {
|
||||
return fmt.Errorf("DCTX format is read-only and does not support writing - it is used for loading Clarion dictionary files only")
|
||||
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]
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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: w.newGUID(),
|
||||
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() + "}"
|
||||
}
|
||||
|
||||
@@ -1,110 +1,152 @@
|
||||
package dctx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"encoding/xml"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestWriter_WriteDatabase_ReturnsError tests that WriteDatabase returns an error
|
||||
// since DCTX format is read-only
|
||||
func TestWriter_WriteDatabase_ReturnsError(t *testing.T) {
|
||||
db := models.InitDatabase("test_db")
|
||||
func TestWriter_WriteSchema(t *testing.T) {
|
||||
// 1. Create a sample schema
|
||||
schema := models.InitSchema("public")
|
||||
table := models.InitTable("users", "public")
|
||||
schema.Name = "TestDB"
|
||||
|
||||
// Table 1: users
|
||||
usersTable := models.InitTable("users", "public")
|
||||
usersTable.Comment = "Stores user information"
|
||||
idCol := models.InitColumn("id", "users", "public")
|
||||
idCol.Type = "bigint"
|
||||
table.Columns["id"] = idCol
|
||||
|
||||
schema.Tables = append(schema.Tables, table)
|
||||
db.Schemas = append(db.Schemas, schema)
|
||||
|
||||
opts := &writers.WriterOptions{
|
||||
OutputPath: "/tmp/test.dctx",
|
||||
}
|
||||
|
||||
writer := NewWriter(opts)
|
||||
err := writer.WriteDatabase(db)
|
||||
|
||||
if err == nil {
|
||||
t.Error("WriteDatabase() should return an error for read-only format")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "read-only") {
|
||||
t.Errorf("Error message should mention 'read-only', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriter_WriteSchema_ReturnsError tests that WriteSchema returns an error
|
||||
// since DCTX format is read-only
|
||||
func TestWriter_WriteSchema_ReturnsError(t *testing.T) {
|
||||
schema := models.InitSchema("public")
|
||||
table := models.InitTable("users", "public")
|
||||
|
||||
idCol := models.InitColumn("id", "users", "public")
|
||||
idCol.Type = "bigint"
|
||||
table.Columns["id"] = idCol
|
||||
|
||||
schema.Tables = append(schema.Tables, table)
|
||||
|
||||
opts := &writers.WriterOptions{
|
||||
OutputPath: "/tmp/test.dctx",
|
||||
}
|
||||
|
||||
writer := NewWriter(opts)
|
||||
err := writer.WriteSchema(schema)
|
||||
|
||||
if err == nil {
|
||||
t.Error("WriteSchema() should return an error for read-only format")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "read-only") {
|
||||
t.Errorf("Error message should mention 'read-only', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriter_WriteTable_ReturnsError tests that WriteTable returns an error
|
||||
// since DCTX format is read-only
|
||||
func TestWriter_WriteTable_ReturnsError(t *testing.T) {
|
||||
table := models.InitTable("users", "public")
|
||||
|
||||
idCol := models.InitColumn("id", "users", "public")
|
||||
idCol.Type = "bigint"
|
||||
idCol.Type = "serial"
|
||||
idCol.IsPrimaryKey = true
|
||||
table.Columns["id"] = idCol
|
||||
usersTable.Columns["id"] = idCol
|
||||
nameCol := models.InitColumn("name", "users", "public")
|
||||
nameCol.Type = "varchar"
|
||||
nameCol.Length = 100
|
||||
usersTable.Columns["name"] = nameCol
|
||||
pkIndex := models.InitIndex("users_pkey", "users", "public")
|
||||
pkIndex.Unique = true
|
||||
pkIndex.Columns = []string{"id"}
|
||||
usersTable.Indexes["users_pkey"] = pkIndex
|
||||
schema.Tables = append(schema.Tables, usersTable)
|
||||
|
||||
// Table 2: posts
|
||||
postsTable := models.InitTable("posts", "public")
|
||||
postsTable.Comment = "Stores blog posts"
|
||||
postIDCol := models.InitColumn("id", "posts", "public")
|
||||
postIDCol.Type = "serial"
|
||||
postIDCol.IsPrimaryKey = true
|
||||
postsTable.Columns["id"] = postIDCol
|
||||
titleCol := models.InitColumn("title", "posts", "public")
|
||||
titleCol.Type = "varchar"
|
||||
titleCol.Length = 255
|
||||
postsTable.Columns["title"] = titleCol
|
||||
userIDCol := models.InitColumn("user_id", "posts", "public")
|
||||
userIDCol.Type = "integer"
|
||||
postsTable.Columns["user_id"] = userIDCol
|
||||
postsPKIndex := models.InitIndex("posts_pkey", "posts", "public")
|
||||
postsPKIndex.Unique = true
|
||||
postsPKIndex.Columns = []string{"id"}
|
||||
postsTable.Indexes["posts_pkey"] = postsPKIndex
|
||||
|
||||
fkIndex := models.InitIndex("posts_user_id_idx", "posts", "public")
|
||||
fkIndex.Columns = []string{"user_id"}
|
||||
postsTable.Indexes["posts_user_id_idx"] = fkIndex
|
||||
schema.Tables = append(schema.Tables, postsTable)
|
||||
|
||||
// Constraint for the relationship
|
||||
fkConstraint := models.InitConstraint("fk_posts_users", models.ForeignKeyConstraint)
|
||||
fkConstraint.Table = "posts"
|
||||
fkConstraint.Schema = "public"
|
||||
fkConstraint.Columns = []string{"user_id"}
|
||||
fkConstraint.ReferencedTable = "users"
|
||||
fkConstraint.ReferencedSchema = "public"
|
||||
fkConstraint.ReferencedColumns = []string{"id"}
|
||||
postsTable.Constraints["fk_posts_users"] = fkConstraint
|
||||
|
||||
// Relation
|
||||
relation := models.InitRelation("fk_posts_users", "public")
|
||||
relation.FromTable = "posts"
|
||||
relation.ToTable = "users"
|
||||
relation.ForeignKey = "fk_posts_users"
|
||||
schema.Relations = append(schema.Relations, relation)
|
||||
|
||||
// 2. Setup writer
|
||||
outputPath := "/tmp/test.dctx"
|
||||
opts := &writers.WriterOptions{
|
||||
OutputPath: "/tmp/test.dctx",
|
||||
OutputPath: outputPath,
|
||||
}
|
||||
|
||||
writer := NewWriter(opts)
|
||||
err := writer.WriteTable(table)
|
||||
|
||||
if err == nil {
|
||||
t.Error("WriteTable() should return an error for read-only format")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "read-only") {
|
||||
t.Errorf("Error message should mention 'read-only', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewWriter tests that NewWriter creates a valid writer instance
|
||||
func TestNewWriter(t *testing.T) {
|
||||
opts := &writers.WriterOptions{
|
||||
OutputPath: "/tmp/test.dctx",
|
||||
}
|
||||
|
||||
writer := NewWriter(opts)
|
||||
|
||||
if writer == nil {
|
||||
t.Error("NewWriter() should return a non-nil writer")
|
||||
}
|
||||
// 3. Write the schema
|
||||
err := writer.WriteSchema(schema)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if writer.options != opts {
|
||||
t.Error("Writer options should match the provided options")
|
||||
}
|
||||
}
|
||||
// 4. Read the file and unmarshal it
|
||||
actualBytes, err := os.ReadFile(outputPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var dctx models.DCTXDictionary
|
||||
err = xml.Unmarshal(actualBytes, &dctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 5. Assert properties of the unmarshaled struct
|
||||
assert.Equal(t, "TestDB", dctx.Name)
|
||||
assert.Equal(t, "1", dctx.Version)
|
||||
assert.Len(t, dctx.Tables, 2)
|
||||
assert.Len(t, dctx.Relations, 1)
|
||||
|
||||
// Assert users table
|
||||
usersTableResult := dctx.Tables[0]
|
||||
assert.Equal(t, "users", usersTableResult.Name)
|
||||
assert.Len(t, usersTableResult.Fields, 2)
|
||||
assert.Len(t, usersTableResult.Keys, 1)
|
||||
userPK := usersTableResult.Keys[0]
|
||||
assert.True(t, userPK.Primary)
|
||||
assert.Equal(t, "users_pkey", userPK.Name)
|
||||
assert.Len(t, userPK.Components, 1)
|
||||
userPKComponent := userPK.Components[0]
|
||||
assert.NotEmpty(t, userPKComponent.FieldId)
|
||||
|
||||
// Assert posts table
|
||||
postsTableResult := dctx.Tables[1]
|
||||
assert.Equal(t, "posts", postsTableResult.Name)
|
||||
assert.Len(t, postsTableResult.Fields, 3)
|
||||
assert.Len(t, postsTableResult.Keys, 2)
|
||||
postsFK := postsTableResult.Keys[1] // Assuming order
|
||||
assert.False(t, postsFK.Primary)
|
||||
assert.Equal(t, "posts_user_id_idx", postsFK.Name)
|
||||
assert.Len(t, postsFK.Components, 1)
|
||||
postsFKComponent := postsFK.Components[0]
|
||||
assert.NotEmpty(t, postsFKComponent.FieldId)
|
||||
|
||||
// Assert relation
|
||||
relationResult := dctx.Relations[0]
|
||||
// PrimaryTable and ForeignTable should be GUIDs in DCTX format
|
||||
assert.NotEmpty(t, relationResult.PrimaryTable, "PrimaryTable should have a GUID")
|
||||
assert.NotEmpty(t, relationResult.ForeignTable, "ForeignTable should have a GUID")
|
||||
assert.NotEmpty(t, relationResult.PrimaryKey)
|
||||
assert.NotEmpty(t, relationResult.ForeignKey)
|
||||
|
||||
// Check if the table GUIDs match
|
||||
assert.Equal(t, usersTableResult.Guid, relationResult.PrimaryTable, "PrimaryTable GUID should match users table")
|
||||
assert.Equal(t, postsTableResult.Guid, relationResult.ForeignTable, "ForeignTable GUID should match posts table")
|
||||
|
||||
// Check if the key GUIDs match up
|
||||
assert.Equal(t, userPK.Guid, relationResult.PrimaryKey)
|
||||
assert.Equal(t, postsFK.Guid, relationResult.ForeignKey)
|
||||
|
||||
// Verify field mappings exist
|
||||
assert.NotEmpty(t, relationResult.ForeignMappings, "Relation should have ForeignMappings")
|
||||
assert.NotEmpty(t, relationResult.PrimaryMappings, "Relation should have PrimaryMappings")
|
||||
|
||||
// ForeignMapping should reference primary table (users) fields
|
||||
assert.Len(t, relationResult.ForeignMappings, 1)
|
||||
assert.NotEmpty(t, relationResult.ForeignMappings[0].Field)
|
||||
|
||||
// PrimaryMapping should reference foreign table (posts) fields
|
||||
assert.Len(t, relationResult.PrimaryMappings, 1)
|
||||
assert.NotEmpty(t, relationResult.PrimaryMappings[0].Field)
|
||||
}
|
||||
Reference in New Issue
Block a user