diff --git a/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs b/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs index 6d625c7f9d..0e4d3aecc5 100644 --- a/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs +++ b/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs @@ -78,9 +78,14 @@ public override void Write(Utf8JsonWriter writer, DatabaseObject value, JsonSeri if (IsSourceDefinitionOrDerivedClassProperty(prop) && propVal is SourceDefinition sourceDef) { EscapeDollaredColumns(sourceDef); + JsonSerializer.Serialize(writer, propVal, options); + // Immediately unescape to restore the original state + UnescapeDollaredColumns(sourceDef); + } + else + { + JsonSerializer.Serialize(writer, propVal, options); } - - JsonSerializer.Serialize(writer, propVal, options); } writer.WriteEndObject(); @@ -97,21 +102,50 @@ private static bool IsSourceDefinitionOrDerivedClassProperty(PropertyInfo prop) /// private static void EscapeDollaredColumns(SourceDefinition sourceDef) { - if (sourceDef.Columns is null || sourceDef.Columns.Count == 0) + // Escape column names in the Columns dictionary + if (sourceDef.Columns is not null && sourceDef.Columns.Count > 0) { - return; + List keysToEscape = sourceDef.Columns.Keys + .Where(k => k.StartsWith(DOLLAR_CHAR, StringComparison.Ordinal)) + .ToList(); + + foreach (string key in keysToEscape) + { + ColumnDefinition col = sourceDef.Columns[key]; + sourceDef.Columns.Remove(key); + string newKey = ESCAPED_DOLLARCHAR + key[1..]; + sourceDef.Columns[newKey] = col; + } } - List keysToEscape = sourceDef.Columns.Keys - .Where(k => k.StartsWith(DOLLAR_CHAR, StringComparison.Ordinal)) - .ToList(); + // Escape column names in SourceEntityRelationshipMap + if (sourceDef.SourceEntityRelationshipMap is not null && sourceDef.SourceEntityRelationshipMap.Count > 0) + { + foreach (RelationshipMetadata relationshipMetadata in sourceDef.SourceEntityRelationshipMap.Values) + { + foreach (List fkDefinitions in relationshipMetadata.TargetEntityToFkDefinitionMap.Values) + { + foreach (ForeignKeyDefinition fkDef in fkDefinitions) + { + EscapeColumnList(fkDef.ReferencedColumns); + EscapeColumnList(fkDef.ReferencingColumns); + } + } + } + } + } - foreach (string key in keysToEscape) + /// + /// Escapes column names in a list that start with '$' to 'DAB_ESCAPE$' prefix. + /// + private static void EscapeColumnList(List columnList) + { + for (int i = 0; i < columnList.Count; i++) { - ColumnDefinition col = sourceDef.Columns[key]; - sourceDef.Columns.Remove(key); - string newKey = ESCAPED_DOLLARCHAR + key[1..]; - sourceDef.Columns[newKey] = col; + if (columnList[i] != null && columnList[i].StartsWith(DOLLAR_CHAR, StringComparison.Ordinal)) + { + columnList[i] = ESCAPED_DOLLARCHAR + columnList[i][1..]; + } } } @@ -120,21 +154,50 @@ private static void EscapeDollaredColumns(SourceDefinition sourceDef) /// private static void UnescapeDollaredColumns(SourceDefinition sourceDef) { - if (sourceDef.Columns is null || sourceDef.Columns.Count == 0) + // Unescape column names in the Columns dictionary + if (sourceDef.Columns is not null && sourceDef.Columns.Count > 0) { - return; + List keysToUnescape = sourceDef.Columns.Keys + .Where(k => k.StartsWith(ESCAPED_DOLLARCHAR, StringComparison.Ordinal)) + .ToList(); + + foreach (string key in keysToUnescape) + { + ColumnDefinition col = sourceDef.Columns[key]; + sourceDef.Columns.Remove(key); + string newKey = DOLLAR_CHAR + key[11..]; + sourceDef.Columns[newKey] = col; + } } - List keysToUnescape = sourceDef.Columns.Keys - .Where(k => k.StartsWith(ESCAPED_DOLLARCHAR, StringComparison.Ordinal)) - .ToList(); + // Unescape column names in SourceEntityRelationshipMap + if (sourceDef.SourceEntityRelationshipMap is not null && sourceDef.SourceEntityRelationshipMap.Count > 0) + { + foreach (RelationshipMetadata relationshipMetadata in sourceDef.SourceEntityRelationshipMap.Values) + { + foreach (List fkDefinitions in relationshipMetadata.TargetEntityToFkDefinitionMap.Values) + { + foreach (ForeignKeyDefinition fkDef in fkDefinitions) + { + UnescapeColumnList(fkDef.ReferencedColumns); + UnescapeColumnList(fkDef.ReferencingColumns); + } + } + } + } + } - foreach (string key in keysToUnescape) + /// + /// Unescapes column names in a list that start with 'DAB_ESCAPE$' prefix to '$'. + /// + private static void UnescapeColumnList(List columnList) + { + for (int i = 0; i < columnList.Count; i++) { - ColumnDefinition col = sourceDef.Columns[key]; - sourceDef.Columns.Remove(key); - string newKey = DOLLAR_CHAR + key[11..]; - sourceDef.Columns[newKey] = col; + if (columnList[i] != null && columnList[i].StartsWith(ESCAPED_DOLLARCHAR, StringComparison.Ordinal)) + { + columnList[i] = DOLLAR_CHAR + columnList[i][11..]; + } } } diff --git a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs index 29735231c2..d03d7bb7ed 100644 --- a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs +++ b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs @@ -381,6 +381,72 @@ public void TestDatabaseStoredProcedureSerializationDeserialization_WithDollarCo VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.StoredProcedureDefinition, _databaseStoredProcedure.StoredProcedureDefinition, "$FirstName", true); } + /// + /// Validates serialization and deserialization of SourceDefinition with dollar-prefixed columns + /// in SourceEntityRelationshipMap (ForeignKeyDefinition ReferencedColumns and ReferencingColumns). + /// Ensures that dollar-prefixed column names in relationship metadata are properly escaped during + /// serialization and unescaped during deserialization. + /// + [TestMethod] + public void TestSourceDefinitionRelationshipMapSerializationDeserialization_WithDollarColumn() + { + InitializeObjects(generateDollaredColumn: true); + + RelationShipPair pair = GetRelationShipPair(); + + // Create ForeignKeyDefinition with dollar-prefixed columns + ForeignKeyDefinition foreignKeyDefinition = new() + { + Pair = pair, + ReferencedColumns = new List { "$Index" }, + ReferencingColumns = new List { "$FirstName" } + }; + + RelationshipMetadata metadata = new(); + metadata.TargetEntityToFkDefinitionMap.Add("customers", new List { foreignKeyDefinition }); + + _databaseTable.TableDefinition.SourceEntityRelationshipMap.Add("persons", metadata); + + // Configure serialization options with ReferenceHandler.Preserve for cyclic objects + _options = new() + { + Converters = { + new DatabaseObjectConverter(), + new TypeConverter(), + }, + ReferenceHandler = ReferenceHandler.Preserve, + }; + + // Create a dictionary to serialize the DatabaseTable (this triggers DatabaseObjectConverter) + Dictionary dict = new() { { "person", _databaseTable } }; + + // Serialize and verify that dollar-prefixed columns in relationship metadata are escaped + string serializedDict = JsonSerializer.Serialize(dict, _options); + Assert.IsTrue(serializedDict.Contains("DAB_ESCAPE$Index"), + "Serialized JSON should contain escaped dollar-prefixed column name in ReferencedColumns."); + Assert.IsTrue(serializedDict.Contains("DAB_ESCAPE$FirstName"), + "Serialized JSON should contain escaped dollar-prefixed column name in ReferencingColumns and Columns."); + + // Deserialize and verify that dollar-prefixed columns are properly unescaped + Dictionary deserializedDict = JsonSerializer.Deserialize>(serializedDict, _options)!; + DatabaseTable deserializedDatabaseTable = (DatabaseTable)deserializedDict["person"]; + + // Verify the ForeignKeyDefinition + ForeignKeyDefinition expectedForeignKeyDefinition = _databaseTable.TableDefinition.SourceEntityRelationshipMap["persons"].TargetEntityToFkDefinitionMap["customers"][0]; + ForeignKeyDefinition deserializedForeignKeyDefinition = deserializedDatabaseTable.TableDefinition.SourceEntityRelationshipMap["persons"].TargetEntityToFkDefinitionMap["customers"][0]; + + // Verify that columns were properly unescaped + Assert.AreEqual(1, deserializedForeignKeyDefinition.ReferencedColumns.Count); + Assert.AreEqual("$Index", deserializedForeignKeyDefinition.ReferencedColumns[0], + "ReferencedColumns should be unescaped back to original dollar-prefixed name."); + Assert.AreEqual(1, deserializedForeignKeyDefinition.ReferencingColumns.Count); + Assert.AreEqual("$FirstName", deserializedForeignKeyDefinition.ReferencingColumns[0], + "ReferencingColumns should be unescaped back to original dollar-prefixed name."); + + // Verify RelationShipPair equality + Assert.IsTrue(expectedForeignKeyDefinition.Equals(deserializedForeignKeyDefinition)); + } + private void InitializeObjects(bool generateDollaredColumn = false) { string columnName = generateDollaredColumn ? "$FirstName" : "FirstName";