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 @@ -35,26 +35,50 @@ public SqliteByteArrayTypeMapping(string storeType, DbType? dbType = System.Data
typeof(byte[]),
jsonValueReaderWriter: SqliteJsonByteArrayReaderWriter.Instance),
storeType,
dbType: dbType))
dbType: dbType),
false)
{
}

private SqliteByteArrayTypeMapping(
RelationalTypeMappingParameters parameters,
bool isJsonColumn)
: base(parameters)
{
_isJsonColumn = isJsonColumn;
}

private readonly bool _isJsonColumn;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected SqliteByteArrayTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}


/// <summary>
/// Creates a copy of this mapping.
/// </summary>
/// <param name="parameters">The parameters for this mapping.</param>
/// <returns>The newly created mapping.</returns>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new SqliteByteArrayTypeMapping(parameters);
=> new SqliteByteArrayTypeMapping(parameters, _isJsonColumn);

internal SqliteByteArrayTypeMapping WithJsonColumn()
=> new(Parameters, true);

/// <summary>
/// Configures the parameter, setting the <see cref="Microsoft.Data.Sqlite.SqliteParameter.SqliteType" /> to
/// <see cref="Microsoft.Data.Sqlite.SqliteType.Text" /> when the mapping is for a JSON column.
/// </summary>
/// <param name="parameter">The parameter to be configured.</param>
protected override void ConfigureParameter(DbParameter parameter)
{
if (_isJsonColumn && parameter is Data.Sqlite.SqliteParameter sqliteParameter)
{
sqliteParameter.SqliteType = Data.Sqlite.SqliteType.Text;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using Microsoft.EntityFrameworkCore.Sqlite.Storage.Json.Internal;
using Microsoft.EntityFrameworkCore.Storage.Json;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
Expand Down Expand Up @@ -33,7 +34,7 @@ public SqliteTimeOnlyTypeMapping(
DbType? dbType = System.Data.DbType.Time)
: base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(typeof(TimeOnly), jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance),
new CoreTypeMappingParameters(typeof(TimeOnly), jsonValueReaderWriter: SqliteJsonTimeOnlyReaderWriter.Instance),
storeType,
dbType: dbType))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ public static bool IsSpatialiteType(string columnType)
: mapping;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
/// <remarks>
/// Finds the type mapping for a given property. This method is overridden to special-case <c>byte[]</c>
/// properties inside JSON columns, returning a mapping that works with Base64-encoded strings.
/// </remarks>
/// <param name="property">The property for which mapping is to be found.</param>
/// <returns>The type mapping, or <see langword="null" /> if none was found.</returns>
public override RelationalTypeMapping? FindMapping(IProperty property)
{
var mapping = base.FindMapping(property);
if (mapping is SqliteByteArrayTypeMapping byteArrayMapping && property.DeclaringType.IsMappedToJson())
{
return byteArrayMapping.WithJsonColumn();
}
return mapping;
}

private RelationalTypeMapping? FindRawMapping(RelationalTypeMappingInfo mappingInfo)
{
var clrType = mappingInfo.ClrType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Json.Internal;

/// <summary>
/// The Sqlite-specific JsonValueReaderWrite for byte[]. Generates the SQLite representation (e.g. X'0102') rather than base64, in order
/// to match our SQLite non-JSON representation.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
public sealed class SqliteJsonTimeOnlyReaderWriter : JsonValueReaderWriter<TimeOnly>
{
private static readonly PropertyInfo InstanceProperty = typeof(SqliteJsonTimeOnlyReaderWriter).GetProperty(nameof(Instance))!;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static SqliteJsonTimeOnlyReaderWriter Instance { get; } = new();

private SqliteJsonTimeOnlyReaderWriter()
{
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override TimeOnly FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> TimeOnly.Parse(manager.CurrentReader.GetString()!);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override void ToJsonTyped(Utf8JsonWriter writer, TimeOnly value)
=> writer.WriteStringValue(value.Ticks % TimeSpan.TicksPerSecond == 0 ? string.Format(CultureInfo.InvariantCulture, @"{0:HH\:mm\:ss}", value)
: value.ToString("o"));

/// <inheritdoc />
public override Expression ConstructorExpression
=> Expression.Property(null, InstanceProperty);
}
31 changes: 30 additions & 1 deletion src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,17 @@ public virtual void Bind()
}
else if (type == typeof(byte[]))
{
// In Sqlite json columns parameter binding for byte[] resulted in a blob being bound to whereas the column is a text column thats why this conversion is needed.
var value1 = (byte[])value;
BindBlob(value1);
if (sqliteType == SqliteType.Text)
{
var value = ToHexString(value1);
BindText(value);
}
else
{
BindBlob(value1);
}
}
else if (type == typeof(char))
{
Expand Down Expand Up @@ -313,4 +322,24 @@ private static double GetTotalDays(int hour, int minute, int second, int millise

return iJD / 86400000.0;
}

private static string ToHexString(byte[] bytes)
{

char[] hexChars = new char[bytes.Length * 2];

for (int i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];

int highNibble = (b >> 4);
int lowNibble = (b & 0x0F);

hexChars[i * 2] = (char)(highNibble < 10 ? highNibble + '0' : highNibble + 'A' - 10);
hexChars[i * 2 + 1] = (char)(lowNibble < 10 ? lowNibble + '0' : lowNibble + 'A' - 10);
}

return new string(hexChars);
}

}
45 changes: 45 additions & 0 deletions test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,51 @@ public override Task Can_read_write_collection_of_nullable_GUID_JSON_values(stri
=> base.Can_read_write_collection_of_nullable_GUID_JSON_values(
"""{"Prop":["00000000-0000-0000-0000-000000000000",null,"8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD","FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"]}""");

public override Task Can_read_write_TimeOnly_JSON_values(string value, string json)
=> base.Can_read_write_TimeOnly_JSON_values(
value, value switch
{
"00:00:00.0000000" => """{"Prop":"00:00:00"}""",
"23:59:59.9999999" => """{"Prop":"23:59:59.9999999"}""",
"11:05:12.3456789" => """{"Prop":"11:05:12.3456789"}""",
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
});

public override Task Can_read_write_nullable_TimeOnly_JSON_values(string? value, string json)
=> base.Can_read_write_nullable_TimeOnly_JSON_values(
value, value switch
{
"00:00:00.0000000" => """{"Prop":"00:00:00"}""",
"23:59:59.9999999" => """{"Prop":"23:59:59.9999999"}""",
"11:05:12.3456789" => """{"Prop":"11:05:12.3456789"}""",
null => """{"Prop":null}""",
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
});

public override Task Can_read_write_collection_of_TimeOnly_JSON_values()
=> Can_read_and_write_JSON_value<TimeOnlyCollectionType, IReadOnlyCollection<TimeOnly>>(
nameof(TimeOnlyCollectionType.TimeOnly),
[
TimeOnly.MinValue,
new TimeOnly(11, 5, 2, 3, 4),
TimeOnly.MaxValue
],
"""{"Prop":["00:00:00","11:05:02.0030040","23:59:59.9999999"]}""",
mappedCollection: true,
new List<TimeOnly>());

public override Task Can_read_write_collection_of_nullable_TimeOnly_JSON_values()
=> Can_read_and_write_JSON_value<NullableTimeOnlyCollectionType, List<TimeOnly?>>(
nameof(NullableTimeOnlyCollectionType.TimeOnly),
[
null,
TimeOnly.MinValue,
new TimeOnly(11, 5, 2, 3, 4),
TimeOnly.MaxValue
],
"""{"Prop":[null,"00:00:00","11:05:02.0030040","23:59:59.9999999"]}""",
mappedCollection: true);

public override Task Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json)
=> Can_read_and_write_JSON_value<EnumU64Type, EnumU64>(nameof(EnumU64Type.EnumU64), value, json);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12219,9 +12219,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly?>(new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var nullableTimeOnlyArrayElementType = nullableTimeOnlyArray.SetElementType(typeof(TimeOnly?),
nullable: true);
Expand Down Expand Up @@ -13956,7 +13956,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o"))),
jsonValueReaderWriter: new JsonConvertedValueReaderWriter<string, TimeOnly>(
JsonTimeOnlyReaderWriter.Instance,
SqliteJsonTimeOnlyReaderWriter.Instance,
new ValueConverter<string, TimeOnly>(
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o")))));
Expand Down Expand Up @@ -14151,9 +14151,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly>(new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var timeOnlyArrayElementType = timeOnlyArray.SetElementType(typeof(TimeOnly));
timeOnlyArrayElementType.TypeMapping = timeOnlyArray.TypeMapping.ElementTypeMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12219,9 +12219,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly?>(new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var nullableTimeOnlyArrayElementType = nullableTimeOnlyArray.SetElementType(typeof(TimeOnly?),
nullable: true);
Expand Down Expand Up @@ -13956,7 +13956,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o"))),
jsonValueReaderWriter: new JsonConvertedValueReaderWriter<string, TimeOnly>(
JsonTimeOnlyReaderWriter.Instance,
SqliteJsonTimeOnlyReaderWriter.Instance,
new ValueConverter<string, TimeOnly>(
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o")))));
Expand Down Expand Up @@ -14151,9 +14151,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly>(new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var timeOnlyArrayElementType = timeOnlyArray.SetElementType(typeof(TimeOnly));
timeOnlyArrayElementType.TypeMapping = timeOnlyArray.TypeMapping.ElementTypeMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ public class GuidTypeFixture : SqliteTypeFixture<Guid>
public class SqliteByteArrayTypeTest(SqliteByteArrayTypeTest.ByteArrayTypeFixture fixture, ITestOutputHelper testOutputHelper)
: RelationalTypeTestBase<byte[], SqliteByteArrayTypeTest.ByteArrayTypeFixture>(fixture, testOutputHelper)
{
// TODO: string representation discrepancy between our JSON and M.D.SQLite's string representation, see #36749.
public override Task Query_property_within_json()
=> Assert.ThrowsAsync<InvalidOperationException>(() => base.Query_property_within_json());

public override Task Query_property_within_json() => base.Query_property_within_json();

public override async Task ExecuteUpdate_within_json_to_nonjson_column()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public class DateTypeFixture : SqliteTypeFixture<DateOnly>
public class SqliteTimeOnlyTypeTest(SqliteTimeOnlyTypeTest.TimeTypeFixture fixture, ITestOutputHelper testOutputHelper)
: RelationalTypeTestBase<TimeOnly, SqliteTimeOnlyTypeTest.TimeTypeFixture>(fixture, testOutputHelper)
{
// TODO: string representation discrepancy between our JSON and M.D.SQLite's string representation, see #36749.

public override Task Query_property_within_json()
=> Assert.ThrowsAsync<InvalidOperationException>(() => base.Query_property_within_json());
=> base.Query_property_within_json();

public override async Task ExecuteUpdate_within_json_to_nonjson_column()
{
Expand Down
Loading