Skip to content
Open
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 @@ -77,7 +77,8 @@ public override void Write(Utf8JsonWriter writer, DatabaseObject value, JsonSeri
// Only escape columns for properties whose type(derived type) is SourceDefinition.
if (IsSourceDefinitionOrDerivedClassProperty(prop) && propVal is SourceDefinition sourceDef)
{
EscapeDollaredColumns(sourceDef);
// Check if we need to escape any column names
propVal = GetSourceDefinitionWithEscapedColumns(sourceDef);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at the cloning approach, it seems to be a shallow copy. this should be fine for now but for awareness should be added in notes/comments.

}

JsonSerializer.Serialize(writer, propVal, options);
Expand All @@ -93,25 +94,126 @@ private static bool IsSourceDefinitionOrDerivedClassProperty(PropertyInfo prop)
}

/// <summary>
/// Escapes column keys that start with '$' to '_$' for serialization.
/// Returns the SourceDefinition as-is if no columns start with '$',
/// otherwise returns a copy with escaped column names.
/// </summary>
private static void EscapeDollaredColumns(SourceDefinition sourceDef)
private static SourceDefinition GetSourceDefinitionWithEscapedColumns(SourceDefinition sourceDef)
{
if (sourceDef.Columns is null || sourceDef.Columns.Count == 0)
// If no columns or no columns starting with '$', return original
if (sourceDef.Columns is null || sourceDef.Columns.Count == 0 ||
!sourceDef.Columns.Keys.Any(k => k.StartsWith(DOLLAR_CHAR, StringComparison.Ordinal)))
{
return;
return sourceDef;
}

List<string> keysToEscape = sourceDef.Columns.Keys
.Where(k => k.StartsWith(DOLLAR_CHAR, StringComparison.Ordinal))
.ToList();
// Create escaped columns dictionary
Dictionary<string, ColumnDefinition> escapedColumns = CreateEscapedColumnsDictionary(sourceDef.Columns);

// Create new instance based on the actual type
return sourceDef switch
{
StoredProcedureDefinition spDef => CreateStoredProcedureDefinition(spDef, escapedColumns),
_ => CreateSourceDefinition(sourceDef, escapedColumns)
};
}

foreach (string key in keysToEscape)
/// <summary>
/// Creates a dictionary with escaped column keys for serialization.
/// </summary>
private static Dictionary<string, ColumnDefinition> CreateEscapedColumnsDictionary(IDictionary<string, ColumnDefinition> originalColumns)
{
Dictionary<string, ColumnDefinition> escapedColumns = new();
foreach (KeyValuePair<string, ColumnDefinition> kvp in originalColumns)
{
ColumnDefinition col = sourceDef.Columns[key];
sourceDef.Columns.Remove(key);
string newKey = ESCAPED_DOLLARCHAR + key[1..];
sourceDef.Columns[newKey] = col;
if (kvp.Key.StartsWith(DOLLAR_CHAR, StringComparison.Ordinal))
{
string escapedKey = ESCAPED_DOLLARCHAR + kvp.Key[1..];
escapedColumns[escapedKey] = kvp.Value;
}
else
{
escapedColumns[kvp.Key] = kvp.Value;
}
}

return escapedColumns;
}

/// <summary>
/// Creates a new StoredProcedureDefinition with escaped columns and copied properties.
/// </summary>
private static StoredProcedureDefinition CreateStoredProcedureDefinition(StoredProcedureDefinition original, Dictionary<string, ColumnDefinition> escapedColumns)
{
StoredProcedureDefinition newSpDef = new()
{
IsInsertDMLTriggerEnabled = original.IsInsertDMLTriggerEnabled,
IsUpdateDMLTriggerEnabled = original.IsUpdateDMLTriggerEnabled,
PrimaryKey = original.PrimaryKey
};

// Add escaped columns
AddColumnsToDictionary(newSpDef.Columns, escapedColumns);

// Copy parameters
AddParametersToDictionary(newSpDef.Parameters, original.Parameters);

// Copy relationship map if it exists
CopyRelationshipMap(newSpDef.SourceEntityRelationshipMap, original.SourceEntityRelationshipMap);

return newSpDef;
}

/// <summary>
/// Creates a new SourceDefinition with escaped columns and copied properties.
/// </summary>
private static SourceDefinition CreateSourceDefinition(SourceDefinition original, Dictionary<string, ColumnDefinition> escapedColumns)
{
SourceDefinition newSourceDef = new()
{
IsInsertDMLTriggerEnabled = original.IsInsertDMLTriggerEnabled,
IsUpdateDMLTriggerEnabled = original.IsUpdateDMLTriggerEnabled,
PrimaryKey = original.PrimaryKey
};

// Add escaped columns
AddColumnsToDictionary(newSourceDef.Columns, escapedColumns);

// Copy relationship map if it exists
CopyRelationshipMap(newSourceDef.SourceEntityRelationshipMap, original.SourceEntityRelationshipMap);

return newSourceDef;
}

/// <summary>
/// Helper method to add columns to a dictionary.
/// </summary>
private static void AddColumnsToDictionary(IDictionary<string, ColumnDefinition> target, Dictionary<string, ColumnDefinition> source)
{
foreach (KeyValuePair<string, ColumnDefinition> kvp in source)
{
target.Add(kvp.Key, kvp.Value);
}
}

/// <summary>
/// Helper method to add parameters to a dictionary.
/// </summary>
private static void AddParametersToDictionary(IDictionary<string, ParameterDefinition> target, IDictionary<string, ParameterDefinition> source)
{
foreach (KeyValuePair<string, ParameterDefinition> kvp in source)
{
target.Add(kvp.Key, kvp.Value);
}
}

/// <summary>
/// Helper method to copy relationship map between SourceDefinition instances.
/// </summary>
private static void CopyRelationshipMap(IDictionary<string, RelationshipMetadata> target, IDictionary<string, RelationshipMetadata> source)
{
foreach (KeyValuePair<string, RelationshipMetadata> kvp in source)
{
target.Add(kvp.Key, kvp.Value);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ public void TestDictionaryDatabaseObjectSerializationDeserialization_WithDollarC

Assert.AreEqual(deserializedDatabaseTable.SourceType, _databaseTable.SourceType);
Assert.AreEqual(deserializedDatabaseTable.FullName, _databaseTable.FullName);
deserializedDatabaseTable.Equals(_databaseTable);
Assert.IsTrue(deserializedDatabaseTable.Equals(_databaseTable));
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.SourceDefinition, _databaseTable.SourceDefinition, "$FirstName");
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.TableDefinition, _databaseTable.TableDefinition, "$FirstName");
}
Expand Down Expand Up @@ -343,7 +343,7 @@ public void TestDatabaseViewSerializationDeserialization_WithDollarColumn()
DatabaseView deserializedDatabaseView = (DatabaseView)deserializedDict["person"];

Assert.AreEqual(deserializedDatabaseView.SourceType, _databaseView.SourceType);
deserializedDatabaseView.Equals(_databaseView);
Assert.IsTrue(deserializedDatabaseView.Equals(_databaseView));
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseView.SourceDefinition, _databaseView.SourceDefinition, "$FirstName");
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseView.ViewDefinition, _databaseView.ViewDefinition, "$FirstName");
}
Expand Down Expand Up @@ -377,7 +377,7 @@ public void TestDatabaseStoredProcedureSerializationDeserialization_WithDollarCo
DatabaseStoredProcedure deserializedDatabaseSP = (DatabaseStoredProcedure)deserializedDict["person"];

Assert.AreEqual(deserializedDatabaseSP.SourceType, _databaseStoredProcedure.SourceType);
deserializedDatabaseSP.Equals(_databaseStoredProcedure);
Assert.IsTrue(deserializedDatabaseSP.Equals(_databaseStoredProcedure));
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.SourceDefinition, _databaseStoredProcedure.SourceDefinition, "$FirstName", true);
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.StoredProcedureDefinition, _databaseStoredProcedure.StoredProcedureDefinition, "$FirstName", true);
}
Expand Down Expand Up @@ -529,7 +529,10 @@ private static void VerifyColumnDefinitionSerializationDeserialization(ColumnDef
Assert.AreEqual(fields, 8);

// test values
expectedColumnDefinition.Equals(deserializedColumnDefinition);
Assert.AreEqual(expectedColumnDefinition.DbType, deserializedColumnDefinition.DbType);
Assert.AreEqual(expectedColumnDefinition.SqlDbType, deserializedColumnDefinition.SqlDbType);
Assert.AreEqual(expectedColumnDefinition.IsAutoGenerated, deserializedColumnDefinition.IsAutoGenerated);
Assert.AreEqual(expectedColumnDefinition.DefaultValue.ToString(), deserializedColumnDefinition.DefaultValue.ToString());
}
Comment on lines +532 to 536
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VerifyColumnDefinitionSerializationDeserialization now compares DefaultValue.ToString(), which will throw if either DefaultValue is null. Since DefaultValue is nullable (object?), this assertion should be null-safe (and ideally validate null vs non-null explicitly) to avoid test crashes when a column has no default.

Copilot uses AI. Check for mistakes.

private static void VerifyParameterDefinitionSerializationDeserialization(ParameterDefinition expectedParameterDefinition, ParameterDefinition deserializedParameterDefinition)
Expand All @@ -538,7 +541,10 @@ private static void VerifyParameterDefinitionSerializationDeserialization(Parame
int fields = typeof(ParameterDefinition).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Length;
Assert.AreEqual(fields, 9);
// test values
expectedParameterDefinition.Equals(deserializedParameterDefinition);
Assert.AreEqual(expectedParameterDefinition.DbType, deserializedParameterDefinition.DbType);
Assert.AreEqual(expectedParameterDefinition.SqlDbType, deserializedParameterDefinition.SqlDbType);
Assert.AreEqual(expectedParameterDefinition.SystemType, deserializedParameterDefinition.SystemType);
Assert.AreEqual(expectedParameterDefinition.ConfigDefaultValue.ToString(), deserializedParameterDefinition.ConfigDefaultValue.ToString());
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VerifyParameterDefinitionSerializationDeserialization now compares ConfigDefaultValue.ToString(), which will throw if either ConfigDefaultValue is null. Since ConfigDefaultValue is nullable (object?), make this assertion null-safe (and consider asserting HasConfigDefault too) so the helper works for parameters without defaults.

Suggested change
Assert.AreEqual(expectedParameterDefinition.ConfigDefaultValue.ToString(), deserializedParameterDefinition.ConfigDefaultValue.ToString());
Assert.AreEqual(expectedParameterDefinition.HasConfigDefault, deserializedParameterDefinition.HasConfigDefault);
if (expectedParameterDefinition.HasConfigDefault)
{
Assert.AreEqual(expectedParameterDefinition.ConfigDefaultValue, deserializedParameterDefinition.ConfigDefaultValue);
}

Copilot uses AI. Check for mistakes.
}
Comment on lines 538 to 548
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VerifyParameterDefinitionSerializationDeserialization no longer asserts several fields on ParameterDefinition (e.g., Name, Required, HasConfigDefault, Default, Description). If the goal is to validate serialization round-trips, add assertions for the remaining properties so changes to parameter serialization are detected by tests.

Copilot uses AI. Check for mistakes.

private static void VerifyRelationShipPair(RelationShipPair expectedRelationShipPair, RelationShipPair deserializedRelationShipPair)
Expand Down