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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public class PageView
| **Arrays** | `Array(T)` | `T[]` or `List<T>` |
| **Maps** | `Map(K, V)` | `Dictionary<K,V>` |
| **Tuples** | `Tuple(T1, ...)` | `Tuple<...>` or `ValueTuple<...>` |
| **Variant** | `Variant(T1, T2, ...)` | `object` |
| **Dynamic** | `Dynamic` | `object` |
| **Wrappers** | `Nullable(T)`, `LowCardinality(T)` | Unwrapped automatically |

## Current Status
Expand Down Expand Up @@ -138,7 +140,7 @@ This calls `InsertBinaryAsync` directly, bypassing EF Core's change tracker enti
- UPDATE / DELETE (ClickHouse mutations are async, not OLTP-compatible)
- Migrations
- JOINs, subqueries, set operations
- Nested type, Variant, Dynamic, JSON, Geo types
- Nested type, JSON, Geo types

## Building

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,11 @@ public ClickHouseTypeMappingSource(
return baseName;
}

// Array(...), Map(...), Tuple(...) — return base name, inner parsing in FindMapping
// Array(...), Map(...), Tuple(...), Variant(...) — return base name, inner parsing in FindMapping
if (string.Equals(baseName, "Array", StringComparison.OrdinalIgnoreCase)
|| string.Equals(baseName, "Map", StringComparison.OrdinalIgnoreCase)
|| string.Equals(baseName, "Tuple", StringComparison.OrdinalIgnoreCase))
|| string.Equals(baseName, "Tuple", StringComparison.OrdinalIgnoreCase)
|| string.Equals(baseName, "Variant", StringComparison.OrdinalIgnoreCase))
{
return baseName;
}
Expand All @@ -225,6 +226,8 @@ public ClickHouseTypeMappingSource(
?? FindArrayMapping(mappingInfo)
?? FindMapMapping(mappingInfo)
?? FindTupleMapping(mappingInfo)
?? FindVariantMapping(mappingInfo)
?? FindDynamicMapping(mappingInfo)
?? FindEnumMapping(mappingInfo)
?? FindExistingMapping(mappingInfo)
?? FindDecimalMapping(mappingInfo);
Expand Down Expand Up @@ -409,6 +412,39 @@ public ClickHouseTypeMappingSource(
return null;
}

private RelationalTypeMapping? FindVariantMapping(in RelationalTypeMappingInfo mappingInfo)
{
if (!string.Equals(mappingInfo.StoreTypeNameBase, "Variant", StringComparison.OrdinalIgnoreCase))
return null;

var storeTypeName = mappingInfo.StoreTypeName;
if (storeTypeName is null)
return null;

var innerTypes = ExtractInnerTypes(storeTypeName, "Variant");
if (innerTypes is null || innerTypes.Count == 0)
return null;

var elementMappings = new List<RelationalTypeMapping>();
foreach (var innerType in innerTypes)
{
var mapping = FindMapping(innerType);
if (mapping is null)
return null;
elementMappings.Add(mapping);
}

return new ClickHouseVariantTypeMapping(elementMappings);
}

private RelationalTypeMapping? FindDynamicMapping(in RelationalTypeMappingInfo mappingInfo)
{
if (!string.Equals(mappingInfo.StoreTypeNameBase, "Dynamic", StringComparison.OrdinalIgnoreCase))
return null;

return new ClickHouseDynamicTypeMapping(this);
}

private static bool IsReferenceTuple(Type? type)
=> type is not null && type.IsGenericType && type.FullName?.StartsWith("System.Tuple`") == true;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Data.Common;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage;

namespace ClickHouse.EntityFrameworkCore.Storage.Internal.Mapping;

public class ClickHouseDynamicTypeMapping : RelationalTypeMapping
{
private static readonly MethodInfo GetValueMethod =
typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetValue), [typeof(int)])!;

private readonly IRelationalTypeMappingSource? _typeMappingSource;

public ClickHouseDynamicTypeMapping(IRelationalTypeMappingSource? typeMappingSource = null)
: base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(
typeof(object),
comparer: new ValueComparer<object?>(
(a, b) => Equals(a, b),
o => o == null ? 0 : o.GetHashCode(),
source => source)),
"Dynamic",
dbType: System.Data.DbType.Object))
{
_typeMappingSource = typeMappingSource;
}

protected ClickHouseDynamicTypeMapping(
RelationalTypeMappingParameters parameters,
IRelationalTypeMappingSource? typeMappingSource)
: base(parameters)
{
_typeMappingSource = typeMappingSource;
}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new ClickHouseDynamicTypeMapping(parameters, _typeMappingSource);

public override MethodInfo GetDataReaderMethod()
=> GetValueMethod;

public override Expression CustomizeDataReaderExpression(Expression expression)
=> expression;

protected override string GenerateNonNullSqlLiteral(object value)
{
if (_typeMappingSource is null)
throw new InvalidOperationException(
"Cannot generate SQL literal for Dynamic type without a type mapping source.");

var valueType = value.GetType();
var mapping = _typeMappingSource.FindMapping(valueType);

if (mapping is null)
throw new InvalidOperationException(
$"Cannot generate SQL literal for Dynamic column: no type mapping found for CLR type '{valueType.Name}'. " +
"Binary INSERT via SaveChanges works correctly; SQL literal generation is a known limitation for unmapped types.");

return mapping.GenerateSqlLiteral(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Data.Common;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage;

namespace ClickHouse.EntityFrameworkCore.Storage.Internal.Mapping;

public class ClickHouseVariantTypeMapping : RelationalTypeMapping
{
private static readonly MethodInfo GetValueMethod =
typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetValue), [typeof(int)])!;

public IReadOnlyList<RelationalTypeMapping> ElementMappings { get; }

public ClickHouseVariantTypeMapping(IReadOnlyList<RelationalTypeMapping> elementMappings)
: base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(
typeof(object),
comparer: new ValueComparer<object?>(
(a, b) => Equals(a, b),
o => o == null ? 0 : o.GetHashCode(),
source => source)),
FormatStoreType(elementMappings),
dbType: System.Data.DbType.Object))
{
ElementMappings = elementMappings;
}

protected ClickHouseVariantTypeMapping(
RelationalTypeMappingParameters parameters,
IReadOnlyList<RelationalTypeMapping> elementMappings)
: base(parameters)
{
ElementMappings = elementMappings;
}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new ClickHouseVariantTypeMapping(parameters, ElementMappings);

public override MethodInfo GetDataReaderMethod()
=> GetValueMethod;

public override Expression CustomizeDataReaderExpression(Expression expression)
=> expression;

protected override string GenerateNonNullSqlLiteral(object value)
{
var valueType = value.GetType();

// Find element mapping matching the value's CLR type
foreach (var mapping in ElementMappings)
{
if (mapping.ClrType == valueType)
return $"{mapping.GenerateSqlLiteral(value)}::{mapping.StoreType}";
}

// Fallback: IsAssignableFrom
foreach (var mapping in ElementMappings)
{
if (mapping.ClrType.IsAssignableFrom(valueType))
return $"{mapping.GenerateSqlLiteral(value)}::{mapping.StoreType}";
}

throw new InvalidOperationException(
$"No element mapping found for CLR type '{valueType.Name}' in Variant({string.Join(", ", ElementMappings.Select(m => m.StoreType))}).");
}

private static string FormatStoreType(IReadOnlyList<RelationalTypeMapping> elementMappings)
=> $"Variant({string.Join(", ", elementMappings.Select(m => m.StoreType))})";
}
Loading
Loading