Skip to content

Commit 011f0ec

Browse files
authored
Use SQLite native representations in JSON (#31490)
Closes #30727
1 parent 22b984a commit 011f0ec

21 files changed

+763
-137
lines changed

src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs

Lines changed: 0 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,6 @@
216216
<data name="OrderByNotSupported" xml:space="preserve">
217217
<value>SQLite does not support expressions of type '{type}' in ORDER BY clauses. Convert the values to a supported type, or use LINQ to Objects to order the results on the client side.</value>
218218
</data>
219-
<data name="QueryingJsonCollectionOfGivenTypeNotSupported" xml:space="preserve">
220-
<value>Querying JSON collections with element provider type '{type}' isn't supported because of SQLite limitations.</value>
221-
</data>
222219
<data name="SequencesNotSupported" xml:space="preserve">
223220
<value>SQLite does not support sequences. See https://go.microsoft.com/fwlink/?LinkId=723262 for more information and examples.</value>
224221
</data>

src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -671,54 +671,8 @@ private static SqlExpression ApplyJsonSqlConversion(
671671
bool isNullable)
672672
=> typeMapping switch
673673
{
674-
// The "default" JSON representation of a GUID is a lower-case string, but we do upper-case GUIDs in our non-JSON
675-
// implementation.
676-
SqliteGuidTypeMapping
677-
=> sqlExpressionFactory.Function("upper", new[] { expression }, isNullable, new[] { true }, typeof(Guid), typeMapping),
678-
679-
// The "standard" JSON timestamp representation is ISO8601, with a T between date and time; but SQLite's representation has
680-
// no T. The following performs a reliable conversions on the string values coming out of json_each.
681-
// Unfortunately, the SQLite datetime() function doesn't present fractional seconds, so we generate the following lovely thing:
682-
// rtrim(rtrim(strftime('%Y-%m-%d %H:%M:%f', $value), '0'), '.')
683-
SqliteDateTimeTypeMapping
684-
=> sqlExpressionFactory.Function(
685-
"rtrim",
686-
new SqlExpression[]
687-
{
688-
sqlExpressionFactory.Function(
689-
"rtrim",
690-
new SqlExpression[]
691-
{
692-
sqlExpressionFactory.Function(
693-
"strftime",
694-
new[]
695-
{
696-
sqlExpressionFactory.Constant("%Y-%m-%d %H:%M:%f"),
697-
expression
698-
},
699-
isNullable, new[] { true }, typeof(DateTime), typeMapping),
700-
sqlExpressionFactory.Constant("0")
701-
},
702-
isNullable, new[] { true }, typeof(DateTime), typeMapping),
703-
sqlExpressionFactory.Constant(".")
704-
},
705-
isNullable, new[] { true }, typeof(DateTime), typeMapping),
706-
707-
// The JSON representation for decimal is e.g. 1 (JSON int), whereas our literal representation is "1.0" (string).
708-
// We can cast the 1 to TEXT, but we'd still get "1" not "1.0".
709-
SqliteDecimalTypeMapping
710-
=> throw new InvalidOperationException(SqliteStrings.QueryingJsonCollectionOfGivenTypeNotSupported("decimal")),
711-
712-
// The JSON representation for new[] { 1, 2 } is AQI= (base64), and SQLite has no built-in base64 conversion function.
713674
ByteArrayTypeMapping
714-
=> throw new InvalidOperationException(SqliteStrings.QueryingJsonCollectionOfGivenTypeNotSupported("byte[]")),
715-
716-
// The JSON representation for DateTimeOffset is ISO8601 (2023-01-01T12:30:00+02:00), but our SQL literal representation
717-
// is 2023-01-01 12:30:00+02:00 (no T).
718-
// Note that datetime('2023-01-01T12:30:00+02:00') yields '2023-01-01 10:30:00', converting to UTC (removing the timezone), so
719-
// we can't use that.
720-
SqliteDateTimeOffsetTypeMapping
721-
=> throw new InvalidOperationException(SqliteStrings.QueryingJsonCollectionOfGivenTypeNotSupported("DateTimeOffset")),
675+
=> sqlExpressionFactory.Function("unhex", new[] { expression }, isNullable, new[] { true }, typeof(byte[]), typeMapping),
722676

723677
_ => expression
724678
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using System.Text;
6+
using System.Text.Encodings.Web;
7+
using System.Text.Json;
8+
using Microsoft.EntityFrameworkCore.Storage.Json;
9+
10+
namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal.Json;
11+
12+
/// <summary>
13+
/// The Sqlite-specific JsonValueReaderWrite for byte[]. Generates the SQLite representation (e.g. X'0102') rather than base64, in order
14+
/// to match our SQLite non-JSON representation.
15+
/// </summary>
16+
/// <remarks>
17+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
18+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
19+
/// any release. You should only use it directly in your code with extreme caution and knowing that
20+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
21+
/// </remarks>
22+
public sealed class SqliteJsonByteArrayReaderWriter : JsonValueReaderWriter<byte[]>
23+
{
24+
/// <summary>
25+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
26+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
27+
/// any release. You should only use it directly in your code with extreme caution and knowing that
28+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
29+
/// </summary>
30+
public static SqliteJsonByteArrayReaderWriter Instance { get; } = new();
31+
32+
private SqliteJsonByteArrayReaderWriter()
33+
{
34+
}
35+
36+
/// <summary>
37+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
38+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
39+
/// any release. You should only use it directly in your code with extreme caution and knowing that
40+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
41+
/// </summary>
42+
public override byte[] FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
43+
=> Convert.FromHexString(manager.CurrentReader.GetString()!);
44+
45+
/// <summary>
46+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
47+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
48+
/// any release. You should only use it directly in your code with extreme caution and knowing that
49+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
50+
/// </summary>
51+
public override void ToJsonTyped(Utf8JsonWriter writer, byte[] value)
52+
=> writer.WriteStringValue(Convert.ToHexString(value));
53+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using System.Text.Encodings.Web;
6+
using System.Text.Json;
7+
using Microsoft.EntityFrameworkCore.Storage.Json;
8+
9+
namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal.Json;
10+
11+
/// <summary>
12+
/// The Sqlite-specific JsonValueReaderWrite for DateTime. Generates a ISO8601 string representation with a space instead of a T
13+
/// separating the date and time components, in order to match our SQLite non-JSON representation.
14+
/// </summary>
15+
/// <remarks>
16+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
17+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
18+
/// any release. You should only use it directly in your code with extreme caution and knowing that
19+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
20+
/// </remarks>
21+
public sealed class SqliteJsonDateTimeOffsetReaderWriter : JsonValueReaderWriter<DateTimeOffset>
22+
{
23+
private const string DateTimeOffsetFormatConst = @"{0:yyyy\-MM\-dd HH\:mm\:ss.FFFFFFFzzz}";
24+
25+
/// <summary>
26+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
27+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
28+
/// any release. You should only use it directly in your code with extreme caution and knowing that
29+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
30+
/// </summary>
31+
public static SqliteJsonDateTimeOffsetReaderWriter Instance { get; } = new();
32+
33+
private SqliteJsonDateTimeOffsetReaderWriter()
34+
{
35+
}
36+
37+
/// <summary>
38+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
39+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
40+
/// any release. You should only use it directly in your code with extreme caution and knowing that
41+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
42+
/// </summary>
43+
public override DateTimeOffset FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
44+
// => manager.CurrentReader.GetDateTimeOffset();
45+
=> DateTimeOffset.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture);
46+
47+
/// <summary>
48+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
49+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
50+
/// any release. You should only use it directly in your code with extreme caution and knowing that
51+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
52+
/// </summary>
53+
public override void ToJsonTyped(Utf8JsonWriter writer, DateTimeOffset value)
54+
// We use UnsafeRelaxedJsonEscaping to prevent the DateTimeOffset plus (+) sign from getting escaped
55+
=> writer.WriteStringValue(
56+
JsonEncodedText.Encode(
57+
string.Format(CultureInfo.InvariantCulture, DateTimeOffsetFormatConst, value),
58+
JavaScriptEncoder.UnsafeRelaxedJsonEscaping));
59+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using System.Text.Json;
6+
using Microsoft.EntityFrameworkCore.Storage.Json;
7+
8+
namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal.Json;
9+
10+
/// <summary>
11+
/// The Sqlite-specific JsonValueReaderWrite for DateTime. Generates a ISO8601 string representation with a space instead of a T
12+
/// separating the date and time components, in order to match our SQLite non-JSON representation.
13+
/// </summary>
14+
/// <remarks>
15+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
16+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
17+
/// any release. You should only use it directly in your code with extreme caution and knowing that
18+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
19+
/// </remarks>
20+
public sealed class SqliteJsonDateTimeReaderWriter : JsonValueReaderWriter<DateTime>
21+
{
22+
private const string DateTimeFormatConst = @"{0:yyyy\-MM\-dd HH\:mm\:ss.FFFFFFF}";
23+
24+
/// <summary>
25+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
26+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
27+
/// any release. You should only use it directly in your code with extreme caution and knowing that
28+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
29+
/// </summary>
30+
public static SqliteJsonDateTimeReaderWriter Instance { get; } = new();
31+
32+
private SqliteJsonDateTimeReaderWriter()
33+
{
34+
}
35+
36+
/// <summary>
37+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
38+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
39+
/// any release. You should only use it directly in your code with extreme caution and knowing that
40+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
41+
/// </summary>
42+
public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
43+
=> DateTime.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture);
44+
45+
/// <summary>
46+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
47+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
48+
/// any release. You should only use it directly in your code with extreme caution and knowing that
49+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
50+
/// </summary>
51+
public override void ToJsonTyped(Utf8JsonWriter writer, DateTime value)
52+
=> writer.WriteStringValue(string.Format(CultureInfo.InvariantCulture, DateTimeFormatConst, value));
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using System.Text.Json;
6+
using Microsoft.EntityFrameworkCore.Storage.Json;
7+
8+
namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal.Json;
9+
10+
/// <summary>
11+
/// The Sqlite-specific JsonValueReaderWrite for decimal. Generates a string representation instead of a JSON number, in order to match
12+
/// our SQLite non-JSON representation.
13+
/// </summary>
14+
/// <remarks>
15+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
16+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
17+
/// any release. You should only use it directly in your code with extreme caution and knowing that
18+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
19+
/// </remarks>
20+
public sealed class SqliteJsonDecimalReaderWriter : JsonValueReaderWriter<decimal>
21+
{
22+
private const string DecimalFormatConst = "{0:0.0###########################}";
23+
24+
/// <summary>
25+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
26+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
27+
/// any release. You should only use it directly in your code with extreme caution and knowing that
28+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
29+
/// </summary>
30+
public static SqliteJsonDecimalReaderWriter Instance { get; } = new();
31+
32+
private SqliteJsonDecimalReaderWriter()
33+
{
34+
}
35+
36+
/// <summary>
37+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
38+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
39+
/// any release. You should only use it directly in your code with extreme caution and knowing that
40+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
41+
/// </summary>
42+
public override decimal FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
43+
=> decimal.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture);
44+
45+
/// <summary>
46+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
47+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
48+
/// any release. You should only use it directly in your code with extreme caution and knowing that
49+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
50+
/// </summary>
51+
public override void ToJsonTyped(Utf8JsonWriter writer, decimal value)
52+
=> writer.WriteStringValue(string.Format(CultureInfo.InvariantCulture, DecimalFormatConst, value));
53+
}

0 commit comments

Comments
 (0)