コード例 #1
0
 public void GetPrimaryKey(DatabaseDefinition dd)
 {
     foreach (var table in dd.GetTables())
     {
         GetPrimaryKey(table);
     }
 }
コード例 #2
0
        public void AddTableDocumentation(DatabaseDefinition dd)
        {
            var reader = Executer.ExecuteQuery(@"
SELECT
    SCHEMA_NAME(t.schema_id) as SchemaName,
    t.name AS TableName, 
    p.value AS Property
FROM
    sys.tables AS t
    INNER JOIN sys.extended_properties AS p ON p.major_id = t.object_id AND p.minor_id = 0 AND p.class = 1
    AND p.name = 'MS_Description'");

            var tables = dd.GetTables();

            foreach (var row in reader.Rows)
            {
                // TODO SchemaAndTableName.Schema might be null on default schema?
                var table = tables.Find(t => t.SchemaAndTableName.Schema == row.GetAs <string>("SchemaName") && t.SchemaAndTableName.TableName == row.GetAs <string>("TableName"));
                if (table != null)
                {
                    var description = row.GetAs <string>("Property");
                    if (!string.IsNullOrEmpty(description))
                    {
                        description = description.Replace("\\n", "\n", StringComparison.OrdinalIgnoreCase).Trim();
                        var descriptionProperty = new SqlTableDescription(table, description);
                        table.Properties.Add(descriptionProperty);
                    }
                }
            }
        }
コード例 #3
0
 public void GetIdentity(DatabaseDefinition dd)
 {
     foreach (var table in dd.GetTables())
     {
         GetIdentity(table);
     }
 }
コード例 #4
0
 public void GetForeignKeys(DatabaseDefinition dd)
 {
     foreach (var table in dd.GetTables())
     {
         GetForeignKeys(table);
     }
 }
コード例 #5
0
        public void Generate(DatabaseDefinition databaseDefinition)
        {
            Log(LogSeverity.Information, "Starting on {DatabaseName}.", "BimGenerator", DatabaseName);

            var root = new BimGeneratorRoot
            {
                Model = new BimGeneratorModel()
            };

            BimHelper.SetDefaultAnnotations(root.Model);
            BimHelper.SetDefaultDataSources(root.Model, DatabaseName);

            var relationShipRegistrations = new RelationShipRegistrations();

            foreach (var sqlTable in databaseDefinition.GetTables())
            {
                if (!Context.Customizer.ShouldSkip(sqlTable.SchemaAndTableName) &&
                    !DocumenterWriterBase.ShouldSkipKnownTechnicalTable(sqlTable.SchemaAndTableName)
                    )
                {
                    root.Model.Tables.Add(GenerateTable(sqlTable));
                    Context.Logger.Log(LogSeverity.Debug, "Table {TableName} added to bim model.", "BimGenerator", sqlTable.SchemaAndTableName);
                    GatherReferencedTablesByFK(relationShipRegistrations, sqlTable);
                }
            }

            GenerateReferences(relationShipRegistrations, root.Model);

            var jsonString = ToJson(root);

            jsonString = RemoveInvalidEmptyItems(jsonString);
            WriteJson(jsonString);
        }
コード例 #6
0
 public void GetIndexes(DatabaseDefinition dd)
 {
     foreach (var table in dd.GetTables())
     {
         GetIndex(table);
     }
 }
コード例 #7
0
        public void GenerateSingleFile(DatabaseDefinition databaseDefinition, string fileName, bool partialClass = false)
        {
            Log(LogSeverity.Information, "Starting on {DatabaseName}.", "CsGenerator", DatabaseName);

            var sb = new StringBuilder();

            var tables = databaseDefinition
                         .GetTables()
                         .Where(x => Context.Customizer?.ShouldSkip(x.SchemaAndTableName) != true)
                         .OrderBy(x => x.SchemaAndTableName.Schema)
                         .ThenBy(x => x.SchemaAndTableName.TableName).ToList();

            WriteSingleFileHeader(sb, tables, partialClass);

            var index = 0;

            foreach (var table in tables)
            {
                sb.AppendLine();
                GenerateTable(sb, table);
                index++;
            }

            WriteSingleFileFooter(sb);

            Context.Logger.Log(LogSeverity.Information, "Writing Document file {FileName}", "Documenter", fileName);

            File.WriteAllText(fileName, sb.ToString(), Encoding.UTF8);
        }
コード例 #8
0
 public void GetForeignKeysAndUniqueConstrainsts(DatabaseDefinition dd)
 {
     foreach (var table in dd.GetTables())
     {
         GetForeignKeys(table);
         GetUniqueConstraints(table);
     }
 }
コード例 #9
0
        public List <IMigration> Compare(DatabaseDefinition originalDd, DatabaseDefinition newDd)
        {
            // TODO needs to be ordered
            var changes = new List <IMigration>();

            // Compare tables
            // handle renamed tables - needs parameter / external info
            foreach (var tableOriginal in originalDd.GetTables())
            {
                if (!newDd.Contains(tableOriginal.SchemaAndTableName))
                {
                    var tableDelete = new TableDelete
                    {
                        SchemaAndTableName = tableOriginal.SchemaAndTableName
                    };

                    changes.Add(tableDelete);
                }
            }

            foreach (var tableNewDd in newDd.GetTables())
            {
                if (!originalDd.Contains(tableNewDd.SchemaAndTableName))
                {
                    var tableNew = new TableNew(tableNewDd);
                    changes.Add(tableNew);
                }
            }

            foreach (var tableOriginal in originalDd.GetTables())
            {
                // not deleted
                if (newDd.Contains(tableOriginal.SchemaAndTableName))
                {
                    var tableNew = newDd.Tables[tableOriginal.SchemaAndTableName];
                    changes.AddRange(CompareColumns(tableOriginal, tableNew));
                    changes.AddRange(ComparerPrimaryKey.ComparePrimaryKeys(tableOriginal, tableNew));
                    changes.AddRange(ComparerForeignKey.CompareForeignKeys(tableOriginal, tableNew));
                    changes.AddRange(ComparerIndex.CompareIndexes(tableOriginal, tableNew));
                    changes.AddRange(ComparerUniqueConstraint.CompareUniqueConstraints(tableOriginal, tableNew));
                }
            }

            return(changes);
        }
コード例 #10
0
 public void GetIndexes(DatabaseDefinition dd)
 {
     foreach (var table in dd.GetTables())
     {
         GetPrimaryKey(table);
         GetUniqueConstraints(table);
         GetIndexes(table);
     }
 }
コード例 #11
0
        public void GenerateMultiFile(DatabaseDefinition databaseDefinition)
        {
            Log(LogSeverity.Information, "Starting on {DatabaseName}.", "CsGenerator", DatabaseName);

            var sb = new StringBuilder();

            WritePartialMainClassHeader(sb);
            sb.AppendLine("}");

            var folder = Path.Combine(Context.GeneratorSettings.WorkingDirectory ?? @".\", DatabaseName);

            Directory.CreateDirectory(folder);
            File.WriteAllText(Path.Combine(folder, DatabaseName + ".cs"), sb.ToString(), Encoding.UTF8);

            var sqlTablesByCategory = new List <KeyValuePair <string, SqlTable> >();

            foreach (var table in databaseDefinition.GetTables())
            {
                if (!Context.Customizer.ShouldSkip(table.SchemaAndTableName))
                {
                    sqlTablesByCategory.Add(new KeyValuePair <string, SqlTable>(Context.Customizer.Category(table.SchemaAndTableName), table));
                }
            }

            var tables = sqlTablesByCategory
                         .OrderBy(kvp => kvp.Key)
                         .ThenBy(t => t.Value.SchemaAndTableName.SchemaAndName);

            foreach (var tableKvp in tables)
            {
                var category = tableKvp.Key;
                var table    = tableKvp.Value;

                sb.Clear();

                WritePartialTableFileHeader(sb);
                GenerateTable(sb, table);
                WritePartialTableFileFooter(sb);

                var categoryInPath = category;
                if (string.IsNullOrEmpty(categoryInPath))
                {
                    categoryInPath = "_no_category_";
                }

                categoryInPath = categoryInPath.Replace('?', '?');

                folder = Path.Combine(Context.GeneratorSettings.WorkingDirectory ?? @".\", DatabaseName, categoryInPath);

                var fileName = Helper.GetSimplifiedSchemaAndTableName(table.SchemaAndTableName, ".") + ".cs";

                Context.Logger.Log(LogSeverity.Information, "Writing Document file {FileName} to folder {Folder}", "Documenter", fileName, folder);

                Directory.CreateDirectory(folder);
                File.WriteAllText(Path.Combine(folder, fileName), sb.ToString(), Encoding.UTF8);
            }
        }
コード例 #12
0
        public List <SchemaCheck> Check(DatabaseDefinition dd)
        {
            var schemaChecks = new List <SchemaCheck>();

            foreach (var table in dd.GetTables())
            {
                schemaChecks.AddRange(CheckSelfReferencingFk(table));
                schemaChecks.AddRange(CheckFkAndPkAreTheSame(table));
                schemaChecks.AddRange(CheckFkContainsIdentity(table));
                schemaChecks.AddRange(CheckTableSingularNameConvention(table));
            }

            return(schemaChecks);
        }
コード例 #13
0
        //private readonly List<KeyValuePair<string, SqlTable>> _sqlTablesByCategoryNew = new List<KeyValuePair<string, SqlTable>>();
        //private readonly List<KeyValuePair<string, SqlTable>> _skippedSqlTablesByCategoryNew = new List<KeyValuePair<string, SqlTable>>();

        public void Document(DatabaseDefinition originalDd, DatabaseDefinition newDd)
        {
            Log(LogSeverity.Information, "Starting on {OriginalDatabaseName} vs. {NewDatabaseName}", "ChangeDocumenter", OriginalDatabaseName, NewDatabaseName);

            var tablesOriginal = RemoveKnownTechnicalTables(originalDd.GetTables());

            foreach (var table in tablesOriginal)
            {
                if (!Context.CustomizerOriginal.ShouldSkip(table.SchemaAndTableName))
                {
                    _sqlTablesByCategoryOrignal.Add(new KeyValuePair <string, SqlTable>(Context.CustomizerOriginal.Category(table.SchemaAndTableName), table));
                }
                else
                {
                    _skippedSqlTablesByCategoryOriginal.Add(new KeyValuePair <string, SqlTable>(Context.CustomizerOriginal.Category(table.SchemaAndTableName), table));
                }
            }

            var tablesNew = RemoveKnownTechnicalTables(newDd.GetTables());

            foreach (var table in tablesNew)
            {
                if (!Context.CustomizerNew.ShouldSkip(table.SchemaAndTableName))
                {
                    _sqlTablesByCategoryOrignal.Add(new KeyValuePair <string, SqlTable>(Context.CustomizerNew.Category(table.SchemaAndTableName), table));
                }
                else
                {
                    _skippedSqlTablesByCategoryOriginal.Add(new KeyValuePair <string, SqlTable>(Context.CustomizerNew.Category(table.SchemaAndTableName), table));
                }
            }

            WriteLine("Database", "", "Original", "New");
            WriteLine("Database", "Database name", OriginalDatabaseName, NewDatabaseName);

            var noOfTablesOriginal           = originalDd.GetTables().Count;
            var noOfNotSkippedTablesOriginal = originalDd.GetTables().Count(t => !Context.CustomizerOriginal.ShouldSkip(t.SchemaAndTableName));
            var noOfTablesNew           = newDd.GetTables().Count;
            var noOfNotSkippedTablesNew = newDd.GetTables().Count(t => !Context.CustomizerNew.ShouldSkip(t.SchemaAndTableName));

            WriteLine("Database", "Number of documented tables", noOfNotSkippedTablesOriginal, noOfNotSkippedTablesNew);
            WriteLine("Database", "Number of skipped tables", noOfTablesOriginal - noOfNotSkippedTablesOriginal, noOfTablesNew - noOfNotSkippedTablesNew);
            WriteLine("Database", "Number of tables", noOfTablesOriginal, noOfTablesNew);

            var comparer = new Comparer(Context);
            var changes  = comparer.Compare(originalDd, newDd);

            WriteLine("Tables", "Schema", "Table Name", "Event");

            foreach (var tableDelete in changes.OfType <TableDelete>())
            {
                if (!Context.CustomizerOriginal.ShouldSkip(tableDelete.SchemaAndTableName))
                {
                    WriteLine("Tables", tableDelete.SchemaAndTableName.Schema, tableDelete.SchemaAndTableName.TableName, "Deleted");
                }
            }

            foreach (var tableNew in changes.OfType <TableNew>())
            {
                if (!Context.CustomizerNew.ShouldSkip(tableNew.SchemaAndTableName))
                {
                    WriteLine("Tables", tableNew.SchemaAndTableName.Schema, tableNew.SchemaAndTableName.TableName, "Added");
                }
            }

            var processedTables = new List <SchemaAndTableName>();

            foreach (var change in changes.OfType <ColumnMigration>())
            {
                switch (change)
                {
                //"Column Name", "Data Type (DbTools)", "Data Type", "Column Length", "Column Scale", "Allow Nulls", "Primary Key", "Identity", "Default Value", "Description", "Foreign Key Name", "Referenced Table", "Link", "Referenced Column"

                case ColumnNew column:
                {
                    if (Context.CustomizerNew.ShouldSkip(column.Table.SchemaAndTableName))
                    {
                        continue;
                    }

                    ProcessColumnMigration(processedTables, column.SqlColumn, "New");
                    break;
                }

                case ColumnDelete column:
                {
                    if (Context.CustomizerOriginal.ShouldSkip(column.Table.SchemaAndTableName))
                    {
                        continue;
                    }

                    ProcessColumnMigration(processedTables, column.SqlColumn, "Delete");
                    break;
                }

                case ColumnChange column:
                {
                    if (Context.CustomizerNew.ShouldSkip(column.NewNameAndType.Table.SchemaAndTableName))
                    {
                        continue;
                    }

                    ProcessColumnMigration(processedTables, column.SqlColumn, "Original");
                    ProcessColumnMigration(processedTables, column.NewNameAndType, "Changed to");
                    break;
                }
                }
            }

            if (!Context.DocumenterSettings.NoForeignKeys)
            {
                var processedFKs = new List <SchemaAndTableName>();

                foreach (var change in changes.OfType <ForeignKeyMigration>())
                {
                    ProcessTable(processedTables, change.ForeignKey.SqlTable); // Ensure table header

                    switch (change)
                    {
                    case ForeignKeyNew fkNew:
                    {
                        if (Context.CustomizerNew.ShouldSkip(fkNew.ForeignKey.ReferredTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessForeignKey(processedFKs, fkNew.ForeignKey, "New");
                        break;
                    }

                    case ForeignKeyDelete fkDelete:
                    {
                        if (Context.CustomizerOriginal.ShouldSkip(fkDelete.ForeignKey.ReferredTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessForeignKey(processedFKs, fkDelete.ForeignKey, "Delete");

                        break;
                    }

                    case ForeignKeyChange fkChange:
                    {
                        if (Context.CustomizerNew.ShouldSkip(fkChange.NewForeignKey.ReferredTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessForeignKey(processedFKs, fkChange.ForeignKey, "Original");
                        ProcessForeignKey(processedFKs, fkChange.NewForeignKey, "Change to");

                        break;
                    }
                    }
                }
            }

            if (!Context.DocumenterSettings.NoIndexes)
            {
                var processedIndexes = new List <SchemaAndTableName>();

                foreach (var change in changes.OfType <IndexMigration>())
                {
                    ProcessTable(processedTables, change.Index.SqlTable); // Ensure table header

                    switch (change)
                    {
                    case IndexNew indexNew:
                    {
                        if (Context.CustomizerNew.ShouldSkip(indexNew.Index.SqlTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessIndex(processedIndexes, indexNew.Index, "New");
                        break;
                    }

                    case IndexDelete indexDelete:
                    {
                        if (Context.CustomizerOriginal.ShouldSkip(indexDelete.Index.SqlTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessIndex(processedIndexes, indexDelete.Index, "Delete");

                        break;
                    }

                    case IndexChange indexChange:
                    {
                        if (Context.CustomizerNew.ShouldSkip(indexChange.NewIndex.SqlTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessIndex(processedIndexes, indexChange.Index, "Original");
                        ProcessIndex(processedIndexes, indexChange.NewIndex, "Change to");

                        break;
                    }
                    }
                }
            }

            if (!Context.DocumenterSettings.NoUniqueConstraints)
            {
                var processedUniqueConsreaints = new List <SchemaAndTableName>();

                foreach (var change in changes.OfType <UniqueConstraintMigration>())
                {
                    ProcessTable(processedTables, change.UniqueConstraint.SqlTable); // Ensure table header

                    switch (change)
                    {
                    case UniqueConstraintNew ucNew:
                    {
                        if (Context.CustomizerNew.ShouldSkip(ucNew.UniqueConstraint.SqlTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessUniqueConstraint(processedUniqueConsreaints, ucNew.UniqueConstraint, "New");
                        break;
                    }

                    case UniqueConstraintDelete ucDelete:
                    {
                        if (Context.CustomizerOriginal.ShouldSkip(ucDelete.UniqueConstraint.SqlTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessUniqueConstraint(processedUniqueConsreaints, ucDelete.UniqueConstraint, "Delete");

                        break;
                    }

                    case UniqueConstraintChange ucChange:
                    {
                        if (Context.CustomizerNew.ShouldSkip(ucChange.NewUniqueConstraint.SqlTable.SchemaAndTableName))
                        {
                            continue;
                        }

                        ProcessUniqueConstraint(processedUniqueConsreaints, ucChange.UniqueConstraint, "Original");
                        ProcessUniqueConstraint(processedUniqueConsreaints, ucChange.NewUniqueConstraint, "Change to");

                        break;
                    }
                    }
                }
            }

            Log(LogSeverity.Information, "Generating Document content.", "ChangeDocumenter");
            var content = DocumenterWriter.GetContent();

            var fileName = FileName ?? (OriginalDatabaseName == null && NewDatabaseName == null
                    ? "DatabaseChanges.xlsx"
                    : $"{OriginalDatabaseName}_vs_{NewDatabaseName}.xlsx");

            var path = Context.DocumenterSettings?.WorkingDirectory;

            Log(LogSeverity.Information, "Writing Document file {FileName} to folder {Folder}", "ChangeDocumenter", fileName, path);

            if (!string.IsNullOrEmpty(path))
            {
                fileName = Path.Combine(path, fileName);
            }

            File.WriteAllBytes(fileName, content);
        }
コード例 #14
0
        public static RelationalModel Convert(DatabaseDefinition sourceDefinition, string defaultSourceSchemaName, CaseInsensitiveStringKeyDictionary <string> schemaNameMap = null, Func <SqlTable, bool> filterDelegate = null)
        {
            var newDefaultSchemaName = schemaNameMap?[defaultSourceSchemaName] ?? defaultSourceSchemaName;
            var newModel             = new RelationalModel(newDefaultSchemaName);
            var sourceTablesOrdered  = sourceDefinition.GetTables()
                                       .Where(x => filterDelegate?.Invoke(x) != false)
                                       .OrderBy(x => x.SchemaAndTableName.SchemaAndName)
                                       .ToList();

            foreach (var sourceTable in sourceTablesOrdered)
            {
                var newSchemaName = schemaNameMap?[sourceTable.SchemaAndTableName.Schema] ?? sourceTable.SchemaAndTableName.Schema ?? newDefaultSchemaName;

                var newSchema = newModel[newSchemaName]
                                ?? newModel.AddSchema(newSchemaName);

                var primaryKey = sourceTable.Properties.OfType <PrimaryKey>().FirstOrDefault();
                var newTable   = newSchema.AddTable(sourceTable.SchemaAndTableName.TableName);

                foreach (var property in sourceTable.Properties.OfType <DwhTableFlagProperty>())
                {
                    newTable.SetFlag(property.Name, true);
                }

                foreach (var property in sourceTable.Properties.OfType <DwhTableDataProperty>())
                {
                    newTable.SetAdditionalData(property.Name, property.Value);
                }

                if (sourceTable.HasProperty <EtlRunInfoDisabledProperty>())
                {
                    newTable.SetEtlRunInfoDisabled();
                }

                if (sourceTable.HasProperty <HasHistoryTableProperty>())
                {
                    newTable.SetHasHistoryTable();
                }

                var sourceTableNameOverrideProperty = sourceTable.Properties.OfType <SourceTableNameOverrideProperty>().FirstOrDefault();
                if (sourceTableNameOverrideProperty != null)
                {
                    newTable.SetSourceTableNameOverride(sourceTableNameOverrideProperty.SourceTableName);
                }

                foreach (var sourceColumn in sourceTable.Columns)
                {
                    var partOfPrimaryKey = primaryKey?.SqlColumns.Any(x => x.SqlColumn == sourceColumn) == true;
                    var newColumn        = newTable.AddColumn(sourceColumn.Name, partOfPrimaryKey);

                    foreach (var property in sourceColumn.Properties.OfType <DwhColumnFlagProperty>())
                    {
                        newColumn.SetFlag(property.Name, true);
                    }

                    foreach (var property in sourceColumn.Properties.OfType <DwhColumnDataProperty>())
                    {
                        newColumn.SetAdditionalData(property.Name, property.Value);
                    }

                    if (sourceColumn.HasProperty <Identity>())
                    {
                        newColumn.SetIdentity();
                    }

                    if (sourceColumn.HasProperty <HistoryDisabledProperty>())
                    {
                        newColumn.SetHistoryDisabled();
                    }

                    if (sourceColumn.HasProperty <RecordTimestampIndicatorProperty>())
                    {
                        newColumn.SetRecordTimestampIndicator();
                    }
                }
            }

            foreach (var table in sourceTablesOrdered)
            {
                var newSourceSchemaName = schemaNameMap?[table.SchemaAndTableName.Schema] ?? table.SchemaAndTableName.Schema ?? newDefaultSchemaName;

                var newSourceSchema = newModel[newSourceSchemaName];
                var newSourceTable  = newSourceSchema[table.SchemaAndTableName.TableName];

                foreach (var fk in table.Properties.OfType <ForeignKey>())
                {
                    var newTargetSchemaName = schemaNameMap?[fk.ReferredTable.SchemaAndTableName.Schema] ?? fk.ReferredTable.SchemaAndTableName.Schema ?? newDefaultSchemaName;

                    var newTargetSchema = newModel[newTargetSchemaName];
                    var newTargetTable  = newTargetSchema[fk.ReferredTable.SchemaAndTableName.TableName];

                    if (newTargetTable == null) // target table is filtered out
                    {
                        continue;
                    }

                    var newFk = newSourceTable.AddForeignKeyTo(newTargetTable);
                    foreach (var map in fk.ForeignKeyColumns)
                    {
                        var sourceColumn = newSourceTable[map.ForeignKeyColumn.Name];
                        var targetColumn = newTargetTable[map.ReferredColumn.Name];
                        newFk.AddColumnPair(sourceColumn, targetColumn);
                    }
                }
            }

            return(newModel);
        }
コード例 #15
0
        public void Document(DatabaseDefinition databaseDefinition)
        {
            Log(LogSeverity.Information, "Starting on {DatabaseName}.", "Documenter", DatabaseName);

            var tables = RemoveKnownTechnicalTables(databaseDefinition.GetTables());

            foreach (var table in tables)
            {
                if (!Customizer.ShouldSkip(table.SchemaAndTableName))
                {
                    _sqlTablesByCategory.Add(new KeyValuePair <string, SqlTable>(Customizer.Category(table.SchemaAndTableName), table));
                }
                else
                {
                    _skippedSqlTablesByCategory.Add(new KeyValuePair <string, SqlTable>(Customizer.Category(table.SchemaAndTableName), table));
                }
            }

            var hasCategories = _sqlTablesByCategory.Any(x => !string.IsNullOrEmpty(x.Key));

            var noOfTables           = databaseDefinition.GetTables().Count;
            var noOfNotSkippedTables = databaseDefinition.GetTables().Count(t => !Customizer.ShouldSkip(t.SchemaAndTableName));

            WriteLine("Database", "Database name", DatabaseName);
            WriteLine("Database", "Number of documented tables", noOfNotSkippedTables);
            WriteLine("Database", "Number of skipped tables", noOfTables - noOfNotSkippedTables);
            WriteLine("Database", "Number of tables", noOfTables);

            if (hasCategories)
            {
                WriteLine("Database");
                WriteLine("Database", "Documented category", "Table count");

                Context.Logger.Log(LogSeverity.Verbose, "Writing tables by category.", "Documenter");

                foreach (var category in _sqlTablesByCategory.Select(kvp => kvp.Key).Distinct().OrderBy(x => x))
                {
                    WriteLine("Database", category ?? "(No category)", _sqlTablesByCategory.Count(kvp => kvp.Key == category));
                }

                if (_skippedSqlTablesByCategory.Count > 0)
                {
                    WriteLine("Database");
                    WriteLine("Database", "Skipped category", "Table count");

                    foreach (var category in _skippedSqlTablesByCategory.Select(kvp => kvp.Key).Distinct().OrderBy(x => x))
                    {
                        WriteLine("Database", category ?? "(No category)", _skippedSqlTablesByCategory.Count(kvp => kvp.Key == category));
                    }
                }

                WriteLine("Tables", "Category", "Schema", "Table Name", "Link", "Number of columns", "Description");

                if (!Context.DocumenterSettings.NoInternalDataTypes)
                {
                    WriteLine("All columns", "Category", "Schema", "Table Name", "Column Name", "Data Type (DbTools)", "Data Type", "Column Length", "Column Scale", "Allow Nulls", "Primary Key", "Identity", "Default Value", "Description");
                }
                else
                {
                    WriteLine("All columns", "Category", "Schema", "Table Name", "Column Name", "Data Type", "Column Length", "Column Scale", "Allow Nulls", "Primary Key", "Identity", "Default Value", "Description");
                }
            }
            else
            {
                WriteLine("Tables", "Schema", "Table Name", "Link", "Number of columns", "Description");
                if (!Context.DocumenterSettings.NoInternalDataTypes)
                {
                    WriteLine("All columns", "Schema", "Table Name", "Column Name", "Data Type (DbTools)", "Data Type", "Column Length", "Column Scale", "Allow Nulls", "Primary Key", "Identity", "Default Value", "Description");
                }
                else
                {
                    WriteLine("All columns", "Schema", "Table Name", "Column Name", "Data Type", "Column Length", "Column Scale", "Allow Nulls", "Primary Key", "Identity", "Default Value", "Description");
                }
            }

            // Ensure sheet order
            if (Customizer is PatternMatchingTableCustomizer)
            {
                Write("Patt.ma.-tables");
                Write("Patt.ma.-patterns");
                Write("Patt.ma.-ma.s w exceptions");
                Write("Patt.ma.-no matches (unused)");
            }

            foreach (var tableKvp in _sqlTablesByCategory.OrderBy(kvp => kvp.Key).ThenBy(t => t.Value.SchemaAndTableName.Schema).ThenBy(t => t.Value.SchemaAndTableName.TableName))
            {
                Context.Logger.Log(LogSeverity.Verbose, "Generating {TableName}.", "Documenter", tableKvp.Value.SchemaAndTableName);
                var category = tableKvp.Key;
                var table    = tableKvp.Value;
                AddTableToTableList(category, table, hasCategories);

                var sheetColor = GetColor(table.SchemaAndTableName);
                if (sheetColor != null)
                {
                    DocumenterWriter.SetSheetColor(Helper.GetSimplifiedSchemaAndTableName(table.SchemaAndTableName), sheetColor.Value);
                }

                AddTableHeader(hasCategories, category, table);

                AddTableDetails(category, table, hasCategories);
            }

            WriteLine("Tables");

            foreach (var tableKvp in _skippedSqlTablesByCategory.OrderBy(kvp => kvp.Key).ThenBy(t => t.Value.SchemaAndTableName.Schema).ThenBy(t => t.Value.SchemaAndTableName.TableName))
            {
                var category = tableKvp.Key;
                var table    = tableKvp.Value;
                AddTableToTableList(category, table, hasCategories);
            }

            Context.Logger.Log(LogSeverity.Verbose, "Generating pattern matching info.", "Documenter");
            AddPatternMatching();
            AddPatternMatchingNoMatch();

            Log(LogSeverity.Information, "Generating Document content.", "Documenter");
            var content = DocumenterWriter.GetContent();

            var fileName = FileName ?? (DatabaseName?.Length == 0 ? "Database.xlsx" : DatabaseName + ".xlsx");

            var path = Context.DocumenterSettings?.WorkingDirectory;

            Log(LogSeverity.Information, "Writing Document file {FileName} to folder {Folder}", "Documenter", fileName, path);

            if (!string.IsNullOrEmpty(path))
            {
                fileName = Path.Combine(path, fileName);
            }

            File.WriteAllBytes(fileName, content);
        }