diff --git a/AL2DBML.sln b/AL2DBML.sln index 07c991e..405701d 100644 --- a/AL2DBML.sln +++ b/AL2DBML.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBML.Application", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBML.DI", "src\AL2DBML.DI\AL2DBML.DI.csproj", "{6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBLM.DBMLWriter", "src\DBMLWriter\AL2DBLM.DBMLWriter.csproj", "{D2033B53-9C03-47D9-BA96-2A59F5FDCC95}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,18 @@ Global {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}.Release|x64.Build.0 = Release|Any CPU {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}.Release|x86.ActiveCfg = Release|Any CPU {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}.Release|x86.Build.0 = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x64.ActiveCfg = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x64.Build.0 = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x86.ActiveCfg = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x86.Build.0 = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|Any CPU.Build.0 = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x64.ActiveCfg = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x64.Build.0 = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x86.ActiveCfg = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -110,5 +124,6 @@ Global {DFD7C487-21C3-414D-B1B5-71A7923863B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {05BCFDA4-6B86-4105-9A52-6B4F9790069D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/src/AL2DBML.Application/Helpers/OutputSchemaHelper.cs b/src/AL2DBML.Application/Helpers/OutputSchemaHelper.cs new file mode 100644 index 0000000..e447ba5 --- /dev/null +++ b/src/AL2DBML.Application/Helpers/OutputSchemaHelper.cs @@ -0,0 +1,32 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Application.Helpers; + +public static class OutputSchemaHelper +{ + public static OutputSchema DeepCopy(OutputSchema schema) => + new() + { + Enums = schema.Enums + .Select(e => new DBMLEnum { Name = e.Name, Values = [.. e.Values] }) + .ToList(), + Tables = schema.Tables + .Select(t => new DBMLTable + { + Name = t.Name, + Fields = t.Fields + .Select(f => new DBMLColumn + { + Name = f.Name, + Type = f.Type, + IsPrimaryKey = f.IsPrimaryKey, + References = f.References?.ToArray(), + IsFlowfield = f.IsFlowfield, + CalcFormula = f.CalcFormula + }) + .ToList() + }) + .ToList() + }; + +} diff --git a/src/AL2DBML.Application/Interfaces/IAlParser.cs b/src/AL2DBML.Application/Interfaces/IAlParser.cs index 987a6cd..0f3689b 100644 --- a/src/AL2DBML.Application/Interfaces/IAlParser.cs +++ b/src/AL2DBML.Application/Interfaces/IAlParser.cs @@ -11,4 +11,5 @@ public interface IAlParser DBMLTable ParseTable(string alTableFileContent); DBMLTable ParseTableExtension(string alTableExtensionFileContent); DBMLColumn ParseField(string alFieldContent); + OutputSchema GetOutputSchema(); } diff --git a/src/AL2DBML.Application/Interfaces/IDBMLWriter.cs b/src/AL2DBML.Application/Interfaces/IDBMLWriter.cs new file mode 100644 index 0000000..3ed6100 --- /dev/null +++ b/src/AL2DBML.Application/Interfaces/IDBMLWriter.cs @@ -0,0 +1,8 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Application.Interfaces; + +public interface IDBMLWriter +{ + Task WriteDBMLAsync(OutputSchema outputSchema); +} diff --git a/src/AL2DBML.Application/Interfaces/ISchemaPostProcessor.cs b/src/AL2DBML.Application/Interfaces/ISchemaPostProcessor.cs new file mode 100644 index 0000000..db5cc45 --- /dev/null +++ b/src/AL2DBML.Application/Interfaces/ISchemaPostProcessor.cs @@ -0,0 +1,8 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Application.Interfaces; + +public interface ISchemaPostProcessor +{ + OutputSchema Process(OutputSchema schema); +} diff --git a/src/AL2DBML.DI/AL2DBML.DI.csproj b/src/AL2DBML.DI/AL2DBML.DI.csproj index 9aa9afd..b0713e0 100644 --- a/src/AL2DBML.DI/AL2DBML.DI.csproj +++ b/src/AL2DBML.DI/AL2DBML.DI.csproj @@ -10,6 +10,7 @@ + diff --git a/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs b/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs index fa0ff8d..d76e2fc 100644 --- a/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs +++ b/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs @@ -8,5 +8,6 @@ public static class AL2DbmlServiceExtensions public static IServiceCollection AddAL2Dbml(this IServiceCollection services) => services .AddApplication() - .AddParser(); + .AddParser() + .AddWriter(); } diff --git a/src/AL2DBML.DI/WriterServiceExtensions.cs b/src/AL2DBML.DI/WriterServiceExtensions.cs new file mode 100644 index 0000000..f3a1fb2 --- /dev/null +++ b/src/AL2DBML.DI/WriterServiceExtensions.cs @@ -0,0 +1,15 @@ +using AL2DBML.Application.Interfaces; +using AL2DBML.DBMLWriter; +using Microsoft.Extensions.DependencyInjection; + +namespace AL2DBML.DI; + +public static class WriterServiceExtensions +{ + public static IServiceCollection AddWriter(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + return services; + } +} diff --git a/src/AL2DBML.Parser/AlParser.cs b/src/AL2DBML.Parser/AlParser.cs index 3f04a11..facc431 100644 --- a/src/AL2DBML.Parser/AlParser.cs +++ b/src/AL2DBML.Parser/AlParser.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using AL2DBML.Application.Helpers; using AL2DBML.Application.Interfaces; using AL2DBML.Core.Enums; using AL2DBML.Core.Models; @@ -174,4 +175,9 @@ public DBMLColumn ParseField(string alFieldContent) CalcFormula = calcFormula }; } + + public OutputSchema GetOutputSchema() + { + return OutputSchemaHelper.DeepCopy(_outputSchema); + } } diff --git a/src/AL2DBML.Tests/AL2DBML.Tests.csproj b/src/AL2DBML.Tests/AL2DBML.Tests.csproj index 7f8be61..688f859 100644 --- a/src/AL2DBML.Tests/AL2DBML.Tests.csproj +++ b/src/AL2DBML.Tests/AL2DBML.Tests.csproj @@ -17,8 +17,7 @@ - - + diff --git a/src/AL2DBML.Tests/TestBase.cs b/src/AL2DBML.Tests/TestBase.cs index 6b7ef0c..4beeb0b 100644 --- a/src/AL2DBML.Tests/TestBase.cs +++ b/src/AL2DBML.Tests/TestBase.cs @@ -7,6 +7,7 @@ public abstract class TestBase { protected IServiceProvider Services { get; } protected IAlParser _parser { get; private set; } + protected IDBMLWriter _writer { get; private set; } protected TestBase() { @@ -15,6 +16,7 @@ protected TestBase() .BuildServiceProvider(); _parser = Services.GetRequiredService(); + _writer = Services.GetRequiredService(); } protected void ResetParser() diff --git a/src/AL2DBML.Tests/Writer/WriterTests.cs b/src/AL2DBML.Tests/Writer/WriterTests.cs new file mode 100644 index 0000000..fdc6824 --- /dev/null +++ b/src/AL2DBML.Tests/Writer/WriterTests.cs @@ -0,0 +1,454 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Tests.Writer; + +public class WriterTests : TestBase +{ + + // --- Schema vide --- + + [Fact] + public async Task WriteDBMLAsync_EmptySchema_ReturnsEmptyString() + { + var result = await _writer.WriteDBMLAsync(new OutputSchema()); + + Assert.Equal(string.Empty, result); + } + + // --- Enums --- + + [Fact] + public async Task WriteDBMLAsync_WithEnum_GeneratesEnumBlock() + { + var schema = new OutputSchema + { + Enums = [new DBMLEnum { Name = "Status", Values = ["Active", "Inactive"] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("enum Status {", result); + Assert.Contains(" Active", result); + Assert.Contains(" Inactive", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithEnumWithSpecialChars_QuotesIdentifiers() + { + var schema = new OutputSchema + { + Enums = [new DBMLEnum { Name = "My Enum", Values = ["Value 1"] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("enum \"My Enum\" {", result); + Assert.Contains(" \"Value 1\"", result); + } + + // --- Tables --- + + [Fact] + public async Task WriteDBMLAsync_WithSimpleTable_GeneratesTableBlock() + { + var schema = new OutputSchema + { + Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Name", Type = "Code[20]" }] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("table Customer {", result); + Assert.Contains(" Name \"Code[20]\"", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithPrimaryKeyField_AddsPkAttribute() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "No.", Type = "Code[20]", IsPrimaryKey = true }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains(" \"No.\" \"Code[20]\" [pk]", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithReferenceField_AddsRefAttribute() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "SalesLine", + Fields = + [ + new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "Id"] } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("ref: > Customer.Id", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithReferenceToSpecialCharField_QuotesRef() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "SalesLine", + Fields = + [ + new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "No."] } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("ref: > Customer.\"No.\"", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithSpecialCharsInTableName_QuotesTableName() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Salesperson/Purchaser", + Fields = [new DBMLColumn { Name = "Code", Type = "Code[20]" }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("table \"Salesperson/Purchaser\" {", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithSpecialCharsInFieldName_QuotesFieldName() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "Search Name", Type = "Code[20]" }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains(" \"Search Name\" \"Code[20]\"", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithFlowfield_AddsNoteAttribute() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn + { + Name = "Balance", + Type = "Decimal", + IsFlowfield = true, + CalcFormula = "Sum(Entry.Amount)" + } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("note: 'FlowField: CalcFormula = Sum(Entry.Amount)'", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithFlowfieldWithSingleQuote_EscapesQuote() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn + { + Name = "Balance", + Type = "Decimal", + IsFlowfield = true, + CalcFormula = "Sum('Entry'.Amount)" + } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains(@"CalcFormula = Sum(\'Entry\'.Amount)", result); + } + + [Fact] + public async Task WriteDBMLAsync_NonFlowfieldWithCalcFormula_DoesNotAddNote() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "Balance", Type = "Decimal", IsFlowfield = false, CalcFormula = "Sum(Entry.Amount)" }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.DoesNotContain("note:", result); + } + + [Fact] + public async Task WriteDBMLAsync_EnumsBeforeTables() + { + var schema = new OutputSchema + { + Enums = [new DBMLEnum { Name = "Status", Values = ["Active"] }], + Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Id", Type = "Code[20]" }] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.True(result.IndexOf("enum") < result.IndexOf("table")); + } + + // --- Non-mutation de l'input --- + + [Fact] + public async Task WriteDBMLAsync_DoesNotMutateInputSchema() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Code[20]", IsPrimaryKey = true }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } + ] + } + ] + }; + + await _writer.WriteDBMLAsync(schema); + + Assert.Equal(2, schema.Tables[0].Fields.Count); + Assert.Contains(schema.Tables[0].Fields, f => f.Name == "UnknownField"); + } + + // --- CleanupUnknownFieldReferences --- + + [Fact] + public async Task WriteDBMLAsync_UnknownRef_ResolvedToSinglePkName() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "Id", Type = "Code[20]", IsPrimaryKey = true }] + }, + new DBMLTable + { + Name = "SalesLine", + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("ref: > Customer.Id", result); + Assert.DoesNotContain("UnknownField", result); + } + + [Fact] + public async Task WriteDBMLAsync_UnknownRef_AlignsTypeWithPkType() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "No", Type = "Code[20]", IsPrimaryKey = true }] + }, + new DBMLTable + { + Name = "SalesLine", + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("CustomerNo \"Code[20]\"", result); + } + + [Fact] + public async Task WriteDBMLAsync_UnknownFieldColumn_RemovedFromTableWithSinglePk() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Code[20]", IsPrimaryKey = true }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.DoesNotContain("UnknownField", result); + } + + [Fact] + public async Task WriteDBMLAsync_UnknownRef_NotResolvedWhenTargetHasMultiplePks() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "SalesLine", + Fields = + [ + new DBMLColumn { Name = "DocType", Type = "Code[20]", IsPrimaryKey = true }, + new DBMLColumn { Name = "LineNo", Type = "Code[20]", IsPrimaryKey = true } + ] + }, + new DBMLTable + { + Name = "SalesSubLine", + Fields = [new DBMLColumn { Name = "SalesNo", Type = "Code[20]", References = ["SalesLine", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("UnknownField", result); + } + + // --- InferSinglePkWhenOnlyUnknownPlusOneColumn --- + + [Fact] + public async Task WriteDBMLAsync_InfersPk_WhenSingleRealFieldPlusUnknownField() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Code[20]" }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } + ] + }, + new DBMLTable + { + Name = "SalesLine", + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("Id \"Code[20]\" [pk]", result); + Assert.Contains("ref: > Customer.Id", result); + Assert.DoesNotContain("UnknownField", result); + } + + [Fact] + public async Task WriteDBMLAsync_DoesNotInferPk_WhenMultipleRealFields() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Code[20]" }, + new DBMLColumn { Name = "Name", Type = "Code[20]" }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.DoesNotContain("[pk]", result); + Assert.Contains("UnknownField", result); + } +} diff --git a/src/DBMLWriter/AL2DBLM.DBMLWriter.csproj b/src/DBMLWriter/AL2DBLM.DBMLWriter.csproj new file mode 100644 index 0000000..2d8d310 --- /dev/null +++ b/src/DBMLWriter/AL2DBLM.DBMLWriter.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/src/DBMLWriter/DBMLWriter.cs b/src/DBMLWriter/DBMLWriter.cs new file mode 100644 index 0000000..8b11a43 --- /dev/null +++ b/src/DBMLWriter/DBMLWriter.cs @@ -0,0 +1,76 @@ +using System.Text; +using System.Text.RegularExpressions; +using AL2DBML.Application.Interfaces; +using AL2DBML.Core.Models; + +namespace AL2DBML.DBMLWriter; + +public class DBLMWriter : IDBMLWriter +{ + private readonly ISchemaPostProcessor _postProcessor; + + public DBLMWriter(ISchemaPostProcessor postProcessor) + { + _postProcessor = postProcessor; + } + + public Task WriteDBMLAsync(OutputSchema outputSchema) + { + var schema = _postProcessor.Process(outputSchema); + + var sb = new StringBuilder(); + sb.Append(WriteEnums(schema.Enums)); + sb.Append(WriteTables(schema.Tables)); + + return Task.FromResult(sb.ToString()); + } + + private static string Quotes(string name) => + Regex.IsMatch(name, @"[^a-zA-Z0-9_]") ? $"\"{name}\"" : name; + + private static string WriteEnums(List enums) + { + var sb = new StringBuilder(); + foreach (var enumObj in enums) + { + sb.AppendLine($"enum {Quotes(enumObj.Name)} {{"); + foreach (var value in enumObj.Values) + sb.AppendLine($" {Quotes(value)}"); + sb.AppendLine("}"); + sb.AppendLine(); + } + return sb.ToString(); + } + + private static string WriteTables(List tables) + { + var sb = new StringBuilder(); + foreach (var table in tables) + { + sb.AppendLine($"table {Quotes(table.Name)} {{"); + foreach (var field in table.Fields) + { + sb.Append($" {Quotes(field.Name)} {Quotes(field.Type)}"); + + var attributes = new List(); + + if (field.IsPrimaryKey) + attributes.Add("pk"); + + if (field.References is { Length: 2 } refs && !string.IsNullOrEmpty(refs[0]) && !string.IsNullOrEmpty(refs[1])) + attributes.Add($"ref: > {Quotes(refs[0])}.{Quotes(refs[1])}"); + + if (field.IsFlowfield && !string.IsNullOrEmpty(field.CalcFormula)) + attributes.Add($"note: 'FlowField: CalcFormula = {field.CalcFormula.Replace("'", "\\'")}'"); + + if (attributes.Count > 0) + sb.Append(" [" + string.Join(", ", attributes) + "]"); + + sb.AppendLine(); + } + sb.AppendLine("}"); + sb.AppendLine(); + } + return sb.ToString(); + } +} diff --git a/src/DBMLWriter/SchemaPostProcessor.cs b/src/DBMLWriter/SchemaPostProcessor.cs new file mode 100644 index 0000000..94f4f9a --- /dev/null +++ b/src/DBMLWriter/SchemaPostProcessor.cs @@ -0,0 +1,67 @@ +using AL2DBML.Application.Helpers; +using AL2DBML.Application.Interfaces; +using AL2DBML.Core.Models; + +namespace AL2DBML.DBMLWriter; + +public class SchemaPostProcessor : ISchemaPostProcessor +{ + private const string Unknown = "UnknownField"; + + public OutputSchema Process(OutputSchema schema) + { + var copy = OutputSchemaHelper.DeepCopy(schema); + + InferSinglePkWhenOnlyUnknownPlusOneColumn(copy); + + var singlePkByTable = copy.Tables + .Select(t => new { Table = t, Pks = t.Fields.Where(f => f.IsPrimaryKey).ToList() }) + .Where(x => x.Pks.Count == 1) + .ToDictionary(x => x.Table.Name, x => x.Pks[0], StringComparer.OrdinalIgnoreCase); + + ResolveUnknownFieldReferences(copy, singlePkByTable); + RemoveUnknownFieldColumns(copy, singlePkByTable); + + return copy; + } + + private static void InferSinglePkWhenOnlyUnknownPlusOneColumn(OutputSchema schema) + { + foreach (var table in schema.Tables) + { + if (table.Fields.Count == 0 || table.Fields.Any(f => f.IsPrimaryKey)) continue; + + var realFields = table.Fields + .Where(f => !string.Equals(f.Name, Unknown, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var hasUnknown = table.Fields.Any(f => string.Equals(f.Name, Unknown, StringComparison.OrdinalIgnoreCase)); + + if (realFields.Count == 1 && hasUnknown) + realFields[0].IsPrimaryKey = true; + } + } + + private static void ResolveUnknownFieldReferences(OutputSchema schema, Dictionary singlePkByTable) + { + foreach (var table in schema.Tables) + { + foreach (var field in table.Fields) + { + if (field.References is not { Length: 2 } refs) continue; + if (!string.Equals(refs[1], Unknown, StringComparison.OrdinalIgnoreCase)) continue; + if (!singlePkByTable.TryGetValue(refs[0], out var pkField)) continue; + + field.References[1] = pkField.Name; + if (!string.IsNullOrWhiteSpace(pkField.Type)) + field.Type = pkField.Type; + } + } + } + + private static void RemoveUnknownFieldColumns(OutputSchema schema, Dictionary singlePkByTable) + { + foreach (var table in schema.Tables.Where(t => singlePkByTable.ContainsKey(t.Name))) + table.Fields.RemoveAll(f => string.Equals(f.Name, Unknown, StringComparison.OrdinalIgnoreCase)); + } +}