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
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,9 @@ private static bool TryUnwrapPrefix(string s, string prefix, out string inner)
}

/// <summary>
/// Splits the inner types of a parameterized store type, respecting nested parens.
/// Splits the inner types of a parameterized store type, respecting nested parens
/// and single-quoted string literals (so commas/parens inside enum value names
/// like Enum8(',' = 1, ';' = 2) or 'a)b' do not corrupt the parse).
/// Example: Map(String, Array(Int32)) → ["String", "Array(Int32)"]
/// </summary>
private static List<string>? ExtractInnerTypes(string storeTypeName, string prefix, int? expectedCount = null)
Expand Down Expand Up @@ -783,12 +785,30 @@ private static List<string> SplitTopLevel(string input)
var results = new List<string>();
var depth = 0;
var start = 0;
var inQuote = false;

for (var i = 0; i < input.Length; i++)
{
if (input[i] == '(') depth++;
else if (input[i] == ')') depth--;
else if (input[i] == ',' && depth == 0)
var c = input[i];

if (c == '\'')
{
// ClickHouse uses doubled '' as the in-string apostrophe escape.
if (inQuote && i + 1 < input.Length && input[i + 1] == '\'')
{
i++;
continue;
}

inQuote = !inQuote;
continue;
}

if (inQuote) continue;

if (c == '(') depth++;
else if (c == ')') depth--;
else if (c == ',' && depth == 0)
{
results.Add(input[start..i].Trim());
start = i + 1;
Expand All @@ -801,15 +821,33 @@ private static List<string> SplitTopLevel(string input)
}

/// <summary>
/// Finds the matching closing paren for the opening paren at the given index.
/// Finds the matching closing paren for the opening paren at the given index,
/// ignoring parens that appear inside single-quoted string literals.
/// </summary>
private static int FindMatchingCloseParen(string s, int openParenIndex)
{
var depth = 0;
var inQuote = false;
for (var i = openParenIndex; i < s.Length; i++)
{
if (s[i] == '(') depth++;
else if (s[i] == ')')
var c = s[i];

if (c == '\'')
{
if (inQuote && i + 1 < s.Length && s[i + 1] == '\'')
{
i++;
continue;
}

inQuote = !inQuote;
continue;
}

if (inQuote) continue;

if (c == '(') depth++;
else if (c == ')')
{
depth--;
if (depth == 0)
Expand Down
18 changes: 18 additions & 0 deletions test/EFCore.ClickHouse.Tests/ExtendedTypeMappingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,24 @@ public void FindMapping_PreservesNullableDecimalWrapper()
Assert.Equal("Nullable(Decimal(18, 4))", mapping.StoreType);
}

// The store-type parser must respect single-quoted string literals when
// counting parens/commas. Without quote-awareness, a parenthesis or comma
// inside an enum value name (e.g. Enum8(',' = 1) or 'a)b') would corrupt
// FindMatchingCloseParen and the inner-types split, causing the surrounding
// parameterized type to fail to resolve.
[Theory]
[InlineData("Array(Enum8('a)b' = 1, 'c' = 2))")]
[InlineData("Array(Enum8('x(' = 1, 'y' = 2))")]
[InlineData("Array(Enum8(',' = 1, ';' = 2))")]
[InlineData("Map(String, Enum8(',' = 1, ';' = 2))")]
[InlineData("Tuple(Enum8('a)b' = 1, 'c' = 2), Int32)")]
public void FindMapping_NestedEnumWithSpecialChars_ResolvesWithoutCorruption(string storeType)
{
var source = GetTypeMappingSource();
var mapping = source.FindMapping(typeof(object), storeType);
Assert.NotNull(mapping);
}

[Theory]
[InlineData("Enum8")]
[InlineData("Enum16")]
Expand Down
Loading