Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -97,21 +102,50 @@ private static bool IsSourceDefinitionOrDerivedClassProperty(PropertyInfo prop)
/// </summary>
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<string> 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<string> 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<ForeignKeyDefinition> fkDefinitions in relationshipMetadata.TargetEntityToFkDefinitionMap.Values)
{
foreach (ForeignKeyDefinition fkDef in fkDefinitions)
{
EscapeColumnList(fkDef.ReferencedColumns);
EscapeColumnList(fkDef.ReferencingColumns);
}
}
}
}
}

foreach (string key in keysToEscape)
/// <summary>
/// Escapes column names in a list that start with '$' to 'DAB_ESCAPE$' prefix.
/// </summary>
private static void EscapeColumnList(List<string> 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..];
}
}
}

Expand All @@ -120,21 +154,50 @@ private static void EscapeDollaredColumns(SourceDefinition sourceDef)
/// </summary>
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<string> 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<string> 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<ForeignKeyDefinition> fkDefinitions in relationshipMetadata.TargetEntityToFkDefinitionMap.Values)
{
foreach (ForeignKeyDefinition fkDef in fkDefinitions)
{
UnescapeColumnList(fkDef.ReferencedColumns);
UnescapeColumnList(fkDef.ReferencingColumns);
}
}
}
}
}

foreach (string key in keysToUnescape)
/// <summary>
/// Unescapes column names in a list that start with 'DAB_ESCAPE$' prefix to '$'.
/// </summary>
private static void UnescapeColumnList(List<string> 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..];
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,72 @@ public void TestDatabaseStoredProcedureSerializationDeserialization_WithDollarCo
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.StoredProcedureDefinition, _databaseStoredProcedure.StoredProcedureDefinition, "$FirstName", true);
}

/// <summary>
/// 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.
/// </summary>
[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<string> { "$Index" },
ReferencingColumns = new List<string> { "$FirstName" }
};

RelationshipMetadata metadata = new();
metadata.TargetEntityToFkDefinitionMap.Add("customers", new List<ForeignKeyDefinition> { 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<string, DatabaseObject> 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<string, DatabaseObject> deserializedDict = JsonSerializer.Deserialize<Dictionary<string, DatabaseObject>>(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";
Expand Down