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 @@ -129,6 +129,13 @@ public DecentDBTypeMappingSource(
var clrType = Nullable.GetUnderlyingType(mappingInfo.ClrType ?? typeof(object)) ?? mappingInfo.ClrType;
if (clrType != null && _clrMappings.TryGetValue(clrType, out var clrMapping))
{
// For decimal types, respect precision/scale from EF Core model configuration
// (e.g. HavePrecision, HasPrecision, or HasColumnType with precision)
if (clrType == typeof(decimal))
{
return CreateDecimalMapping(mappingInfo, mappingInfo.StoreTypeName);
}

return clrMapping;
}

Expand All @@ -138,13 +145,68 @@ public DecentDBTypeMappingSource(
var normalized = NormalizeStoreTypeName(storeType);
if (_storeMappings.TryGetValue(normalized, out var storeMapping))
{
// For DECIMAL/NUMERIC store types, respect precision/scale from store type name or mappingInfo
if (normalized is "DECIMAL" or "NUMERIC")
{
return CreateDecimalMapping(mappingInfo, mappingInfo.StoreTypeName ?? storeType);
}

return storeMapping;
}
}

return null;
}

private static DecimalTypeMapping CreateDecimalMapping(
in RelationalTypeMappingInfo mappingInfo,
string? storeTypeName)
{
const int defaultPrecision = 18;
const int defaultScale = 4;

var precision = mappingInfo.Precision;
var scale = mappingInfo.Scale;

// If precision/scale not provided by EF Core model, try parsing from store type name
if (!precision.HasValue && !scale.HasValue && !string.IsNullOrWhiteSpace(storeTypeName))
{
(precision, scale) = ParsePrecisionScale(storeTypeName);
}

var p = precision ?? defaultPrecision;
var s = scale ?? defaultScale;

return new DecimalTypeMapping($"DECIMAL({p},{s})", DbType.Decimal, precision: p, scale: s);
}

private static (int? precision, int? scale) ParsePrecisionScale(string storeTypeName)
{
var openParen = storeTypeName.IndexOf('(');
var closeParen = storeTypeName.IndexOf(')');
if (openParen < 0 || closeParen <= openParen)
{
return (null, null);
}

var inner = storeTypeName.AsSpan()[(openParen + 1)..closeParen];
var commaIdx = inner.IndexOf(',');

if (commaIdx >= 0
&& int.TryParse(inner[..commaIdx].Trim(), out var p)
&& int.TryParse(inner[(commaIdx + 1)..].Trim(), out var s))
{
return (p, s);
}

if (int.TryParse(inner.Trim(), out var pOnly))
{
return (pOnly, null);
}

return (null, null);
}

private static string NormalizeStoreTypeName(string storeTypeName)
{
var idx = storeTypeName.IndexOf('(');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
using DecentDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Xunit;

namespace DecentDB.EntityFrameworkCore.Tests;

/// <summary>
/// Verifies that the EF Core provider respects decimal precision and scale
/// from ConfigureConventions, HasPrecision, and HasColumnType.
/// </summary>
public sealed class DecimalPrecisionTests : IDisposable
{
private readonly string _tempDir = Path.Combine(Path.GetTempPath(), $"decentdb_prec_{Guid.NewGuid():N}");

public DecimalPrecisionTests()
{
Directory.CreateDirectory(_tempDir);
}

public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, true);
}
}

[Fact]
public void DefaultDecimalMapping_UsesDefaultPrecisionAndScale()
{
var dbPath = Path.Combine(_tempDir, "default.ddb");
using var context = CreateContext<DefaultDecimalContext>(dbPath);
var mappingSource = context.GetService<IRelationalTypeMappingSource>();

var mapping = (RelationalTypeMapping)mappingSource.FindMapping(typeof(decimal))!;
Assert.Equal("DECIMAL(18,4)", mapping.StoreType);
}

[Fact]
public void ConfigureConventions_HavePrecision_IsRespected()
{
var dbPath = Path.Combine(_tempDir, "conventions.ddb");
using var context = CreateContext<CustomPrecisionConventionContext>(dbPath);

context.Database.EnsureCreated();

context.Items.Add(new PrecisionItem { Value = 123.456789m });
context.SaveChanges();

var item = context.Items.First();
Assert.Equal(123.456789m, item.Value);
}

[Fact]
public void HasPrecision_OnProperty_IsRespected()
{
var dbPath = Path.Combine(_tempDir, "hasprecision.ddb");
using var context = CreateContext<PropertyPrecisionContext>(dbPath);

context.Database.EnsureCreated();

context.Items.Add(new PrecisionItem { Value = 99.12m });
context.SaveChanges();

var item = context.Items.First();
Assert.Equal(99.12m, item.Value);
}

[Fact]
public void HasColumnType_WithPrecisionScale_IsRespected()
{
var dbPath = Path.Combine(_tempDir, "columntype.ddb");
using var context = CreateContext<ColumnTypeContext>(dbPath);

context.Database.EnsureCreated();

context.Items.Add(new PrecisionItem { Value = 12345.67m });
context.SaveChanges();

var item = context.Items.First();
Assert.Equal(12345.67m, item.Value);
}

[Fact]
public void NullableDecimal_WithPrecision_IsRespected()
{
var dbPath = Path.Combine(_tempDir, "nullable.ddb");
using var context = CreateContext<NullableDecimalContext>(dbPath);

context.Database.EnsureCreated();

context.Items.Add(new NullableDecimalItem { Value = 42.123456m });
context.Items.Add(new NullableDecimalItem { Value = null });
context.SaveChanges();

var items = context.Items.OrderBy(i => i.Id).ToList();
Assert.Equal(42.123456m, items[0].Value);
Assert.Null(items[1].Value);
}

[Fact]
public void MultipleDecimalProperties_WithDifferentPrecisions()
{
var dbPath = Path.Combine(_tempDir, "multi.ddb");
using var context = CreateContext<MultiPrecisionContext>(dbPath);

context.Database.EnsureCreated();

context.Items.Add(new MultiDecimalItem
{
Price = 99.99m,
TaxRate = 0.0825m,
Weight = 1234.5m
});
context.SaveChanges();

var item = context.Items.First();
Assert.Equal(99.99m, item.Price);
Assert.Equal(0.0825m, item.TaxRate);
Assert.Equal(1234.5m, item.Weight);
}

private static TContext CreateContext<TContext>(string dbPath) where TContext : DbContext
{
var options = new DbContextOptionsBuilder<TContext>()
.UseDecentDB($"Data Source={dbPath}")
.Options;
return (TContext)Activator.CreateInstance(typeof(TContext), options)!;
}

#region Test entities and contexts

public class PrecisionItem
{
public int Id { get; set; }
public decimal Value { get; set; }
}

public class NullableDecimalItem
{
public int Id { get; set; }
public decimal? Value { get; set; }
}

public class MultiDecimalItem
{
public int Id { get; set; }
public decimal Price { get; set; }
public decimal TaxRate { get; set; }
public decimal Weight { get; set; }
}

private sealed class DefaultDecimalContext : DbContext
{
public DefaultDecimalContext(DbContextOptions<DefaultDecimalContext> options) : base(options) { }
public DbSet<PrecisionItem> Items { get; set; } = null!;
}

private sealed class CustomPrecisionConventionContext : DbContext
{
public CustomPrecisionConventionContext(DbContextOptions<CustomPrecisionConventionContext> options) : base(options) { }
public DbSet<PrecisionItem> Items { get; set; } = null!;

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<decimal>().HavePrecision(18, 6);
}
}

private sealed class PropertyPrecisionContext : DbContext
{
public PropertyPrecisionContext(DbContextOptions<PropertyPrecisionContext> options) : base(options) { }
public DbSet<PrecisionItem> Items { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PrecisionItem>().Property(e => e.Value).HasPrecision(10, 2);
}
}

private sealed class ColumnTypeContext : DbContext
{
public ColumnTypeContext(DbContextOptions<ColumnTypeContext> options) : base(options) { }
public DbSet<PrecisionItem> Items { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PrecisionItem>().Property(e => e.Value).HasColumnType("DECIMAL(10,2)");
}
}

private sealed class NullableDecimalContext : DbContext
{
public NullableDecimalContext(DbContextOptions<NullableDecimalContext> options) : base(options) { }
public DbSet<NullableDecimalItem> Items { get; set; } = null!;

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<decimal>().HavePrecision(18, 6);
}
}

private sealed class MultiPrecisionContext : DbContext
{
public MultiPrecisionContext(DbContextOptions<MultiPrecisionContext> options) : base(options) { }
public DbSet<MultiDecimalItem> Items { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MultiDecimalItem>().Property(e => e.Price).HasPrecision(10, 2);
modelBuilder.Entity<MultiDecimalItem>().Property(e => e.TaxRate).HasPrecision(8, 4);
modelBuilder.Entity<MultiDecimalItem>().Property(e => e.Weight).HasPrecision(12, 1);
}
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using DecentDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Xunit;

namespace DecentDB.EntityFrameworkCore.Tests;

/// <summary>
/// Verifies EF.Functions.Like translation for DecentDB.
/// EF Core's base RelationalMethodCallTranslatorProvider should translate
/// EF.Functions.Like to SQL LIKE. This test confirms it works with DecentDB.
/// </summary>
public sealed class EfFunctionsLikeTests : IDisposable
{
private readonly string _tempDir = Path.Combine(Path.GetTempPath(), $"decentdb_like_{Guid.NewGuid():N}");

public EfFunctionsLikeTests()
{
Directory.CreateDirectory(_tempDir);
}

public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, true);
}

[Fact]
public void EfFunctionsLike_TranslatesToSqlLike()
{
var dbPath = Path.Combine(_tempDir, "like.ddb");
using var context = CreateContext(dbPath);
context.Database.EnsureCreated();

context.Items.AddRange(
new LikeTestItem { Name = "Alice" },
new LikeTestItem { Name = "Bob" },
new LikeTestItem { Name = "Charlie" });
context.SaveChanges();

var results = context.Items.Where(i => EF.Functions.Like(i.Name, "%li%")).ToList();
Assert.Equal(2, results.Count); // Alice, Charlie
}

private static LikeTestContext CreateContext(string dbPath)
{
var options = new DbContextOptionsBuilder<LikeTestContext>()
.UseDecentDB($"Data Source={dbPath}")
.Options;
return new LikeTestContext(options);
}

public class LikeTestItem
{
public int Id { get; set; }
public string Name { get; set; } = "";
}

private sealed class LikeTestContext : DbContext
{
public LikeTestContext(DbContextOptions<LikeTestContext> options) : base(options) { }
public DbSet<LikeTestItem> Items { get; set; } = null!;
}
}
Loading
Loading