From 5d3c86119e37e179668a9707c550c0d6d69ef2a4 Mon Sep 17 00:00:00 2001 From: Hein Date: Sun, 4 Jan 2026 15:49:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(domains):=20=E2=9C=A8=20add=20domain=20sup?= =?UTF-8?q?port=20for=20DrawDB=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce Domain and DomainTable models for logical grouping of tables. - Implement export and import functionality for domains in DrawDB format. - Update template execution modes to include domain processing. - Enhance documentation for domain features and usage. --- docs/DOMAINS_DRAWDB.md | 149 ++++++++++++++++++++++++++ docs/TEMPLATE_MODE.md | 22 +++- pkg/models/models.go | 45 ++++++++ pkg/readers/drawdb/reader.go | 26 +++++ pkg/writers/drawdb/writer.go | 45 ++++++++ pkg/writers/template/template_data.go | 13 +++ pkg/writers/template/writer.go | 26 +++++ 7 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 docs/DOMAINS_DRAWDB.md diff --git a/docs/DOMAINS_DRAWDB.md b/docs/DOMAINS_DRAWDB.md new file mode 100644 index 0000000..f23cb51 --- /dev/null +++ b/docs/DOMAINS_DRAWDB.md @@ -0,0 +1,149 @@ +# Domains and DrawDB Areas Integration + +## Overview + +Domains provide a way to organize tables from potentially multiple schemas into logical business groupings. When working with DrawDB format, domains are automatically imported/exported as **Subject Areas** - a native DrawDB feature for visually grouping tables. + +## How It Works + +### Writing Domains to DrawDB (Export) + +When you export a database with domains to DrawDB format: + +1. **Schema Areas** are created automatically for each schema (existing behavior) +2. **Domain Areas** are created for each domain, calculated based on the positions of the tables they contain +3. The domain area bounds are automatically calculated to encompass all its tables with a small padding + +```go +// Example: Creating a domain and exporting to DrawDB +db := models.InitDatabase("mydb") + +// Create an "authentication" domain +authDomain := models.InitDomain("authentication") +authDomain.Tables = append(authDomain.Tables, + models.InitDomainTable("users", "public"), + models.InitDomainTable("roles", "public"), + models.InitDomainTable("permissions", "public"), +) +db.Domains = append(db.Domains, authDomain) + +// Create a "financial" domain spanning multiple schemas +finDomain := models.InitDomain("financial") +finDomain.Tables = append(finDomain.Tables, + models.InitDomainTable("accounts", "public"), + models.InitDomainTable("transactions", "public"), + models.InitDomainTable("ledger", "finance"), // Different schema! +) +db.Domains = append(db.Domains, finDomain) + +// Write to DrawDB - domains become subject areas +writer := drawdb.NewWriter(&writers.WriterOptions{ + OutputPath: "schema.json", +}) +writer.WriteDatabase(db) +``` + +The resulting DrawDB JSON will have Subject Areas for both: +- "authentication" area containing the auth tables +- "financial" area containing the financial tables from both schemas + +### Reading Domains from DrawDB (Import) + +When you import a DrawDB file with Subject Areas: + +1. **Subject Areas** are automatically converted to **Domains** +2. Tables are assigned to a domain if they fall within the area's visual bounds +3. Table references include both the table name and schema name + +```go +// Example: Reading DrawDB with areas +reader := drawdb.NewReader(&readers.ReaderOptions{ + FilePath: "schema.json", +}) + +db, err := reader.ReadDatabase() +if err != nil { + log.Fatal(err) +} + +// Access domains +for _, domain := range db.Domains { + fmt.Printf("Domain: %s\n", domain.Name) + for _, domainTable := range domain.Tables { + fmt.Printf(" - %s.%s\n", domainTable.SchemaName, domainTable.TableName) + + // Access the actual table reference if loaded + if domainTable.RefTable != nil { + fmt.Printf(" Description: %s\n", domainTable.RefTable.Description) + } + } +} +``` + +## Domain Structure + +```go +type Domain struct { + Name string // Domain name (e.g., "authentication", "user_data") + Description string // Optional human-readable description + Tables []*DomainTable // Tables belonging to this domain + Comment string // Optional comment + Metadata map[string]any // Extensible metadata + Sequence uint // Ordering hint +} + +type DomainTable struct { + TableName string // Table name + SchemaName string // Schema containing the table + Sequence uint // Ordering hint + RefTable *Table // Pointer to actual table (in-memory only, not serialized) +} +``` + +## Multi-Schema Domains + +One of the key features of domains is that they can span multiple schemas: + +``` +Domain: "user_data" +├── public.users +├── public.profiles +├── public.user_preferences +├── auth.user_sessions +└── auth.mfa_devices +``` + +This allows you to organize related tables even when they're stored in different schemas. + +## Visual Organization in DrawDB + +When viewing the exported DrawDB file in DrawDB Editor: + +1. **Schema areas** appear in one color (original behavior) +2. **Domain areas** appear in a different color +3. Domain area bounds are calculated to fit all contained tables +4. Areas can overlap - a table can visually belong to multiple areas + +## Integration with Other Formats + +Currently, domain/area integration is implemented for DrawDB format. + +To implement similar functionality for other formats: + +1. Identify if the format has a native grouping/area feature +2. Add conversion logic in the reader to map format areas → Domain model +3. Add conversion logic in the writer to map Domain model → format areas + +Example formats that could support domains: +- **DBML**: Could use DBML's `TableGroup` feature +- **DrawDB**: ✅ Already implemented (Subject Areas) +- **GraphQL**: Could use schema directives +- **Custom formats**: Implement as needed + +## Tips and Best Practices + +1. **Keep domains focused**: Each domain should represent a distinct business area +2. **Document purposes**: Use Description and Comment fields to explain each domain +3. **Use meaningful names**: Domain names should clearly reflect their purpose +4. **Maintain schema consistency**: Keep related tables together in the same schema when possible +5. **Use metadata**: Store tool-specific information in the Metadata field diff --git a/docs/TEMPLATE_MODE.md b/docs/TEMPLATE_MODE.md index 0d9b815..909f894 100644 --- a/docs/TEMPLATE_MODE.md +++ b/docs/TEMPLATE_MODE.md @@ -44,12 +44,13 @@ The `--mode` flag controls how the template is executed: |------|-------------|--------|-------------| | `database` | Execute once for entire database | Single file | Documentation, reports, overview files | | `schema` | Execute once per schema | One file per schema | Schema-specific documentation | +| `domain` | Execute once per domain | One file per domain | Domain-based documentation, domain exports | | `script` | Execute once per script | One file per script | Script processing | | `table` | Execute once per table | One file per table | Model generation, table docs | ### Filename Patterns -For multi-file modes (`schema`, `script`, `table`), use `--filename-pattern` to control output filenames: +For multi-file modes (`schema`, `domain`, `script`, `table`), use `--filename-pattern` to control output filenames: ```bash # Default pattern @@ -296,6 +297,13 @@ The data available in templates depends on the execution mode: .Metadata // map[string]interface{} - User metadata ``` +### Domain Mode +```go +.Domain // *models.Domain - Current domain +.ParentDatabase // *models.Database - Parent database context +.Metadata // map[string]interface{} - User metadata +``` + ### Table Mode ```go .Table // *models.Table - Current table @@ -317,6 +325,7 @@ The data available in templates depends on the execution mode: **Database:** - `.Name` - Database name - `.Schemas` - List of schemas +- `.Domains` - List of domains (business domain groupings) - `.Description`, `.Comment` - Documentation **Schema:** @@ -325,6 +334,17 @@ The data available in templates depends on the execution mode: - `.Views`, `.Sequences`, `.Scripts` - Other objects - `.Enums` - Enum types +**Domain:** +- `.Name` - Domain name +- `.Tables` - List of DomainTable references +- `.Description`, `.Comment` - Documentation +- `.Metadata` - Custom metadata map + +**DomainTable:** +- `.TableName` - Name of the table +- `.SchemaName` - Schema containing the table +- `.RefTable` - Pointer to actual Table object (if loaded) + **Table:** - `.Name` - Table name - `.Schema` - Schema name diff --git a/pkg/models/models.go b/pkg/models/models.go index 71037d5..9381d3b 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -21,6 +21,7 @@ type Database struct { Name string `json:"name" yaml:"name"` Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"` Schemas []*Schema `json:"schemas" yaml:"schemas" xml:"schemas"` + Domains []*Domain `json:"domains,omitempty" yaml:"domains,omitempty" xml:"domains,omitempty"` Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"` DatabaseType DatabaseType `json:"database_type,omitempty" yaml:"database_type,omitempty" xml:"database_type,omitempty"` DatabaseVersion string `json:"database_version,omitempty" yaml:"database_version,omitempty" xml:"database_version,omitempty"` @@ -32,6 +33,32 @@ func (d *Database) SQLName() string { return strings.ToLower(d.Name) } +// Domain represents a logical business domain grouping multiple tables from potentially different schemas. +// Domains allow for organizing database tables by functional areas (e.g., authentication, user data, financial). +type Domain struct { + Name string `json:"name" yaml:"name" xml:"name"` + Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"` + Tables []*DomainTable `json:"tables" yaml:"tables" xml:"tables"` + Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty" xml:"-"` + Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"` +} + +// SQLName returns the domain name in lowercase for SQL compatibility. +func (d *Domain) SQLName() string { + return strings.ToLower(d.Name) +} + +// DomainTable represents a reference to a specific table within a domain. +// It identifies the table by name and schema, allowing a single domain to include +// tables from multiple schemas. +type DomainTable struct { + TableName string `json:"table_name" yaml:"table_name" xml:"table_name"` + SchemaName string `json:"schema_name" yaml:"schema_name" xml:"schema_name"` + Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"` + RefTable *Table `json:"-" yaml:"-" xml:"-"` // Excluded to prevent circular references +} + // Schema represents a database schema, which is a logical grouping of database objects // such as tables, views, sequences, and relationships within a database. type Schema struct { @@ -295,6 +322,7 @@ func InitDatabase(name string) *Database { return &Database{ Name: name, Schemas: make([]*Schema, 0), + Domains: make([]*Domain, 0), } } @@ -402,3 +430,20 @@ func InitSequence(name, schema string) *Sequence { StartValue: 1, } } + +// InitDomain initializes a new Domain with empty slices and maps +func InitDomain(name string) *Domain { + return &Domain{ + Name: name, + Tables: make([]*DomainTable, 0), + Metadata: make(map[string]any), + } +} + +// InitDomainTable initializes a new DomainTable reference +func InitDomainTable(tableName, schemaName string) *DomainTable { + return &DomainTable{ + TableName: tableName, + SchemaName: schemaName, + } +} diff --git a/pkg/readers/drawdb/reader.go b/pkg/readers/drawdb/reader.go index bc66ec6..f42f2c8 100644 --- a/pkg/readers/drawdb/reader.go +++ b/pkg/readers/drawdb/reader.go @@ -140,6 +140,32 @@ func (r *Reader) convertToDatabase(drawSchema *drawdb.DrawDBSchema) (*models.Dat db.Schemas = append(db.Schemas, schema) } + // Convert DrawDB subject areas to domains + for _, area := range drawSchema.SubjectAreas { + domain := models.InitDomain(area.Name) + + // Find all tables that visually belong to this area + // A table belongs to an area if its position is within the area bounds + for _, drawTable := range drawSchema.Tables { + if drawTable.X >= area.X && drawTable.X <= (area.X+area.Width) && + drawTable.Y >= area.Y && drawTable.Y <= (area.Y+area.Height) { + + schemaName := drawTable.Schema + if schemaName == "" { + schemaName = "public" + } + + domainTable := models.InitDomainTable(drawTable.Name, schemaName) + domain.Tables = append(domain.Tables, domainTable) + } + } + + // Only add domain if it has tables + if len(domain.Tables) > 0 { + db.Domains = append(db.Domains, domain) + } + } + return db, nil } diff --git a/pkg/writers/drawdb/writer.go b/pkg/writers/drawdb/writer.go index 1a30f32..2e5bb87 100644 --- a/pkg/writers/drawdb/writer.go +++ b/pkg/writers/drawdb/writer.go @@ -127,6 +127,51 @@ func (w *Writer) databaseToDrawDB(d *models.Database) *DrawDBSchema { } } + // Create subject areas for domains + for domainIdx, domainModel := range d.Domains { + // Calculate bounds for all tables in this domain + minX, minY := 999999, 999999 + maxX, maxY := 0, 0 + + domainTableCount := 0 + for _, domainTable := range domainModel.Tables { + // Find the table in the schema to get its position + for _, t := range schema.Tables { + if t.Name == domainTable.TableName { + if t.X < minX { + minX = t.X + } + if t.Y < minY { + minY = t.Y + } + if t.X+colWidth > maxX { + maxX = t.X + colWidth + } + if t.Y+rowHeight > maxY { + maxY = t.Y + rowHeight + } + domainTableCount++ + break + } + } + } + + // Only create area if domain has tables in this schema + if domainTableCount > 0 { + area := &DrawDBArea{ + ID: areaID, + Name: domainModel.Name, + Color: getColorForIndex(len(d.Schemas) + domainIdx), // Use different colors than schemas + X: minX - 20, + Y: minY - 20, + Width: maxX - minX + 40, + Height: maxY - minY + 40, + } + schema.SubjectAreas = append(schema.SubjectAreas, area) + areaID++ + } + } + // Add relationships for _, schemaModel := range d.Schemas { for _, table := range schemaModel.Tables { diff --git a/pkg/writers/template/template_data.go b/pkg/writers/template/template_data.go index 17cf846..7c6f216 100644 --- a/pkg/writers/template/template_data.go +++ b/pkg/writers/template/template_data.go @@ -7,6 +7,7 @@ type TemplateData struct { // One of these will be populated based on execution mode Database *models.Database Schema *models.Schema + Domain *models.Domain Script *models.Script Table *models.Table @@ -57,6 +58,15 @@ func NewSchemaData(schema *models.Schema, metadata map[string]interface{}) *Temp } } +// NewDomainData creates template data for domain mode +func NewDomainData(domain *models.Domain, db *models.Database, metadata map[string]interface{}) *TemplateData { + return &TemplateData{ + Domain: domain, + ParentDatabase: db, + Metadata: metadata, + } +} + // NewScriptData creates template data for script mode func NewScriptData(script *models.Script, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData { return &TemplateData{ @@ -85,6 +95,9 @@ func (td *TemplateData) Name() string { if td.Schema != nil { return td.Schema.Name } + if td.Domain != nil { + return td.Domain.Name + } if td.Script != nil { return td.Script.Name } diff --git a/pkg/writers/template/writer.go b/pkg/writers/template/writer.go index 3c81713..b123a4c 100644 --- a/pkg/writers/template/writer.go +++ b/pkg/writers/template/writer.go @@ -20,6 +20,8 @@ const ( DatabaseMode EntrypointMode = "database" // SchemaMode executes the template once per schema (multi-file output) SchemaMode EntrypointMode = "schema" + // DomainMode executes the template once per domain (multi-file output) + DomainMode EntrypointMode = "domain" // ScriptMode executes the template once per script (multi-file output) ScriptMode EntrypointMode = "script" // TableMode executes the template once per table (multi-file output) @@ -80,6 +82,8 @@ func (w *Writer) WriteDatabase(db *models.Database) error { return w.executeDatabaseMode(db) case SchemaMode: return w.executeSchemaMode(db) + case DomainMode: + return w.executeDomainMode(db) case ScriptMode: return w.executeScriptMode(db) case TableMode: @@ -143,6 +147,28 @@ func (w *Writer) executeSchemaMode(db *models.Database) error { return nil } +// executeDomainMode executes the template once per domain +func (w *Writer) executeDomainMode(db *models.Database) error { + for _, domain := range db.Domains { + data := NewDomainData(domain, db, w.options.Metadata) + output, err := w.executeTemplate(data) + if err != nil { + return fmt.Errorf("failed to execute template for domain %s: %w", domain.Name, err) + } + + filename, err := w.generateFilename(data) + if err != nil { + return fmt.Errorf("failed to generate filename for domain %s: %w", domain.Name, err) + } + + if err := w.writeOutput(output, filename); err != nil { + return fmt.Errorf("failed to write output for domain %s: %w", domain.Name, err) + } + } + + return nil +} + // executeScriptMode executes the template once per script func (w *Writer) executeScriptMode(db *models.Database) error { for _, schema := range db.Schemas {