Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,4 @@ Visual Studio 2017/
.claude/
*.claude
.claude-*
data/
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.0.2] - 2025-11-19

### Fixed

- **DatabaseTypeRequest.Equals()**: Now uses type-specific equality semantics (Issue #19)
- **decimal**: Compares only `CSharpType` and `Size` (precision/scale) - ignores `Width` and `Unicode`
- **string**: Compares `CSharpType`, explicit width (`_maxWidthForStrings`), and `Unicode` - ignores `Size`
- **byte[]**: Compares `CSharpType` and explicit width (`_maxWidthForStrings`) - ignores `Size` and `Unicode`
- **Other types** (bool, int, long, DateTime, TimeSpan, Guid, etc.): Compares only `CSharpType`
- Fixes round-trip equality when comparing guesser-created and SQL reverse-engineered types
- Compares backing field `_maxWidthForStrings` instead of computed `Width` property to avoid Size interference
- `GetHashCode()` updated to match new equality semantics for consistency
- Added 19 comprehensive tests covering all type-specific equality cases and edge cases

## [2.0.1] - 2025-11-15

### Added
Expand Down
230 changes: 230 additions & 0 deletions Tests/DatabaseTypeRequestEnhancedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,5 +413,235 @@ public void Max_ReturnsOriginalInstance_WhenAlreadyLargeEnough()
// Assert
Assert.That(result, Is.SameAs(largeRequest));
}

#region Type-Specific Equality Tests (Issue #19)

[Test]
public void Equals_Decimal_IgnoresWidth()
{
// Arrange - same decimal type and Size, different Width
var request1 = new DatabaseTypeRequest(typeof(decimal), 100, new DecimalSize(5, 2));
var request2 = new DatabaseTypeRequest(typeof(decimal), null, new DecimalSize(5, 2));

// Act & Assert
Assert.That(request1, Is.EqualTo(request2), "Decimals with same Size but different Width should be equal");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()), "Hash codes must match for equal decimals");
}

[Test]
public void Equals_Decimal_IgnoresUnicode()
{
// Arrange - same decimal type and Size, different Unicode
var request1 = new DatabaseTypeRequest(typeof(decimal), null, new DecimalSize(4, 1)) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(decimal), null, new DecimalSize(4, 1)) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2), "Decimals with same Size but different Unicode should be equal");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()), "Hash codes must match for equal decimals");
}

[Test]
public void Equals_Decimal_ComparesSizeCorrectly()
{
// Arrange - same decimal type, different Size
var request1 = new DatabaseTypeRequest(typeof(decimal), null, new DecimalSize(5, 2));
var request2 = new DatabaseTypeRequest(typeof(decimal), null, new DecimalSize(4, 1));

// Act & Assert
Assert.That(request1, Is.Not.EqualTo(request2), "Decimals with different Size should not be equal");
}

[Test]
public void Equals_NullableDecimal_IgnoresWidthAndUnicode()
{
// Arrange - nullable decimal
var request1 = new DatabaseTypeRequest(typeof(decimal?), 100, new DecimalSize(5, 2)) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(decimal?), null, new DecimalSize(5, 2)) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2), "Nullable decimals with same Size but different Width/Unicode should be equal");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_DateTime_IgnoresAllPropertiesExceptType()
{
// Arrange - same DateTime type, different Width, Size, Unicode
var request1 = new DatabaseTypeRequest(typeof(DateTime), 100, new DecimalSize(5, 2)) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(DateTime), 200, new DecimalSize(8, 4)) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2), "DateTimes with same type should be equal regardless of Width/Size/Unicode");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_TimeSpan_IgnoresAllPropertiesExceptType()
{
// Arrange
var request1 = new DatabaseTypeRequest(typeof(TimeSpan), 50, new DecimalSize(3, 1)) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(TimeSpan), null, new DecimalSize(0, 0)) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2));
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_Int_IgnoresAllPropertiesExceptType()
{
// Arrange
var request1 = new DatabaseTypeRequest(typeof(int), 100, new DecimalSize(5, 2)) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(int), null, new DecimalSize(0, 0)) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2));
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_Bool_IgnoresAllPropertiesExceptType()
{
// Arrange
var request1 = new DatabaseTypeRequest(typeof(bool), 1) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(bool), 10) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2));
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_Guid_IgnoresAllPropertiesExceptType()
{
// Arrange
var request1 = new DatabaseTypeRequest(typeof(Guid), 36) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(Guid), null) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2));
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_ByteArray_ComparesWidthOnly()
{
// Arrange - same Width
var request1 = new DatabaseTypeRequest(typeof(byte[]), 1000, new DecimalSize(5, 2)) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(byte[]), 1000, new DecimalSize(0, 0)) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2), "byte[] with same Width should be equal (Size/Unicode ignored)");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_ByteArray_DifferentWidth_NotEqual()
{
// Arrange - different Width
var request1 = new DatabaseTypeRequest(typeof(byte[]), 1000);
var request2 = new DatabaseTypeRequest(typeof(byte[]), 2000);

// Act & Assert
Assert.That(request1, Is.Not.EqualTo(request2), "byte[] with different Width should not be equal");
}

[Test]
public void Equals_String_ComparesWidthAndUnicode()
{
// Arrange - same Width and Unicode
var request1 = new DatabaseTypeRequest(typeof(string), 100, new DecimalSize(5, 2)) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(string), 100, new DecimalSize(0, 0)) { Unicode = true };

// Act & Assert
Assert.That(request1, Is.EqualTo(request2), "Strings with same Width/Unicode should be equal (Size ignored)");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_String_DifferentWidth_NotEqual()
{
// Arrange
var request1 = new DatabaseTypeRequest(typeof(string), 100) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(string), 200) { Unicode = true };

// Act & Assert
Assert.That(request1, Is.Not.EqualTo(request2), "Strings with different Width should not be equal");
}

[Test]
public void Equals_String_DifferentUnicode_NotEqual()
{
// Arrange
var request1 = new DatabaseTypeRequest(typeof(string), 100) { Unicode = true };
var request2 = new DatabaseTypeRequest(typeof(string), 100) { Unicode = false };

// Act & Assert
Assert.That(request1, Is.Not.EqualTo(request2), "Strings with different Unicode should not be equal");
}

[Test]
public void Equals_RoundTrip_DecimalFromGuesserAndSQL()
{
// This test simulates the scenario from issue #19:
// Guesser creates a DatabaseTypeRequest with Width set
// SQL reverse-engineering creates one with Width=null
// They should be equal if Size matches

// Arrange - simulate guesser-created (with Width)
var fromGuesser = new DatabaseTypeRequest(typeof(decimal), 4, new DecimalSize(4, 1));

// Simulate SQL reverse-engineered (without Width)
var fromSQL = new DatabaseTypeRequest(typeof(decimal), null, new DecimalSize(4, 1));

// Act & Assert
Assert.That(fromSQL, Is.EqualTo(fromGuesser),
"Round-trip: Decimal from SQL and Guesser should be equal when Size matches");
Assert.That(fromSQL.GetHashCode(), Is.EqualTo(fromGuesser.GetHashCode()));
}

[Test]
public void Equals_String_WithSameWidthButDifferentSize_AreEqual()
{
// This tests the Copilot-identified edge case:
// Two strings with same _maxWidthForStrings but different Size should be equal
// because Width property computes Math.Max(_maxWidthForStrings, Size.ToStringLength())

// Arrange - both have _maxWidthForStrings=10, but different Size values
var request1 = new DatabaseTypeRequest(typeof(string), 10, new DecimalSize(20, 0));
var request2 = new DatabaseTypeRequest(typeof(string), 10, new DecimalSize(0, 0));

// Verify the Width property values are different due to Size
Assert.That(request1.Width, Is.EqualTo(20), "Width should include Size length");
Assert.That(request2.Width, Is.EqualTo(10), "Width should be just the explicit value");

// Act & Assert - they should still be equal because we compare _maxWidthForStrings, not Width
Assert.That(request1, Is.EqualTo(request2),
"Strings with same _maxWidthForStrings should be equal even if Size differs");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

[Test]
public void Equals_ByteArray_WithSameWidthButDifferentSize_AreEqual()
{
// This tests the Copilot-identified edge case for byte arrays:
// Two byte[] with same _maxWidthForStrings but different Size should be equal

// Arrange
var request1 = new DatabaseTypeRequest(typeof(byte[]), 1000, new DecimalSize(20, 0));
var request2 = new DatabaseTypeRequest(typeof(byte[]), 1000, new DecimalSize(0, 0));

// Verify the Width property values are the same (1000) despite different Size values,
// because Width returns Math.Max(_maxWidthForStrings, Size.ToStringLength())
Assert.That(request1.Width, Is.EqualTo(1000), "Width should be max of explicit and Size");
Assert.That(request2.Width, Is.EqualTo(1000), "Width should be just the explicit value");

// Act & Assert
Assert.That(request1, Is.EqualTo(request2),
"byte[] with same _maxWidthForStrings should be equal even if Size differs");
Assert.That(request1.GetHashCode(), Is.EqualTo(request2.GetHashCode()));
}

#endregion
}
}
55 changes: 52 additions & 3 deletions TypeGuesser/DatabaseTypeRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,43 @@ public DatabaseTypeRequest() : this(typeof(string))

#region Equality
/// <summary>
/// Property based equality
/// Property based equality. Compares only the properties relevant to each type:
/// - string: CSharpType, _maxWidthForStrings, Unicode
/// - decimal: CSharpType, Size (precision/scale)
/// - byte[]: CSharpType, _maxWidthForStrings
/// - All other types (bool, int, long, DateTime, TimeSpan, Guid, etc.): CSharpType only
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
private bool Equals(DatabaseTypeRequest other)
{
return CSharpType == other.CSharpType && Width == other.Width && Equals(Size, other.Size) && Unicode == other.Unicode;
if (CSharpType != other.CSharpType) return false;

var underlyingType = Nullable.GetUnderlyingType(CSharpType) ?? CSharpType;

// String: Compare _maxWidthForStrings and Unicode (Size is irrelevant for varchar/nvarchar)
// Note: We compare the backing field, not the Width property, because Width includes Size.ToStringLength()
if (underlyingType == typeof(string))
{
return _maxWidthForStrings == other._maxWidthForStrings && Unicode == other.Unicode;
}

// Decimal: Compare Size only (Width/Unicode are irrelevant for decimal(p,s))
if (underlyingType == typeof(decimal))
{
return Equals(Size, other.Size);
}

// byte[]: Compare _maxWidthForStrings only (Unicode/Size are irrelevant for varbinary(n))
// Note: We compare the backing field, not the Width property, because Width includes Size.ToStringLength()
if (underlyingType == typeof(byte[]))
{
return _maxWidthForStrings == other._maxWidthForStrings;
}

// All other types (bool, byte, short, int, long, float, double, DateTime, TimeSpan, Guid):
// Type alone is sufficient - these have fixed SQL storage
return true;
}

/// <inheritdoc/>
Expand All @@ -133,7 +163,26 @@ public override bool Equals(object? obj)
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(CSharpType, Width, Size, Unicode);
var underlyingType = Nullable.GetUnderlyingType(CSharpType) ?? CSharpType;

// Hash code must match Equals() logic
if (underlyingType == typeof(string))
{
return HashCode.Combine(CSharpType, _maxWidthForStrings, Unicode);
}

if (underlyingType == typeof(decimal))
{
return HashCode.Combine(CSharpType, Size);
}

if (underlyingType == typeof(byte[]))
{
return HashCode.Combine(CSharpType, _maxWidthForStrings);
}

// For all other types, only hash the type
return HashCode.Combine(CSharpType);
}

/// <summary>
Expand Down