diff --git a/lib/dbsteward.go b/lib/dbsteward.go index 1b94481..7366bf4 100644 --- a/lib/dbsteward.go +++ b/lib/dbsteward.go @@ -473,7 +473,7 @@ func (dbsteward *DBSteward) doXmlDataInsert(defFile string, dataFile string) { defFileModified := defFile + ".xmldatainserted" dbsteward.Info("Saving modified dbsteward definition as %s", defFileModified) - err = xml.SaveDefinition(defFileModified, defDoc) + err = xml.SaveDefinition(dbsteward.Logger(), defFileModified, defDoc) dbsteward.FatalIfError(err, "saving file") } func (dbsteward *DBSteward) doXmlSort(files []string) { @@ -493,7 +493,7 @@ func (dbsteward *DBSteward) doXmlConvert(files []string) { doc, err := xml.LoadDefintion(file) dbsteward.FatalIfError(err, "Could not load %s", file) xml.SqlFormatConvert(doc) - convertedXml, err := xml.FormatXml(doc) + convertedXml, err := xml.FormatXml(dbsteward.Logger(), doc) dbsteward.FatalIfError(err, "formatting xml") convertedXml = strings.Replace(convertedXml, "pgdbxml>", "dbsteward>", -1) err = util.WriteFile(convertedXml, convertedFileName) @@ -509,7 +509,7 @@ func (dbsteward *DBSteward) doXmlSlonyId(files []string, slonyOut string) { outputPrefix := dbsteward.calculateFileOutputPrefix(files) compositeFile := outputPrefix + "_composite.xml" dbsteward.Info("Saving composite as %s", compositeFile) - err = xml.SaveDefinition(compositeFile, dbDoc) + err = xml.SaveDefinition(dbsteward.Logger(), compositeFile, dbDoc) dbsteward.FatalIfError(err, "saving file") dbsteward.Info("Slony ID numbering any missing attributes") @@ -521,7 +521,7 @@ func (dbsteward *DBSteward) doXmlSlonyId(files []string, slonyOut string) { slonyIdNumberedFile = slonyOut } dbsteward.Info("Saving Slony ID numbered XML as %s", slonyIdNumberedFile) - err = xml.SaveDefinition(slonyIdNumberedFile, slonyIdDoc) + err = xml.SaveDefinition(dbsteward.Logger(), slonyIdNumberedFile, slonyIdDoc) dbsteward.FatalIfError(err, "saving file") } func (dbsteward *DBSteward) doBuild(files []string, dataFiles []string, addendums uint) { @@ -542,13 +542,13 @@ func (dbsteward *DBSteward) doBuild(files []string, dataFiles []string, addendum outputPrefix := dbsteward.calculateFileOutputPrefix(files) compositeFile := outputPrefix + "_composite.xml" dbsteward.Info("Saving composite as %s", compositeFile) - err = xml.SaveDefinition(compositeFile, dbDoc) + err = xml.SaveDefinition(dbsteward.Logger(), compositeFile, dbDoc) dbsteward.FatalIfError(err, "saving file") if addendumsDoc != nil { addendumsFile := outputPrefix + "_addendums.xml" dbsteward.Info("Saving addendums as %s", addendumsFile) - err = xml.SaveDefinition(compositeFile, addendumsDoc) + err = xml.SaveDefinition(dbsteward.Logger(), compositeFile, addendumsDoc) dbsteward.FatalIfError(err, "saving file") } @@ -573,13 +573,13 @@ func (dbsteward *DBSteward) doDiff(oldFiles []string, newFiles []string, dataFil oldOutputPrefix := dbsteward.calculateFileOutputPrefix(oldFiles) oldCompositeFile := oldOutputPrefix + "_composite.xml" dbsteward.Info("Saving composite as %s", oldCompositeFile) - err = xml.SaveDefinition(oldCompositeFile, oldDbDoc) + err = xml.SaveDefinition(dbsteward.Logger(), oldCompositeFile, oldDbDoc) dbsteward.FatalIfError(err, "saving file") newOutputPrefix := dbsteward.calculateFileOutputPrefix(newFiles) newCompositeFile := newOutputPrefix + "_composite.xml" dbsteward.Info("Saving composite as %s", newCompositeFile) - err = xml.SaveDefinition(newCompositeFile, newDbDoc) + err = xml.SaveDefinition(dbsteward.Logger(), newCompositeFile, newDbDoc) dbsteward.FatalIfError(err, "saving file") err = dbsteward.Lookup().OperationsConstructor().BuildUpgrade( @@ -591,7 +591,7 @@ func (dbsteward *DBSteward) doDiff(oldFiles []string, newFiles []string, dataFil func (dbsteward *DBSteward) doExtract(dbHost string, dbPort uint, dbName, dbUser, dbPass string, outputFile string) { output := dbsteward.Lookup().OperationsConstructor().ExtractSchema(dbHost, dbPort, dbName, dbUser, dbPass) dbsteward.Info("Saving extracted database schema to %s", outputFile) - err := xml.SaveDefinition(outputFile, output) + err := xml.SaveDefinition(dbsteward.Logger(), outputFile, output) dbsteward.FatalIfError(err, "saving file") } func (dbsteward *DBSteward) doDbDataDiff(files []string, dataFiles []string, addendums uint, dbHost string, dbPort uint, dbName, dbUser, dbPass string) { @@ -614,12 +614,12 @@ func (dbsteward *DBSteward) doDbDataDiff(files []string, dataFiles []string, add outputPrefix := dbsteward.calculateFileOutputPrefix(files) compositeFile := outputPrefix + "_composite.xml" dbsteward.Info("Saving composite as %s", compositeFile) - err = xml.SaveDefinition(compositeFile, dbDoc) + err = xml.SaveDefinition(dbsteward.Logger(), compositeFile, dbDoc) dbsteward.FatalIfError(err, "saving file") output, err := dbsteward.Lookup().OperationsConstructor().CompareDbData(dbDoc, dbHost, dbPort, dbName, dbUser, dbPass) dbsteward.FatalIfError(err, "comparing data") - err = xml.SaveDefinition(compositeFile, output) + err = xml.SaveDefinition(dbsteward.Logger(), compositeFile, output) dbsteward.FatalIfError(err, "saving file") } func (dbsteward *DBSteward) doSqlDiff(oldSql, newSql []string, outputFile string) { diff --git a/lib/encoding/xml/column.go b/lib/encoding/xml/column.go index c79490e..773be33 100644 --- a/lib/encoding/xml/column.go +++ b/lib/encoding/xml/column.go @@ -3,6 +3,7 @@ package xml import ( "encoding/xml" "fmt" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" ) @@ -42,18 +43,72 @@ type Column struct { AfterAddPostStage3 string `xml:"afterAddPostStage3,attr,omitempty"` } +func ColumnsFromIR(l *slog.Logger, cols []*ir.Column) ([]*Column, error) { + if len(cols) == 0 { + return nil, nil + } + var rv []*Column + for _, c := range cols { + if c != nil { + nc, err := ColumnFromIR(l, c) + if err != nil { + return nil, err + } + rv = append(rv, nc) + } + } + return rv, nil +} + +func ColumnFromIR(l *slog.Logger, col *ir.Column) (*Column, error) { + if col == nil { + return nil, nil + } + l = l.With(slog.String("column", col.Name)) + l.Debug("converting column") + defer l.Debug("done converting column") + rv := Column{ + Name: col.Name, + Type: col.Type, + Nullable: col.Nullable, + Default: col.Default, + Description: col.Description, + Unique: col.Unique, + Check: col.Check, + SerialStart: col.SerialStart, + OldColumnName: col.OldColumnName, + ConvertUsing: col.ConvertUsing, + ForeignSchema: col.ForeignSchema, + ForeignTable: col.ForeignTable, + ForeignColumn: col.ForeignColumn, + ForeignKeyName: col.ForeignKeyName, + ForeignIndexName: col.ForeignIndexName, + ForeignOnUpdate: string(col.ForeignOnUpdate), + ForeignOnDelete: string(col.ForeignOnDelete), + Statistics: col.Statistics, + BeforeAddStage1: col.BeforeAddStage1, + AfterAddStage1: col.AfterAddStage1, + BeforeAddStage2: col.BeforeAddStage2, + AfterAddStage2: col.AfterAddStage2, + BeforeAddStage3: col.BeforeAddStage3, + AfterAddStage3: col.AfterAddStage3, + // Ignoring depricated fields for now + } + return &rv, nil +} + // Implement some custom unmarshalling behavior -func (self *Column) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { +func (col *Column) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { type colAlias Column // prevents recursion while decoding, as type aliases have no methods // set defaults - col := &colAlias{ + ca := &colAlias{ Nullable: true, // as in SQL NULL } - err := decoder.DecodeElement(col, &start) + err := decoder.DecodeElement(ca, &start) if err != nil { return err } - *self = Column(*col) + *col = Column(*col) return nil } diff --git a/lib/encoding/xml/constraint.go b/lib/encoding/xml/constraint.go index ef4e1ad..d0f3928 100644 --- a/lib/encoding/xml/constraint.go +++ b/lib/encoding/xml/constraint.go @@ -2,6 +2,7 @@ package xml import ( "fmt" + "log/slog" "strings" "github.com/dbsteward/dbsteward/lib/ir" @@ -16,6 +17,29 @@ type Constraint struct { ForeignTable string `xml:"foreignTable,attr,omitempty"` } +func ConstraintsFromIR(l *slog.Logger, cs []*ir.Constraint) ([]*Constraint, error) { + if len(cs) == 0 { + return nil, nil + } + var rv []*Constraint + for _, c := range cs { + if c != nil { + rv = append( + rv, + &Constraint{ + Name: c.Name, + Type: string(c.Type), + Definition: c.Definition, + ForeignIndexName: c.ForeignIndexName, + ForeignSchema: c.ForeignSchema, + ForeignTable: c.ForeignTable, + }, + ) + } + } + return rv, nil +} + func (c *Constraint) ToIR() (*ir.Constraint, error) { rv := ir.Constraint{ Name: c.Name, @@ -32,17 +56,17 @@ func (c *Constraint) ToIR() (*ir.Constraint, error) { return &rv, nil } -func (self *Constraint) IdentityMatches(other *Constraint) bool { +func (c *Constraint) IdentityMatches(other *Constraint) bool { if other == nil { return false } - return strings.EqualFold(self.Name, other.Name) + return strings.EqualFold(c.Name, other.Name) } -func (self *Constraint) Merge(overlay *Constraint) { +func (c *Constraint) Merge(overlay *Constraint) { if overlay == nil { return } - self.Type = overlay.Type - self.Definition = overlay.Definition + c.Type = overlay.Type + c.Definition = overlay.Definition } diff --git a/lib/encoding/xml/data_rows.go b/lib/encoding/xml/data_rows.go index 614a42e..2c8ddf5 100644 --- a/lib/encoding/xml/data_rows.go +++ b/lib/encoding/xml/data_rows.go @@ -1,6 +1,7 @@ package xml import ( + "log/slog" "strings" "github.com/dbsteward/dbsteward/lib/ir" @@ -15,6 +16,47 @@ type DataRows struct { TabRows []string `xml:"tabrow"` } +func DataRowsFromIR(l *slog.Logger, rows *ir.DataRows) (*DataRows, error) { + if rows == nil { + return nil, nil + } + rv := DataRows{ + TabRowDelimiter: rows.TabRowDelimiter, + Columns: rows.Columns, + } + rv.TabRows = append(rv.TabRows, rows.TabRows...) + for _, row := range rows.Rows { + if row != nil { + rv.Rows = append( + rv.Rows, + &DataRow{ + Columns: DataColFromIR(l, row.Columns), + Delete: row.Delete, + }, + ) + } + } + return &rv, nil +} + +func DataColFromIR(l *slog.Logger, cols []*ir.DataCol) []*DataCol { + var rv []*DataCol + for _, col := range cols { + if col != nil { + rv = append( + rv, + &DataCol{ + Null: col.Null, + Empty: col.Empty, + Sql: col.Sql, + Text: col.Text, + }, + ) + } + } + return rv +} + func (dr *DataRows) ToIR() (*ir.DataRows, error) { if dr == nil { return nil, nil @@ -76,20 +118,20 @@ func (dc *DataCol) ToIR() (*ir.DataCol, error) { return &rv, nil } -func (self *DataRows) AddColumn(name string, value string) error { - if self.HasColumn(name) { +func (drs *DataRows) AddColumn(name string, value string) error { + if drs.HasColumn(name) { return errors.Errorf("already has column %s", name) } - self.Columns = append(self.Columns, name) - for _, row := range self.Rows { + drs.Columns = append(drs.Columns, name) + for _, row := range drs.Rows { // TODO(feat) what about nulls? row.Columns = append(row.Columns, &DataCol{Text: value}) } return nil } -func (self *DataRows) HasColumn(name string) bool { - for _, col := range self.Columns { +func (drs *DataRows) HasColumn(name string) bool { + for _, col := range drs.Columns { if col == name { return true } @@ -98,13 +140,13 @@ func (self *DataRows) HasColumn(name string) bool { } // Replaces TabRows with Rows -func (self *DataRows) ConvertTabRows() { - delimiter := util.CoalesceStr(self.TabRowDelimiter, "\t") +func (drs *DataRows) ConvertTabRows() { + delimiter := util.CoalesceStr(drs.TabRowDelimiter, "\t") delimiter = strings.ReplaceAll(delimiter, "\\t", "\t") delimiter = strings.ReplaceAll(delimiter, "\\n", "\n") - self.TabRowDelimiter = "" + drs.TabRowDelimiter = "" - for _, tabrow := range self.TabRows { + for _, tabrow := range drs.TabRows { tabcols := strings.Split(tabrow, delimiter) row := &DataRow{ Columns: make([]*DataCol, len(tabcols)), @@ -119,26 +161,26 @@ func (self *DataRows) ConvertTabRows() { } } } - self.TabRows = nil + drs.TabRows = nil } -func (self *DataRows) GetColMap(row *DataRow) map[string]*DataCol { - return self.GetColMapKeys(row, self.Columns) +func (drs *DataRows) GetColMap(row *DataRow) map[string]*DataCol { + return drs.GetColMapKeys(row, drs.Columns) } -func (self *DataRows) GetColMapKeys(row *DataRow, keys []string) map[string]*DataCol { +func (drs *DataRows) GetColMapKeys(row *DataRow, keys []string) map[string]*DataCol { out := map[string]*DataCol{} for i, col := range row.Columns { - if util.IStrsContains(keys, self.Columns[i]) { - out[self.Columns[i]] = col + if util.IStrsContains(keys, drs.Columns[i]) { + out[drs.Columns[i]] = col } } return out } -func (self *DataRows) TryGetRowMatchingColMap(colmap map[string]*DataCol) *DataRow { - for _, row := range self.Rows { - if self.RowMatchesColMap(row, colmap) { +func (drs *DataRows) TryGetRowMatchingColMap(colmap map[string]*DataCol) *DataRow { + for _, row := range drs.Rows { + if drs.RowMatchesColMap(row, colmap) { return row } } @@ -146,10 +188,10 @@ func (self *DataRows) TryGetRowMatchingColMap(colmap map[string]*DataCol) *DataR } // `row` matches `colmap` if all the columns in colmap match a column in the row -func (self *DataRows) RowMatchesColMap(row *DataRow, colmap map[string]*DataCol) bool { +func (drs *DataRows) RowMatchesColMap(row *DataRow, colmap map[string]*DataCol) bool { for colName, col := range colmap { // find the corresponding column - idx := util.IStrsIndex(self.Columns, colName) + idx := util.IStrsIndex(drs.Columns, colName) if idx < 0 { return false // the column doesn't exist in this DataRows } @@ -162,11 +204,11 @@ func (self *DataRows) RowMatchesColMap(row *DataRow, colmap map[string]*DataCol) return true } -func (self *DataRows) tryGetColIndexesOfNames(names []string) ([]int, bool) { +func (drs *DataRows) tryGetColIndexesOfNames(names []string) ([]int, bool) { out := make([]int, len(names)) for i, name := range names { found := false - for j, col := range self.Columns { + for j, col := range drs.Columns { if strings.EqualFold(name, col) { out[i] = j found = true @@ -181,8 +223,8 @@ func (self *DataRows) tryGetColIndexesOfNames(names []string) ([]int, bool) { } // checks to see that ownRow == otherRow, accounting for possible differences in column count or order -func (self *DataRows) RowEquals(ownRow, otherRow *DataRow, otherColumns []string) bool { - if len(self.Columns) != len(otherColumns) { +func (drs *DataRows) RowEquals(ownRow, otherRow *DataRow, otherColumns []string) bool { + if len(drs.Columns) != len(otherColumns) { return false } @@ -190,7 +232,7 @@ func (self *DataRows) RowEquals(ownRow, otherRow *DataRow, otherColumns []string return false } - otherIndexes, ok := self.tryGetColIndexesOfNames(otherColumns) + otherIndexes, ok := drs.tryGetColIndexesOfNames(otherColumns) if !ok { return false } @@ -204,19 +246,19 @@ func (self *DataRows) RowEquals(ownRow, otherRow *DataRow, otherColumns []string return false } -func (self *DataCol) Equals(other *DataCol) bool { - if self == nil || other == nil { +func (dc *DataCol) Equals(other *DataCol) bool { + if dc == nil || other == nil { return false } - if self.Null && other.Null { + if dc.Null && other.Null { return true } - if self.Empty && other.Empty { + if dc.Empty && other.Empty { return true } - if self.Sql != other.Sql { + if dc.Sql != other.Sql { return false } // TODO(feat) something other than plain old string equality? - return self.Text == other.Text + return dc.Text == other.Text } diff --git a/lib/encoding/xml/database.go b/lib/encoding/xml/database.go index ff87af8..58a46d2 100644 --- a/lib/encoding/xml/database.go +++ b/lib/encoding/xml/database.go @@ -2,6 +2,7 @@ package xml import ( "fmt" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" ) @@ -22,11 +23,43 @@ type RoleAssignment struct { CustomRoles DelimitedList `xml:"customRole,omitempty"` } +func RoleAssignmentFromIR(l *slog.Logger, i *ir.RoleAssignment) *RoleAssignment { + l.Debug("translating role assignments") + defer l.Debug("done translating role assignments") + ra := RoleAssignment{ + Application: i.Application, + Owner: i.Owner, + Replication: i.Replication, + ReadOnly: i.ReadOnly, + CustomRoles: i.CustomRoles, + } + return &ra +} + type ConfigParam struct { Name string `xml:"name,attr"` Value string `xml:"value,attr"` } +func ConfigParamsFromIR(l *slog.Logger, c []*ir.ConfigParam) []*ConfigParam { + l.Debug("translating config parameters") + defer l.Debug("done translating config parameters") + if len(c) == 0 { + return nil + } + var cp []*ConfigParam + for _, ircp := range c { + cp = append( + cp, + &ConfigParam{ + Name: ircp.Name, + Value: ircp.Value, + }, + ) + } + return cp +} + func (db *Database) ToIR() (*ir.Database, error) { if db == nil { return nil, nil diff --git a/lib/encoding/xml/datatype.go b/lib/encoding/xml/datatype.go index 5932542..a3c0680 100644 --- a/lib/encoding/xml/datatype.go +++ b/lib/encoding/xml/datatype.go @@ -1,6 +1,10 @@ package xml -import "github.com/dbsteward/dbsteward/lib/ir" +import ( + "log/slog" + + "github.com/dbsteward/dbsteward/lib/ir" +) type DataType struct { Name string `xml:"name,attr,omitempty"` @@ -12,26 +16,116 @@ type DataType struct { DomainConstraints []*DataTypeDomainConstraint `xml:"domainConstraint"` } +func TypesFromIR(l *slog.Logger, types []*ir.TypeDef) ([]*DataType, error) { + if len(types) == 0 { + return nil, nil + } + var rv []*DataType + for _, t := range types { + if t != nil { + ndt, err := TypeFromIR(l, t) + if err != nil { + return nil, err + } + rv = append(rv, ndt) + } + } + return rv, nil +} + +func TypeFromIR(l *slog.Logger, t *ir.TypeDef) (*DataType, error) { + l = l.With(slog.String("TypeDef", t.Name)) + ndt := DataType{ + Name: t.Name, + Kind: t.Kind.String(), + // SlonySetId Not in the IR + EnumValues: DataTypeEnumValuesFromIR(l, t.EnumValues), + CompositeFields: DataTypeCompositFieldsFromIR(l, t.CompositeFields), + DomainType: DataTypeDomainTypeFromIR(l, t.DomainType), + DomainConstraints: DataTypeDomainConstraintsFromIR(l, t.DomainConstraints), + } + return &ndt, nil +} + type DataTypeEnumValue struct { Value string `xml:"name,attr"` } +func DataTypeEnumValuesFromIR(l *slog.Logger, vals []ir.DataTypeEnumValue) []*DataTypeEnumValue { + if len(vals) == 0 { + return nil + } + var rv []*DataTypeEnumValue + for _, val := range vals { + rv = append( + rv, + &DataTypeEnumValue{Value: string(val)}, + ) + } + return rv +} + type DataTypeCompositeField struct { Name string `xml:"name,attr"` Type string `xml:"type,attr"` } +func DataTypeCompositFieldsFromIR(l *slog.Logger, vals []ir.DataTypeCompositeField) []*DataTypeCompositeField { + if len(vals) == 0 { + return nil + } + var rv []*DataTypeCompositeField + for _, val := range vals { + rv = append( + rv, + &DataTypeCompositeField{ + Name: val.Name, + Type: val.Type, + }, + ) + } + return rv +} + type DataTypeDomainType struct { BaseType string `xml:"baseType,attr"` Default string `xml:"default,attr,omitempty"` Nullable bool `xml:"null,attr,omitempty"` } +func DataTypeDomainTypeFromIR(l *slog.Logger, d *ir.DataTypeDomainType) *DataTypeDomainType { + if d == nil { + return nil + } + return &DataTypeDomainType{ + BaseType: d.BaseType, + Default: d.Default, + Nullable: d.Nullable, + } +} + type DataTypeDomainConstraint struct { Name string `xml:"name,attr,omitempty"` Check string `xml:",chardata"` } -func (self *DataType) ToIR() (*ir.TypeDef, error) { +func DataTypeDomainConstraintsFromIR(l *slog.Logger, cons []ir.DataTypeDomainConstraint) []*DataTypeDomainConstraint { + if len(cons) == 0 { + return nil + } + var rv []*DataTypeDomainConstraint + for _, con := range cons { + rv = append( + rv, + &DataTypeDomainConstraint{ + Name: con.Name, + Check: con.Check, + }, + ) + } + return rv +} + +func (dt *DataType) ToIR() (*ir.TypeDef, error) { panic("todo") } diff --git a/lib/encoding/xml/document.go b/lib/encoding/xml/document.go index 7518bb6..66eda97 100644 --- a/lib/encoding/xml/document.go +++ b/lib/encoding/xml/document.go @@ -2,6 +2,7 @@ package xml import ( "encoding/xml" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" "github.com/dbsteward/dbsteward/lib/util" @@ -22,10 +23,46 @@ type IncludeFile struct { Name string `xml:"name,attr"` } +func IncludeFilesFromIR(l *slog.Logger, files []*ir.IncludeFile) ([]*IncludeFile, error) { + if len(files) == 0 { + return nil, nil + } + var rv []*IncludeFile + for _, file := range files { + if file != nil { + rv = append( + rv, + &IncludeFile{ + Name: file.Name, + }, + ) + } + } + return rv, nil +} + type InlineAssembly struct { Name string `xml:"name,attr"` } +func InlineAssemblyFromIR(l *slog.Logger, recs []*ir.InlineAssembly) ([]*InlineAssembly, error) { + if len(recs) == 0 { + return nil, nil + } + var rv []*InlineAssembly + for _, rec := range recs { + if rec != nil { + rv = append( + rv, + &InlineAssembly{ + Name: rec.Name, + }, + ) + } + } + return rv, nil +} + type Sql struct { Author string `xml:"author,attr"` Ticket string `xml:"ticket,attr"` @@ -41,33 +78,33 @@ type Sql struct { // Errors may arise if this operation cannot be completed for some reason. // No semantic validation is performed at this point, as that's outside // of the scope of an "xml" package - see `ir.Definition.Validate()` -func (self *Document) ToIR() (*ir.Definition, error) { - includeFiles, err := util.MapErr(self.IncludeFiles, (*IncludeFile).ToIR) +func (doc *Document) ToIR() (*ir.Definition, error) { + includeFiles, err := util.MapErr(doc.IncludeFiles, (*IncludeFile).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process includeFile tags") } - inlineAssembly, err := util.MapErr(self.InlineAssembly, (*InlineAssembly).ToIR) + inlineAssembly, err := util.MapErr(doc.InlineAssembly, (*InlineAssembly).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process inlineAssembly tags") } - database, err := self.Database.ToIR() + database, err := doc.Database.ToIR() if err != nil { return nil, errors.Wrap(err, "could not process database tag") } - schemas, err := util.MapErr(self.Schemas, (*Schema).ToIR) + schemas, err := util.MapErr(doc.Schemas, (*Schema).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema tags") } - languages, err := util.MapErr(self.Languages, (*Language).ToIR) + languages, err := util.MapErr(doc.Languages, (*Language).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process language tags") } - sql, err := util.MapErr(self.Sql, (*Sql).ToIR) + sql, err := util.MapErr(doc.Sql, (*Sql).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process sql tags") } @@ -82,22 +119,45 @@ func (self *Document) ToIR() (*ir.Definition, error) { }, nil } -// FromModel builds a Document from a ir.Definition -// -// Errors may arise if this operation cannot be completed for some reason. -func (self *Document) FromModel(def *ir.Definition) error { - return errors.New("not implemented") +// FromIR builds an XML Document from an ir.Definition +func FromIR(l *slog.Logger, def *ir.Definition) (*Document, error) { + l = l.With(slog.String("operation", "translate IR to XML")) + l.Debug("starting conversion") + defer l.Debug("complted conversion") + doc := Document{ + Database: &Database{ + SqlFormat: string(def.Database.SqlFormat), + Roles: RoleAssignmentFromIR(l, def.Database.Roles), + ConfigParams: ConfigParamsFromIR(l, def.Database.ConfigParams), + }, + } + var err error + doc.Schemas, err = SchemasFromIR(l, def.Schemas) + if err != nil { + return nil, err + } + doc.IncludeFiles, err = IncludeFilesFromIR(l, def.IncludeFiles) + if err != nil { + return nil, err + } + doc.InlineAssembly, err = InlineAssemblyFromIR(l, def.InlineAssembly) + if err != nil { + return nil, err + } + // Languages + // SQL + return &doc, nil } // TODO should there be a ir.IncludeFile, or should we always overlay when converting to model? -func (self *IncludeFile) ToIR() (*ir.IncludeFile, error) { - return &ir.IncludeFile{Name: self.Name}, nil +func (doc *IncludeFile) ToIR() (*ir.IncludeFile, error) { + return &ir.IncludeFile{Name: doc.Name}, nil } -func (self *InlineAssembly) ToIR() (*ir.InlineAssembly, error) { - return &ir.InlineAssembly{Name: self.Name}, nil +func (doc *InlineAssembly) ToIR() (*ir.InlineAssembly, error) { + return &ir.InlineAssembly{Name: doc.Name}, nil } -func (self *Sql) ToIR() (*ir.Sql, error) { +func (sql *Sql) ToIR() (*ir.Sql, error) { panic("todo") } diff --git a/lib/encoding/xml/file.go b/lib/encoding/xml/file.go index 71301d3..d3d39c8 100644 --- a/lib/encoding/xml/file.go +++ b/lib/encoding/xml/file.go @@ -23,23 +23,24 @@ func LoadDefintion(file string) (*ir.Definition, error) { return ReadDef(f) } -func SaveDefinition(filename string, def *ir.Definition) error { +func SaveDefinition(l *slog.Logger, filename string, def *ir.Definition) error { + l = l.With(slog.String("filename", filename)) f, err := os.Create(filename) if err != nil { return fmt.Errorf("could not open file %s for writing: %w", filename, err) } defer f.Close() - err = WriteDef(f, def) + err = WriteDef(l, f, def) if err != nil { return fmt.Errorf("could not write XML document to '%s': %w", filename, err) } return nil } -func FormatXml(def *ir.Definition) (string, error) { +func FormatXml(l *slog.Logger, def *ir.Definition) (string, error) { buf := &bytes.Buffer{} - err := WriteDef(buf, def) + err := WriteDef(l, buf, def) if err != nil { return "", fmt.Errorf("could not marshal definition: %w", err) } @@ -76,7 +77,7 @@ func XmlCompositeAddendums(l *slog.Logger, files []string, addendums uint) (*ir. return nil, nil, fmt.Errorf("failed to composite xml file %s: %w", file, err) } } - formatted, err := FormatXml(composite) + formatted, err := FormatXml(l, composite) if err != nil { return nil, nil, err } diff --git a/lib/encoding/xml/foreignkey.go b/lib/encoding/xml/foreignkey.go index 8ed65d5..02662a6 100644 --- a/lib/encoding/xml/foreignkey.go +++ b/lib/encoding/xml/foreignkey.go @@ -1,6 +1,7 @@ package xml import ( + "log/slog" "strings" "github.com/dbsteward/dbsteward/lib/ir" @@ -17,6 +18,31 @@ type ForeignKey struct { OnDelete string `xml:"onDelete,attr,omitempty"` } +func ForeignKeysFromIR(l *slog.Logger, ks []*ir.ForeignKey) ([]*ForeignKey, error) { + if len(ks) == 0 { + return nil, nil + } + var rv []*ForeignKey + for _, k := range ks { + if k != nil { + rv = append( + rv, + &ForeignKey{ + Columns: k.Columns, + ForeignSchema: k.ForeignSchema, + ForeignTable: k.ForeignTable, + ForeignColumns: k.ForeignColumns, + ConstraintName: k.ConstraintName, + IndexName: k.IndexName, + OnUpdate: string(k.OnUpdate), + OnDelete: string(k.OnDelete), + }, + ) + } + } + return rv, nil +} + func (fk *ForeignKey) ToIR() (*ir.ForeignKey, error) { rv := ir.ForeignKey{ Columns: fk.Columns, diff --git a/lib/encoding/xml/function.go b/lib/encoding/xml/function.go index 84de4f0..8404751 100644 --- a/lib/encoding/xml/function.go +++ b/lib/encoding/xml/function.go @@ -2,6 +2,7 @@ package xml import ( "fmt" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" ) @@ -20,12 +21,62 @@ type Function struct { Grants []*Grant `xml:"grant"` } +func FunctionsFromIR(l *slog.Logger, funcs []*ir.Function) ([]*Function, error) { + if len(funcs) == 0 { + return nil, nil + } + var rv []*Function + for _, f := range funcs { + if f != nil { + ll := l.With(slog.String("function", f.Name)) + nf := Function{ + Name: f.Name, + Owner: f.Owner, + Description: f.Description, + Returns: f.Returns, + CachePolicy: f.CachePolicy, + ForceRedefine: f.ForceRedefine, + SecurityDefiner: f.SecurityDefiner, + Parameters: FunctionParametersFromIR(ll, f.Parameters), + Definitions: FunctionDefitionsFromIR(ll, f.Definitions), + } + var err error + nf.Grants, err = GrantsFromIR(ll, f.Grants) + if err != nil { + return nil, err + } + rv = append(rv, &nf) + } + } + return rv, nil +} + type FunctionParameter struct { Name string `xml:"name,attr,omitempty"` Type string `xml:"type,attr"` Direction string `xml:"direction,attr,omitempty"` } +func FunctionParametersFromIR(l *slog.Logger, params []*ir.FunctionParameter) []*FunctionParameter { + if len(params) == 0 { + return nil + } + var rv []*FunctionParameter + for _, param := range params { + if param != nil { + rv = append( + rv, + &FunctionParameter{ + Name: param.Name, + Type: param.Type, + Direction: string(param.Direction), + }, + ) + } + } + return rv +} + func (fp *FunctionParameter) ToIR() (*ir.FunctionParameter, error) { if fp == nil { return nil, nil @@ -48,6 +99,26 @@ type FunctionDefinition struct { Text string `xml:",cdata"` } +func FunctionDefitionsFromIR(l *slog.Logger, defs []*ir.FunctionDefinition) []*FunctionDefinition { + if len(defs) == 0 { + return nil + } + var rv []*FunctionDefinition + for _, def := range defs { + if def != nil { + rv = append( + rv, + &FunctionDefinition{ + SqlFormat: string(def.SqlFormat), + Language: def.Language, + Text: def.Text, + }, + ) + } + } + return rv +} + func (fd *FunctionDefinition) ToIR() (*ir.FunctionDefinition, error) { if fd == nil { return nil, nil diff --git a/lib/encoding/xml/grant.go b/lib/encoding/xml/grant.go index 2472eda..45d8f16 100644 --- a/lib/encoding/xml/grant.go +++ b/lib/encoding/xml/grant.go @@ -1,6 +1,10 @@ package xml -import "github.com/dbsteward/dbsteward/lib/ir" +import ( + "log/slog" + + "github.com/dbsteward/dbsteward/lib/ir" +) type Grant struct { Roles DelimitedList `xml:"role,attr,omitempty"` @@ -8,6 +12,26 @@ type Grant struct { With string `xml:"with,attr,omitempty"` } +func GrantsFromIR(l *slog.Logger, gs []*ir.Grant) ([]*Grant, error) { + if len(gs) == 0 { + return nil, nil + } + var rv []*Grant + for _, g := range gs { + if g != nil { + rv = append( + rv, + &Grant{ + Roles: g.Roles, + Permissions: g.Permissions, + With: g.With, + }, + ) + } + } + return rv, nil +} + func (g *Grant) ToIR() (*ir.Grant, error) { rv := ir.Grant{ Roles: g.Roles, diff --git a/lib/encoding/xml/index.go b/lib/encoding/xml/index.go index ee30ecc..0f10eda 100644 --- a/lib/encoding/xml/index.go +++ b/lib/encoding/xml/index.go @@ -2,6 +2,7 @@ package xml import ( "fmt" + "log/slog" "strings" "github.com/dbsteward/dbsteward/lib/ir" @@ -23,6 +24,72 @@ type IndexDim struct { Value string `xml:",chardata"` } +func IndexesFromIR(l *slog.Logger, idxs []*ir.Index) ([]*Index, error) { + if len(idxs) == 0 { + return nil, nil + } + var rv []*Index + for _, idx := range idxs { + if idx != nil { + rv = append(rv, IndexFromIR(l, idx)) + } + } + return rv, nil +} + +func IndexFromIR(l *slog.Logger, idx *ir.Index) *Index { + l = l.With(slog.String("index", idx.Name)) + l.Debug("converting index") + defer l.Debug("done converting index") + return &Index{ + Name: idx.Name, + Using: string(idx.Using), + Unique: idx.Unique, + Concurrently: idx.Concurrently, + Dimensions: IndexDimensionsFromIR(l, idx.Dimensions), + Conditions: IndexConditionsFromIR(l, idx.Conditions), + } +} + +func IndexDimensionsFromIR(l *slog.Logger, dims []*ir.IndexDim) []*IndexDim { + if len(dims) == 0 { + return nil + } + var rv []*IndexDim + for _, dim := range dims { + if dim != nil { + rv = append( + rv, + &IndexDim{ + Name: dim.Name, + Sql: dim.Sql, + Value: dim.Value, + }, + ) + } + } + return rv +} + +func IndexConditionsFromIR(l *slog.Logger, conds []*ir.IndexCond) []*IndexCond { + if len(conds) == 0 { + return nil + } + var rv []*IndexCond + for _, cond := range conds { + if cond != nil { + rv = append( + rv, + &IndexCond{ + SqlFormat: string(cond.SqlFormat), + Condition: cond.Condition, + }, + ) + } + } + return rv +} + func (id *IndexDim) ToIR() (*ir.IndexDim, error) { return &ir.IndexDim{ Name: id.Name, @@ -93,66 +160,66 @@ func newIndexType(s string) (ir.IndexType, error) { return "", fmt.Errorf("invalid index type '%s'", s) } -func (self *Index) AddDimensionNamed(name, value string) { +func (idx *Index) AddDimensionNamed(name, value string) { // TODO(feat) sanity check - self.Dimensions = append(self.Dimensions, &IndexDim{ + idx.Dimensions = append(idx.Dimensions, &IndexDim{ Name: name, Value: value, }) } -func (self *Index) AddDimension(value string) { - self.AddDimensionNamed( - fmt.Sprintf("%s_%d", self.Name, len(self.Dimensions)+1), +func (idx *Index) AddDimension(value string) { + idx.AddDimensionNamed( + fmt.Sprintf("%s_%d", idx.Name, len(idx.Dimensions)+1), value, ) } -func (self *Index) TryGetCondition(sqlFormat string) util.Opt[*IndexCond] { +func (idx *Index) TryGetCondition(sqlFormat string) util.Opt[*IndexCond] { // TODO(go,core) fallback to returning empty sqlformat condition if it exists - return util.Find(self.Conditions, func(c *IndexCond) bool { + return util.Find(idx.Conditions, func(c *IndexCond) bool { return strings.EqualFold(c.SqlFormat, sqlFormat) }) } -func (self *Index) IdentityMatches(other *Index) bool { +func (idx *Index) IdentityMatches(other *Index) bool { if other == nil { return false } - return strings.EqualFold(self.Name, other.Name) + return strings.EqualFold(idx.Name, other.Name) } -func (self *Index) Equals(other *Index, sqlFormat string) bool { - if self == nil || other == nil { +func (idx *Index) Equals(other *Index, sqlFormat string) bool { + if idx == nil || other == nil { // nil != nil in this case return false } - if !strings.EqualFold(self.Name, other.Name) { + if !strings.EqualFold(idx.Name, other.Name) { return false } - if self.Unique != other.Unique { + if idx.Unique != other.Unique { return false } - if self.Concurrently != other.Concurrently { + if idx.Concurrently != other.Concurrently { return false } - if !strings.EqualFold(self.Using, other.Using) { + if !strings.EqualFold(idx.Using, other.Using) { return false } - if len(self.Dimensions) != len(other.Dimensions) { + if len(idx.Dimensions) != len(other.Dimensions) { return false } // dimension order matters - for i, dim := range self.Dimensions { + for i, dim := range idx.Dimensions { if !dim.Equals(other.Dimensions[i]) { return false } } // if any conditions are defined, there must be a condition for the requested sqlFormat, and the two must be textually equal - if len(self.Conditions) > 0 || len(other.Conditions) > 0 { - if self.TryGetCondition(sqlFormat).Equals(other.TryGetCondition(sqlFormat)) { + if len(idx.Conditions) > 0 || len(other.Conditions) > 0 { + if idx.TryGetCondition(sqlFormat).Equals(other.TryGetCondition(sqlFormat)) { return false } } @@ -160,28 +227,28 @@ func (self *Index) Equals(other *Index, sqlFormat string) bool { return true } -func (self *Index) Merge(overlay *Index) { +func (idx *Index) Merge(overlay *Index) { if overlay == nil { return } - self.Using = overlay.Using - self.Unique = overlay.Unique - self.Dimensions = overlay.Dimensions + idx.Using = overlay.Using + idx.Unique = overlay.Unique + idx.Dimensions = overlay.Dimensions } -func (self *IndexDim) Equals(other *IndexDim) bool { - if self == nil || other == nil { +func (idxd *IndexDim) Equals(other *IndexDim) bool { + if idxd == nil || other == nil { return false } // name does _not_ matter for equality - it's a dbsteward concept - return self.Value == other.Value + return idxd.Value == other.Value } -func (self *IndexCond) Equals(other *IndexCond) bool { - if self == nil || other == nil { +func (idxc *IndexCond) Equals(other *IndexCond) bool { + if idxc == nil || other == nil { return false } - return strings.EqualFold(self.SqlFormat, other.SqlFormat) && - strings.TrimSpace(self.Condition) == strings.TrimSpace(other.Condition) + return strings.EqualFold(idxc.SqlFormat, other.SqlFormat) && + strings.TrimSpace(idxc.Condition) == strings.TrimSpace(other.Condition) } diff --git a/lib/encoding/xml/oneighty_test.go b/lib/encoding/xml/oneighty_test.go new file mode 100644 index 0000000..5bf4395 --- /dev/null +++ b/lib/encoding/xml/oneighty_test.go @@ -0,0 +1,32 @@ +package xml + +import ( + "log/slog" + "os" + "testing" + + "github.com/dbsteward/dbsteward/lib/ir" + "github.com/stretchr/testify/assert" +) + +// one eighty test uses an IR to build an XML document +// then extracts it and ensures it results in the +// same IR + +// TODO list: Things that don't work yet but are feature improvements + +func TestOneEighty(t *testing.T) { + const role = "postgres" + irSchema := ir.FullFeatureSchema(role) + handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) + logger := slog.New(handler) + xmlDoc, err := FromIR(logger, &irSchema) + if err != nil { + t.Fatal(err) + } + reflectedSchema, err := xmlDoc.ToIR() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, ir.FullFeatureSchema(role), *reflectedSchema, "reflection does not match original") +} diff --git a/lib/encoding/xml/partition.go b/lib/encoding/xml/partition.go index 714ccb4..3eab6c8 100644 --- a/lib/encoding/xml/partition.go +++ b/lib/encoding/xml/partition.go @@ -2,6 +2,7 @@ package xml import ( "fmt" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" ) @@ -13,6 +14,39 @@ type TablePartition struct { Segments []*TablePartitionSegment `xml:"tablePartitionSegment"` } +func TablePartitionFromIR(l *slog.Logger, p *ir.TablePartition) (*TablePartition, error) { + if p == nil { + return nil, nil + } + rv := TablePartition{ + Type: string(p.Type), + SqlFormat: string(p.SqlFormat), + } + for _, opt := range p.Options { + if opt != nil { + rv.Options = append( + rv.Options, + &TablePartitionOption{ + Name: opt.Name, + Value: opt.Value, + }, + ) + } + } + for _, seg := range p.Segments { + if seg != nil { + rv.Segments = append( + rv.Segments, + &TablePartitionSegment{ + Name: seg.Name, + Value: seg.Value, + }, + ) + } + } + return &rv, nil +} + func (tp *TablePartition) ToIR() (*ir.TablePartition, error) { if tp == nil { return nil, nil diff --git a/lib/encoding/xml/schema.go b/lib/encoding/xml/schema.go index 6c1d6ed..0d53021 100644 --- a/lib/encoding/xml/schema.go +++ b/lib/encoding/xml/schema.go @@ -1,6 +1,8 @@ package xml import ( + "log/slog" + "github.com/dbsteward/dbsteward/lib/ir" "github.com/dbsteward/dbsteward/lib/util" "github.com/pkg/errors" @@ -20,41 +22,98 @@ type Schema struct { Views []*View `xml:"view"` } -func (self *Schema) ToIR() (*ir.Schema, error) { - tables, err := util.MapErr(self.Tables, (*Table).ToIR) +func SchemasFromIR(l *slog.Logger, in []*ir.Schema) ([]*Schema, error) { + l.Debug("start converting schemas") + defer l.Debug("done converting schemas") + if len(in) == 0 { + return nil, nil + } + var rv []*Schema + for _, irsch := range in { + if irsch != nil { + nsch, err := SchemaFromIR(l, irsch) + if err != nil { + return nil, err + } + rv = append(rv, nsch) + } + } + return rv, nil +} + +func SchemaFromIR(l *slog.Logger, in *ir.Schema) (*Schema, error) { + l = l.With(slog.String("schema", in.Name)) + l.Debug("start converting schema") + defer l.Debug("done converting schema") + rv := Schema{ + Name: in.Name, + Description: in.Description, + Owner: in.Owner, + SlonySetId: in.SlonySetId.Ptr(), + } + var err error + rv.Tables, err = TablesFromIR(l, in.Tables) + if err != nil { + return nil, err + } + rv.Grants, err = GrantsFromIR(l, in.Grants) + if err != nil { + return nil, err + } + rv.Types, err = TypesFromIR(l, in.Types) + if err != nil { + return nil, err + } + rv.Sequences, err = SequencesFromIR(l, in.Sequences) + if err != nil { + return nil, err + } + rv.Functions, err = FunctionsFromIR(l, in.Functions) + if err != nil { + return nil, err + } + rv.Views, err = ViewsFromIR(l, in.Views) + if err != nil { + return nil, err + } + return &rv, nil +} + +func (sch *Schema) ToIR() (*ir.Schema, error) { + tables, err := util.MapErr(sch.Tables, (*Table).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema table tags") } - grants, err := util.MapErr(self.Grants, (*Grant).ToIR) + grants, err := util.MapErr(sch.Grants, (*Grant).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema grant tags") } - types, err := util.MapErr(self.Types, (*DataType).ToIR) + types, err := util.MapErr(sch.Types, (*DataType).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema type tags") } - sequences, err := util.MapErr(self.Sequences, (*Sequence).ToIR) + sequences, err := util.MapErr(sch.Sequences, (*Sequence).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema sequence tags") } - functions, err := util.MapErr(self.Functions, (*Function).ToIR) + functions, err := util.MapErr(sch.Functions, (*Function).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema function tags") } - triggers, err := util.MapErr(self.Triggers, (*Trigger).ToIR) + triggers, err := util.MapErr(sch.Triggers, (*Trigger).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema trigger tags") } - views, err := util.MapErr(self.Views, (*View).ToIR) + views, err := util.MapErr(sch.Views, (*View).ToIR) if err != nil { return nil, errors.Wrap(err, "could not process schema view tags") } return &ir.Schema{ - Name: self.Name, - Description: self.Description, - Owner: self.Owner, - SlonySetId: util.SomePtr(self.SlonySetId), + Name: sch.Name, + Description: sch.Description, + Owner: sch.Owner, + SlonySetId: util.SomePtr(sch.SlonySetId), Tables: tables, Grants: grants, Types: types, diff --git a/lib/encoding/xml/sequence.go b/lib/encoding/xml/sequence.go index ed8ac29..7cc3b3a 100644 --- a/lib/encoding/xml/sequence.go +++ b/lib/encoding/xml/sequence.go @@ -2,27 +2,63 @@ package xml import ( "fmt" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" "github.com/dbsteward/dbsteward/lib/util" ) type Sequence struct { - Name string `xml:"name,attr"` - Owner string `xml:"owner,attr,omitempty"` - Description string `xml:"description,attr,omitempty"` - Cache *int `xml:"cache,attr,omitempty"` - Start *int `xml:"start,attr,omitempty"` - Min *int `xml:"min,attr,omitempty"` - Max *int `xml:"max,attr,omitempty"` - Increment *int `xml:"inc,attr,omitempty"` - Cycle bool `xml:"cycle,attr,omitempty"` - OwnedByColumn string `xml:"ownedBy,attr,omitempty"` + Name string `xml:"name,attr"` + Owner string `xml:"owner,attr,omitempty"` + Description string `xml:"description,attr,omitempty"` + Cache *int `xml:"cache,attr,omitempty"` + Start *int `xml:"start,attr,omitempty"` + Min *int `xml:"min,attr,omitempty"` + Max *int `xml:"max,attr,omitempty"` + Increment *int `xml:"inc,attr,omitempty"` + Cycle bool `xml:"cycle,attr,omitempty"` + OwnedBySchema string + OwnedByTable string + OwnedByColumn string SlonyId int `xml:"slonyId,attr,omitempty"` SlonySetId *int `xml:"slonySetId,attr,omitempty"` Grants []*Grant `xml:"grant"` } +func SequencesFromIR(l *slog.Logger, seqs []*ir.Sequence) ([]*Sequence, error) { + if len(seqs) == 0 { + return nil, nil + } + var rv []*Sequence + for _, seq := range seqs { + if seq != nil { + ll := l.With(slog.String("sequence", seq.Name)) + ns := Sequence{ + Name: seq.Name, + Owner: seq.Owner, + Description: seq.Description, + Cache: seq.Cache.Ptr(), + Start: seq.Start.Ptr(), + Min: seq.Min.Ptr(), + Max: seq.Max.Ptr(), + Increment: seq.Increment.Ptr(), + Cycle: seq.Cycle, + OwnedBySchema: seq.OwnedBySchema, + OwnedByTable: seq.OwnedByTable, + OwnedByColumn: seq.OwnedByColumn, + } + var err error + ns.Grants, err = GrantsFromIR(ll, seq.Grants) + if err != nil { + return nil, err + } + rv = append(rv, &ns) + } + } + return rv, nil +} + func (s *Sequence) ToIR() (*ir.Sequence, error) { rv := ir.Sequence{ Name: s.Name, @@ -34,6 +70,8 @@ func (s *Sequence) ToIR() (*ir.Sequence, error) { Max: util.SomePtr(s.Max), Increment: util.SomePtr(s.Increment), Cycle: s.Cycle, + OwnedBySchema: s.OwnedBySchema, + OwnedByTable: s.OwnedByTable, OwnedByColumn: s.OwnedByColumn, } diff --git a/lib/encoding/xml/table.go b/lib/encoding/xml/table.go index 7347b86..0c28aea 100644 --- a/lib/encoding/xml/table.go +++ b/lib/encoding/xml/table.go @@ -2,6 +2,7 @@ package xml import ( "fmt" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" ) @@ -35,6 +36,99 @@ type TableOption struct { Value string `xml:"value"` } +func TablesFromIR(l *slog.Logger, irts []*ir.Table) ([]*Table, error) { + if len(irts) == 0 { + return nil, nil + } + l.Debug("start converting tables") + defer l.Debug("done converting tables") + if len(irts) == 0 { + return nil, nil + } + var rv []*Table + for _, irt := range irts { + if irt != nil { + t, err := TableFromIR(l, irt) + if err != nil { + return nil, err + } + rv = append(rv, t) + } + } + return rv, nil +} + +func TableFromIR(l *slog.Logger, irt *ir.Table) (*Table, error) { + l = l.With(slog.String("table", irt.Name)) + l.Debug("start converting table") + defer l.Debug("done converting table") + t := Table{ + Name: irt.Name, + Description: irt.Description, + Owner: irt.Owner, + PrimaryKey: irt.PrimaryKey, + PrimaryKeyName: irt.PrimaryKeyName, + ClusterIndex: irt.ClusterIndex, + InheritsTable: irt.InheritsTable, + InheritsSchema: irt.InheritsSchema, + OldTableName: irt.OldTableName, + OldSchemaName: irt.OldSchemaName, + // SlonySetId: Does not appear in the IR + // SlonyID: Does not appear in the IR + TableOptions: TableOptionsFromIR(l, irt.TableOptions), + } + var err error + t.Partitioning, err = TablePartitionFromIR(l, irt.Partitioning) + if err != nil { + return nil, err + } + t.Columns, err = ColumnsFromIR(l, irt.Columns) + if err != nil { + return nil, err + } + t.ForeignKeys, err = ForeignKeysFromIR(l, irt.ForeignKeys) + if err != nil { + return nil, err + } + t.Indexes, err = IndexesFromIR(l, irt.Indexes) + if err != nil { + return nil, err + } + t.Constraints, err = ConstraintsFromIR(l, irt.Constraints) + if err != nil { + return nil, err + } + t.Grants, err = GrantsFromIR(l, irt.Grants) + if err != nil { + return nil, err + } + t.Rows, err = DataRowsFromIR(l, irt.Rows) + if err != nil { + return nil, err + } + return &t, nil +} + +func TableOptionsFromIR(l *slog.Logger, irops []*ir.TableOption) []*TableOption { + if len(irops) == 0 { + return nil + } + var rv []*TableOption + for _, irop := range irops { + if irop != nil { + rv = append( + rv, + &TableOption{ + SqlFormat: string(irop.SqlFormat), + Name: irop.Name, + Value: irop.Value, + }, + ) + } + } + return rv +} + func (topt TableOption) ToIR() (*ir.TableOption, error) { sqlFormat, err := ir.NewSqlFormat(topt.SqlFormat) if err != nil { diff --git a/lib/encoding/xml/view.go b/lib/encoding/xml/view.go index 09a1867..781e1b3 100644 --- a/lib/encoding/xml/view.go +++ b/lib/encoding/xml/view.go @@ -2,6 +2,7 @@ package xml import ( "fmt" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" ) @@ -16,11 +17,56 @@ type View struct { Queries []*ViewQuery `xml:"viewQuery"` } +func ViewsFromIR(l *slog.Logger, views []*ir.View) ([]*View, error) { + if len(views) == 0 { + return nil, nil + } + var rv []*View + for _, view := range views { + if view != nil { + ll := l.With(slog.String("view", view.Name)) + nv := View{ + Name: view.Name, + Description: view.Description, + Owner: view.Owner, + DependsOnViews: view.DependsOnViews, + Queries: ViewQueriesFromIR(ll, view.Queries), + } + var err error + nv.Grants, err = GrantsFromIR(ll, view.Grants) + if err != nil { + return nil, err + } + rv = append(rv, &nv) + } + } + return rv, nil +} + type ViewQuery struct { SqlFormat string `xml:"sqlFormat,attr,omitempty"` Text string `xml:",cdata"` } +func ViewQueriesFromIR(l *slog.Logger, queries []*ir.ViewQuery) []*ViewQuery { + if len(queries) == 0 { + return nil + } + var rv []*ViewQuery + for _, query := range queries { + if query != nil { + rv = append( + rv, + &ViewQuery{ + SqlFormat: string(query.SqlFormat), + Text: query.Text, + }, + ) + } + } + return rv +} + func (vq *ViewQuery) ToIR() (*ir.ViewQuery, error) { if vq == nil { return nil, nil diff --git a/lib/encoding/xml/xml.go b/lib/encoding/xml/xml.go index f2aa398..6d16f95 100644 --- a/lib/encoding/xml/xml.go +++ b/lib/encoding/xml/xml.go @@ -13,6 +13,7 @@ package xml import ( "encoding/xml" "io" + "log/slog" "github.com/dbsteward/dbsteward/lib/ir" "github.com/pkg/errors" @@ -31,7 +32,7 @@ func ReadDoc(r io.Reader) (*Document, error) { // WriteDoc writes the given `Document` to the given `io.Writer` // in XML that conforms to the DTD at the project root -func WriteDoc(w io.Writer, doc *Document) error { +func WriteDoc(l *slog.Logger, w io.Writer, doc *Document) error { // TODO(go,nth) get rid of empty closing tags like => // Go doesn't natively support this (yet?), and google is being google about it // https://github.com/golang/go/issues/21399 @@ -50,11 +51,10 @@ func ReadDef(r io.Reader) (*ir.Definition, error) { return doc.ToIR() } -func WriteDef(w io.Writer, def *ir.Definition) error { - doc := &Document{} - err := doc.FromModel(def) +func WriteDef(l *slog.Logger, w io.Writer, def *ir.Definition) error { + doc, err := FromIR(l, def) if err != nil { return err } - return WriteDoc(w, doc) + return WriteDoc(l, w, doc) } diff --git a/lib/ir/foreignkey.go b/lib/ir/foreignkey.go index 206cd7b..c1dcfcc 100644 --- a/lib/ir/foreignkey.go +++ b/lib/ir/foreignkey.go @@ -17,7 +17,7 @@ const ( func NewForeignKeyAction(s string) (ForeignKeyAction, error) { if s == "" { - return ForeignKeyActionNoAction, nil + return "", nil } fka := ForeignKeyAction(s) if fka.Equals(ForeignKeyActionNoAction) { @@ -53,31 +53,31 @@ type ForeignKey struct { OnDelete ForeignKeyAction } -func (self *ForeignKey) GetReferencedKey() KeyNames { - cols := self.ForeignColumns +func (fk *ForeignKey) GetReferencedKey() KeyNames { + cols := fk.ForeignColumns if len(cols) == 0 { - cols = self.Columns + cols = fk.Columns } return KeyNames{ - Schema: self.ForeignSchema, - Table: self.ForeignTable, + Schema: fk.ForeignSchema, + Table: fk.ForeignTable, Columns: cols, - KeyName: self.ConstraintName, + KeyName: fk.ConstraintName, } } -func (self *ForeignKey) IdentityMatches(other *ForeignKey) bool { - if self == nil || other == nil { +func (fk *ForeignKey) IdentityMatches(other *ForeignKey) bool { + if fk == nil || other == nil { return false } // TODO(go,core) validate this constraint/index name matching behavior // TODO(feat) case sensitivity - return strings.EqualFold(self.ConstraintName, other.ConstraintName) + return strings.EqualFold(fk.ConstraintName, other.ConstraintName) } -func (self *ForeignKey) Validate(doc *Definition, schema *Schema, table *Table) []error { +func (fk *ForeignKey) Validate(doc *Definition, schema *Schema, table *Table) []error { out := []error{} - if self.ConstraintName == "" { + if fk.ConstraintName == "" { out = append(out, fmt.Errorf("foreign key in table %s.%s must have a constraint name", schema.Name, table.Name)) } // TODO(go,3) validate reference, remove other codepaths diff --git a/lib/util/lists.go b/lib/util/lists.go index c47f7e9..136e3d8 100644 --- a/lib/util/lists.go +++ b/lib/util/lists.go @@ -127,6 +127,9 @@ func Map[S ~[]T, T, U any](slice S, f func(T) U) []U { } func MapErr[S ~[]T, T, U any](slice S, f func(T) (U, error)) ([]U, error) { + if len(slice) == 0 { + return nil, nil + } out := make([]U, len(slice)) for i, t := range slice { u, err := f(t)