diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDictionaryDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDictionaryDbFunctionsExtensions.cs
new file mode 100644
index 000000000..6509ebab3
--- /dev/null
+++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDictionaryDbFunctionsExtensions.cs
@@ -0,0 +1,126 @@
+// ReSharper disable once CheckNamespace
+
+namespace Microsoft.EntityFrameworkCore;
+
+///
+/// Provides extension methods supporting `Dictionary<TKey,TValue>` and `ImmutableDictionary<TKey,TValue>` function translation for PostgreSQL for the `hstore`, `json` and `jsonb` store types.
+///
+public static class NpgsqlDictionaryDbFunctionsExtensions
+{
+ ///
+ /// Deletes keys from input operand Dictionary. Returns the same store type as the provided input for `hstore` and `jsonb` columns.
+ ///
+ /// Works with `hstore`, `json` and `jsonb` type columns
+ ///
+ /// SQL translation: input - key
+ ///
+ /// Note: for `json` type columns, input will be cast to `jsonb` and will output `jsonb` which requires PostgreSQL 9.3
+ ///
+ /// The instance.
+ /// The input dictionary.
+ /// The key to remove.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static T Remove(this DbFunctions _, T input, string key)
+ where T : IEnumerable>
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Remove)));
+
+ ///
+ /// Converts string dictionary to an array of alternating keys and values.
+ ///
+ /// HStore SQL translation: hstore_to_array(input)
+ ///
+ /// The instance.
+ /// The input hstore.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static List ToKeyValueList(this DbFunctions _, IEnumerable> input)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToKeyValueList)));
+
+ ///
+ /// Constructs an hstore `Dictionary<string, string>` from a key/value pair string array
+ ///
+ /// SQL translation: hstore(input)
+ ///
+ /// The instance.
+ /// The input string array of key value pairs.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static Dictionary DictionaryFromKeyValueList(this DbFunctions _, IList input)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DictionaryFromKeyValueList)));
+
+ ///
+ /// Constructs an hstore `Dictionary<string, string>` from a string array of keys and a string array of values
+ ///
+ /// SQL translation: hstore(keys, values)
+ ///
+ /// The instance.
+ /// The input string array of keys.
+ /// The input string array of values.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static Dictionary DictionaryFromKeysAndValues(this DbFunctions _, IList keys, IList values)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DictionaryFromKeysAndValues)));
+
+ ///
+ /// Converts an `hstore` to a `json` value type `Dictionary<string, object?>`, but attempts to distinguish numerical and boolean values so they are unquoted in the JSON.
+ ///
+ /// SQL translation: hstore_to_json_loose(input)
+ ///
+ /// The instance.
+ /// The input hstore.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static Dictionary ToJsonLoose(this DbFunctions _, IEnumerable> input)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToJsonLoose)));
+
+ ///
+ /// Converts an `hstore` to a `jsonb` value type `Dictionary<string, object?>`, but attempts to distinguish numerical and boolean values so they are unquoted in the JSON.
+ ///
+ /// SQL translation: hstore_to_jsonb_loose(input)
+ ///
+ /// The instance.
+ /// The input hstore.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static Dictionary ToJsonbLoose(this DbFunctions _, IEnumerable> input)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToJsonbLoose)));
+
+ ///
+ /// Converts a `json` or `jsonb` type `IEnumerable<KeyValuePair<string, string>>` (i.e. Dictionary<string, string>> or related type) to an hstore type `Dictionary<string, string>>`
+ ///
+ /// Can be used during a migration of changing a column's StoreType from 'json' to 'hstore' with the `Using` clause
+ ///
+ /// HStore SQL translation: input
+ /// Json SQL translation: select hstore(array_agg(key), array_agg(value)) FROM json_each_text(input)
+ /// Json SQL translation: select hstore(array_agg(key), array_agg(value)) FROM jsonb_each_text(input)
+ ///
+ /// The instance.
+ /// The input hstore.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static Dictionary ToHstore(this DbFunctions _, IEnumerable> input)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToHstore)));
+
+ ///
+ /// Converts an `jsonb` or `hstore` type value to a `jsonb` type `Dictionary<TKey, TKey>`
+ ///
+ ///
+ /// Hstore SQL translation: hstore_to_json(input)
+ /// Json SQL translation: input
+ /// Jsonb SQL translation: input::json
+ ///
+ /// The instance.
+ /// The input hstore.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static Dictionary ToJson(this DbFunctions _, IEnumerable> input)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToJson)));
+
+ ///
+ /// Converts an `hstore` or `json` type value to a `jsonb` type `Dictionary<TKey, TValue>`
+ ///
+ /// Can be used during a migration of changing a column's StoreType from 'hstore' to 'jsonb' with the `Using` clause
+ ///
+ /// HStore SQL translation: hstore_to_jsonb(input)
+ /// Json SQL translation: input::jsonb
+ /// Jsonb SQL translation: input
+ ///
+ /// The instance.
+ /// The input hstore.
+ /// PostgreSQL documentation for 'hstore' functions.
+ public static Dictionary ToJsonb(this DbFunctions _, IEnumerable> input)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToJsonb)));
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDictionaryTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDictionaryTranslator.cs
new file mode 100644
index 000000000..8a6347ce4
--- /dev/null
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDictionaryTranslator.cs
@@ -0,0 +1,883 @@
+using System.Collections.Immutable;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
+using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
+
+///
+/// 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.
+///
+public class DictionaryTranslator : IMethodCallTranslator, IMemberTranslator
+{
+ #region Types
+
+ private static readonly Type StringDictionaryType = typeof(Dictionary);
+ private static readonly Type ImmutableStringDictionaryType = typeof(ImmutableDictionary);
+ private static readonly Type ExtensionsType = typeof(NpgsqlDictionaryDbFunctionsExtensions);
+ private static readonly Type GenericKvpType = typeof(KeyValuePair<,>).MakeGenericType(
+ Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(1));
+ private static readonly Type EnumerableType = typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0));
+ private static readonly Type GenericListType = typeof(List<>);
+ private static readonly Type StringType = typeof(string);
+ private static readonly Type BoolType = typeof(string);
+ private static readonly Type StringListType = typeof(List);
+
+
+ #endregion
+
+ #region MethodInfo(s)
+
+ private static readonly MethodInfo Enumerable_Any =
+ typeof(Enumerable).GetMethod(nameof(Enumerable.Any), BindingFlags.Public | BindingFlags.Static, [EnumerableType])!;
+
+ private static readonly MethodInfo Enumerable_Count =
+ typeof(Enumerable).GetMethod(nameof(Enumerable.Count), BindingFlags.Public | BindingFlags.Static, [EnumerableType])!;
+
+ private static readonly MethodInfo Enumerable_ToList =
+ typeof(Enumerable).GetMethod(nameof(Enumerable.ToList), BindingFlags.Public | BindingFlags.Static, [EnumerableType])!;
+
+ private static readonly MethodInfo Enumerable_ToDictionary =
+ typeof(Enumerable).GetMethod(
+ nameof(Enumerable.ToDictionary), BindingFlags.Public | BindingFlags.Static,
+ [typeof(IEnumerable<>).MakeGenericType(GenericKvpType)])!;
+
+ private static readonly MethodInfo GenericImmutableDictionary_ToImmutableDictionary =
+ typeof(ImmutableDictionary).GetMethod(
+ nameof(ImmutableDictionary.ToImmutableDictionary), BindingFlags.Public | BindingFlags.Static,
+ [typeof(IEnumerable<>).MakeGenericType(GenericKvpType)])!;
+
+ private static readonly MethodInfo Enumerable_Concat = typeof(Enumerable).GetMethod(
+ nameof(Enumerable.Concat), BindingFlags.Public | BindingFlags.Static,
+ [EnumerableType, EnumerableType])!;
+
+ private static readonly MethodInfo Enumerable_Except = typeof(Enumerable).GetMethod(
+ nameof(Enumerable.Except), BindingFlags.Public | BindingFlags.Static,
+ [EnumerableType, EnumerableType])!;
+
+ private static readonly MethodInfo Enumerable_SequenceEqual = typeof(Enumerable).GetMethod(
+ nameof(Enumerable.SequenceEqual), BindingFlags.Public | BindingFlags.Static,
+ [EnumerableType, EnumerableType])!;
+
+ #endregion
+
+ #region Extension MethodInfo(s)
+
+ private static readonly MethodInfo Extension_ToHstore =
+ ExtensionsType.GetMethod(nameof(NpgsqlDictionaryDbFunctionsExtensions.ToHstore))!;
+
+ private static readonly MethodInfo Extension_ToJson =
+ ExtensionsType.GetMethod(nameof(NpgsqlDictionaryDbFunctionsExtensions.ToJson))!;
+
+ private static readonly MethodInfo Extension_ToJsonb =
+ ExtensionsType.GetMethod(nameof(NpgsqlDictionaryDbFunctionsExtensions.ToJsonb))!;
+
+ private static readonly MethodInfo Extension_ToJsonLoose =
+ ExtensionsType.GetMethod(nameof(NpgsqlDictionaryDbFunctionsExtensions.ToJsonLoose))!;
+
+ private static readonly MethodInfo Extension_ToJsonbLoose =
+ ExtensionsType.GetMethod(nameof(NpgsqlDictionaryDbFunctionsExtensions.ToJsonbLoose))!;
+
+ private static readonly MethodInfo Extension_Remove =
+ ExtensionsType.GetMethod(nameof(NpgsqlDictionaryDbFunctionsExtensions.Remove))!;
+
+ private static readonly MethodInfo Extension_ToKeysAndValues =
+ ExtensionsType.GetMethod(nameof(NpgsqlDictionaryDbFunctionsExtensions.ToKeyValueList))!;
+
+ private static readonly MethodInfo Extension_FromKeysAndValues_List =
+ ExtensionsType.GetMethod(
+ nameof(NpgsqlDictionaryDbFunctionsExtensions.DictionaryFromKeyValueList), BindingFlags.Public | BindingFlags.Static,
+ null, [typeof(DbFunctions), typeof(IList)], null)!;
+
+ private static readonly MethodInfo Extension_FromKeysAndValues_List_List =
+ ExtensionsType.GetMethod(
+ nameof(NpgsqlDictionaryDbFunctionsExtensions.DictionaryFromKeyValueList), BindingFlags.Public | BindingFlags.Static,
+ null, [typeof(DbFunctions), typeof(IList), typeof(IList)], null)!;
+
+ #endregion
+
+ #region Fields
+
+ private readonly RelationalTypeMapping _stringListTypeMapping;
+ private readonly RelationalTypeMapping _stringTypeMapping;
+ private readonly RelationalTypeMapping _stringDictionaryMapping;
+ private readonly RelationalTypeMapping _jsonTypeMapping;
+ private readonly RelationalTypeMapping _jsonbTypeMapping;
+ private readonly RelationalTypeMapping _immutableStringDictionaryMapping;
+ private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly IModel _model;
+
+ #endregion
+
+ ///
+ /// 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.
+ ///
+ public DictionaryTranslator(
+ IRelationalTypeMappingSource typeMappingSource,
+ NpgsqlSqlExpressionFactory sqlExpressionFactory,
+ IModel model)
+ {
+ _typeMappingSource = typeMappingSource;
+ _model = model;
+ _sqlExpressionFactory = sqlExpressionFactory;
+ _stringListTypeMapping = typeMappingSource.FindMapping(StringListType, model)!;
+ _stringTypeMapping = typeMappingSource.FindMapping(StringType, model)!;
+ _stringDictionaryMapping = typeMappingSource.FindMapping(StringDictionaryType, model)!;
+ _immutableStringDictionaryMapping = typeMappingSource.FindMapping(ImmutableStringDictionaryType, model)!;
+ _jsonTypeMapping = typeMappingSource.FindMapping("json")!;
+ _jsonbTypeMapping = typeMappingSource.FindMapping("jsonb")!;
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Translate(
+ SqlExpression? instance,
+ MethodInfo method,
+ IReadOnlyList arguments,
+ IDiagnosticsLogger logger)
+ {
+ if (instance is null)
+ {
+ if (arguments.Count is 3)
+ {
+ if (!IsDictionaryType(arguments[1].Type))
+ {
+ return null;
+ }
+
+ if (method == Extension_FromKeysAndValues_List_List)
+ {
+ return FromKeysAndValues(arguments[1], arguments[2]);
+ }
+
+ if (method.IsClosedFormOf(Extension_Remove))
+ {
+ return Subtract(arguments[1], arguments[2]);
+ }
+
+ return null;
+ }
+
+ if (arguments.Count is 2)
+ {
+ if (!IsDictionaryType(arguments[1].Type) && !IsDictionaryType(arguments[0].Type))
+ {
+ return null;
+ }
+
+ if (method == Extension_ToHstore)
+ {
+ return ToHstore(arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Extension_ToJson))
+ {
+ return ToJson(arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Extension_ToJsonb))
+ {
+ return ToJsonb(arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Extension_ToKeysAndValues))
+ {
+ return ToKeysAndValues(arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Extension_FromKeysAndValues_List))
+ {
+ return FromKeysAndValues(arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Extension_ToJsonLoose))
+ {
+ return ToJsonLoose(arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Extension_ToJsonbLoose))
+ {
+ return ToJsonbLoose(arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Enumerable_SequenceEqual))
+ {
+ return Equal(arguments[0], arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Enumerable_Concat))
+ {
+ return Concat(arguments[0], arguments[1]);
+ }
+
+ if (method.IsClosedFormOf(Enumerable_Except))
+ {
+ return Subtract(arguments[0], arguments[1], false);
+ }
+
+ return null;
+ }
+
+ if (arguments.Count is 1)
+ {
+ if (method.IsClosedFormOf(Enumerable_Count) || method.IsClosedFormOf(Enumerable_Any))
+ {
+ var keyValueType = method.GetGenericArguments()[0];
+ if (!keyValueType.IsGenericType || keyValueType.GetGenericTypeDefinition() != typeof(KeyValuePair<,>))
+ {
+ return null;
+ }
+ return method.Name == nameof(Enumerable.Count)
+ ? Count(arguments[0])
+ : NotEmpty(arguments[0], GetDictionaryValueType(keyValueType));
+ }
+
+ if (method.IsClosedFormOf(Enumerable_ToDictionary))
+ {
+ return arguments[0].Type == method.ReturnType
+ ? arguments[0]
+ : arguments[0].TypeMapping?.StoreType == "hstore"
+ ? arguments[0] is SqlConstantExpression or SqlParameterExpression
+ ? _sqlExpressionFactory.ApplyTypeMapping(arguments[0], _stringDictionaryMapping)
+ : _sqlExpressionFactory.Convert(arguments[0], StringDictionaryType, _stringDictionaryMapping)
+ : null;
+ }
+
+ if (method.IsClosedFormOf(GenericImmutableDictionary_ToImmutableDictionary))
+ {
+ return arguments[0].Type == method.ReturnType
+ ? arguments[0]
+ : arguments[0].TypeMapping?.StoreType == "hstore"
+ ? arguments[0] is SqlConstantExpression or SqlParameterExpression
+ ? _sqlExpressionFactory.ApplyTypeMapping(arguments[0], _immutableStringDictionaryMapping)
+ : _sqlExpressionFactory.Convert(
+ arguments[0], ImmutableStringDictionaryType, _immutableStringDictionaryMapping)
+ : null;
+ }
+
+ // Hstore: store.Keys.ToList() => akeys(store)
+ // store.Values.ToList() -> avals(store)
+ // Json: store.Keys.ToList() => array(select json_object_keys(instance))
+ // store.Values.ToList() => select array_agg(value) from json_each_text(instance))
+ // Jsonb: store.Keys.ToList() => array(select jsonb_object_keys(instance))
+ // store.Values.ToList() => select array_agg(value) from jsonb_each_text(instance))
+ if (method.IsClosedFormOf(Enumerable_ToList)
+ && (arguments[0] is SqlFunctionExpression { Name: "akeys" or "avals", Arguments: [{ TypeMapping.StoreType: "hstore" }] }
+ || arguments[0] is SqlFunctionExpression
+ {
+ Name: "array",
+ Arguments:
+ [
+ ScalarSubqueryExpression
+ {
+ Subquery.Projection:
+ [
+ {
+ Expression: SqlFunctionExpression
+ {
+ Name: "json_object_keys" or "jsonb_object_keys",
+ Arguments: [{ TypeMapping.StoreType: "json" or "jsonb" }]
+ }
+ }
+ ]
+ }
+ ]
+ }
+ || arguments[0] is ScalarSubqueryExpression
+ {
+ Subquery.Tables:
+ [
+ TableValuedFunctionExpression
+ {
+ Name: "json_each_text" or "jsonb_each_text" or "json_each" or "jsonb_each",
+ Arguments: [{ TypeMapping.StoreType: "json" or "jsonb" }]
+ }
+ ]
+ }))
+ {
+ return arguments[0];
+ }
+
+ return null;
+ }
+
+ return null;
+ }
+
+ if (!IsDictionaryMethod(method))
+ {
+ return null;
+ }
+
+ if (method.Name == "get_Item")
+ {
+ return ValueForKey(instance, arguments[0]);
+ }
+
+ if (method.Name == nameof(ImmutableDictionary.Remove))
+ {
+ return Subtract(instance, arguments[0]);
+ }
+
+ if (method.Name == nameof(Dictionary.ContainsKey))
+ {
+ return ContainsKey(instance, arguments[0]);
+ }
+
+ if (method.Name == nameof(Dictionary.ContainsValue))
+ {
+ return ContainsValue(instance, arguments[0]);
+ }
+
+ return null;
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Translate(
+ SqlExpression? instance,
+ MemberInfo member,
+ Type returnType,
+ IDiagnosticsLogger logger)
+ {
+ if (instance is null || !IsDictionaryType(instance.Type))
+ {
+ return null;
+ }
+ return member.Name switch
+ {
+ nameof(Dictionary.Keys) => Keys(instance),
+ nameof(Dictionary.Count) => Count(instance, true),
+ nameof(ImmutableDictionary.IsEmpty) => Empty(instance),
+ nameof(Dictionary.Values) => Values(instance),
+ _ => null
+ };
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Keys(SqlExpression instance)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "json" => JsonObjectKeys(instance, "json_object_keys"),
+ "jsonb" => JsonObjectKeys(instance, "jsonb_object_keys"),
+ "hstore" => _sqlExpressionFactory.Function(
+ "akeys", [instance], true, TrueArrays[1], StringListType, _stringListTypeMapping),
+ _ => null
+ };
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Values(SqlExpression instance)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "json" => JsonObjectValues(instance, false),
+ "jsonb" => JsonObjectValues(instance, true),
+ "hstore" => _sqlExpressionFactory.Function(
+ "avals", [instance], true, TrueArrays[1], StringListType, _stringListTypeMapping),
+ _ => null
+ };
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Count(SqlExpression instance, bool nullable = false)
+ => IsDictionaryStore(instance)
+ ? _sqlExpressionFactory.Function("cardinality", [Keys(instance)!], nullable, TrueArrays[1], typeof(int))
+ : null;
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? NotEmpty(SqlExpression instance, Type valueType)
+ {
+ var emptyDictionary = valueType == StringType
+ ? new Dictionary()
+ : typeof(Dictionary<,>).MakeGenericType(StringType, valueType).GetConstructor([])!.Invoke([]);
+ return instance.TypeMapping!.StoreType switch
+ {
+ "json" => _sqlExpressionFactory.NotEqual(
+ ConvertToJsonb(instance),
+ _sqlExpressionFactory.Constant(emptyDictionary, _jsonbTypeMapping)),
+ "jsonb" => _sqlExpressionFactory.NotEqual(
+ instance,
+ _sqlExpressionFactory.Constant(emptyDictionary, _jsonbTypeMapping)),
+ "hstore" => _sqlExpressionFactory.NotEqual(Count(instance)!, _sqlExpressionFactory.Constant(0)),
+ _ => null
+ };
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Empty(SqlExpression instance)
+ {
+ var valueType = GetDictionaryValueType(instance.Type);
+ var emptyDictionary = valueType == StringType
+ ? new Dictionary()
+ : typeof(Dictionary<,>).MakeGenericType(StringType, valueType).GetConstructor([])!.Invoke([]);
+ return instance.TypeMapping!.StoreType switch
+ {
+ "json" => _sqlExpressionFactory.Equal(
+ ConvertToJsonb(instance),
+ _sqlExpressionFactory.Constant(emptyDictionary, _jsonbTypeMapping)),
+ "jsonb" => _sqlExpressionFactory.Equal(
+ instance,
+ _sqlExpressionFactory.Constant(emptyDictionary, _jsonbTypeMapping)),
+ "hstore" => _sqlExpressionFactory.Equal(Count(instance)!, _sqlExpressionFactory.Constant(0)),
+ _ => null
+ };
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Subtract(SqlExpression left, SqlExpression right, bool leftType = true)
+ {
+ var leftCoerced = ToHstoreOrJsonb(left);
+ var rightCoerced = ToHstoreStringArrayStringOrJsonb(right);
+ if (leftCoerced is null || rightCoerced is null)
+ {
+ return null;
+ }
+ return _sqlExpressionFactory.MakePostgresBinary(
+ PgExpressionType.DictionarySubtract, left, right, (leftType ? left : right).TypeMapping);
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Concat(SqlExpression left, SqlExpression right)
+ {
+ var coerced = CoerceToSameStoreType(left, right, false);
+ if (!coerced.HasValue)
+ {
+ return null;
+ }
+ return _sqlExpressionFactory.MakePostgresBinary(
+ PgExpressionType.DictionaryConcat, coerced.Value.Item1, coerced.Value.Item2, coerced.Value.Item2.TypeMapping);
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Equal(SqlExpression left, SqlExpression right)
+ {
+ var coerced = CoerceToSameStoreType(left, right, true);
+ if (!coerced.HasValue)
+ {
+ return null;
+ }
+ return _sqlExpressionFactory.MakeBinary(
+ ExpressionType.Equal, coerced.Value.Item1, coerced.Value.Item1, coerced.Value.Item1.TypeMapping);
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Contains(SqlExpression left, SqlExpression right)
+ {
+ var coerced = CoerceToSameStoreType(left, right, false);
+ if (!coerced.HasValue)
+ {
+ return null;
+ }
+ return _sqlExpressionFactory.MakePostgresBinary(
+ PgExpressionType.Contains, coerced.Value.Item1, coerced.Value.Item1);
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ContainedBy(SqlExpression left, SqlExpression right)
+ {
+ var coerced = CoerceToSameStoreType(left, right, false);
+ if (!coerced.HasValue)
+ {
+ return null;
+ }
+ return _sqlExpressionFactory.MakePostgresBinary(
+ PgExpressionType.ContainedBy, coerced.Value.Item1, coerced.Value.Item1);
+ }
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression FromKeysAndValues(params SqlExpression[] arguments)
+ => _sqlExpressionFactory.Function(
+ "hstore", arguments, true, TrueArrays[arguments.Length], StringDictionaryType, _stringDictionaryMapping);
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ToKeysAndValues(SqlExpression instance)
+ => instance.TypeMapping!.StoreType switch
+ {
+ "hstore" => _sqlExpressionFactory.Function(
+ "hstore_to_array", [instance], true, TrueArrays[1], StringListType, _stringListTypeMapping),
+ _ => null
+ };
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ValuesForKeys(SqlExpression instance, SqlExpression keys)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "hstore" => _sqlExpressionFactory.MakePostgresBinary(
+ PgExpressionType.DictionaryValueForKey, instance, keys, _stringListTypeMapping),
+ "json" => JsonObjectValuesForKeys(instance, keys, false),
+ "jsonb" => JsonObjectValuesForKeys(instance, keys, true),
+ _ => null
+ };
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ValueForKey(SqlExpression instance, SqlExpression key)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "hstore" => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.DictionaryValueForKey, instance, key, _stringTypeMapping),
+ "json" => JsonObjectValueForKey(instance, key, _jsonTypeMapping),
+ "jsonb" => JsonObjectValueForKey(instance, key, _jsonbTypeMapping),
+ _ => null
+ };
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? Slice(SqlExpression instance, SqlExpression keys)
+ => instance.TypeMapping?.StoreType == "hstore"
+ ? _sqlExpressionFactory.Function(
+ "slice", [instance, keys], true, TrueArrays[2], instance.Type, instance.TypeMapping)
+ : null;
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ContainsKey(SqlExpression instance, SqlExpression key)
+ => IsDictionaryStore(instance)
+ ? _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.DictionaryContainsKey, ToHstoreOrJsonb(instance)!, key)
+ : null;
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ContainsValue(SqlExpression instance, SqlExpression value)
+ => IsDictionaryStore(instance)
+ ? _sqlExpressionFactory.Any(value, Values(instance)!, PgAnyOperatorType.Equal)
+ : null;
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ToJsonb(SqlExpression instance)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "hstore" => _sqlExpressionFactory.Function(
+ "hstore_to_jsonb", [instance], true, TrueArrays[1], StringDictionaryType, _jsonbTypeMapping),
+ "json" => ConvertToJsonb(instance),
+ "jsonb" => instance,
+ _ => null
+ };
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ToJson(SqlExpression instance)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "hstore" => _sqlExpressionFactory.Function(
+ "hstore_to_json", [instance], true, TrueArrays[1], StringDictionaryType, _jsonTypeMapping),
+ "json" => instance,
+ "jsonb" => instance is SqlParameterExpression or SqlConstantExpression
+ ? _sqlExpressionFactory.ApplyTypeMapping(instance, _jsonTypeMapping)
+ : _sqlExpressionFactory.Convert(instance, instance.Type, _jsonTypeMapping),
+ _ => null
+ };
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ToJsonLoose(SqlExpression instance)
+ => instance.TypeMapping?.StoreType is "hstore"
+ ? _sqlExpressionFactory.Function(
+ "hstore_to_json_loose", [instance], true, TrueArrays[1], typeof(Dictionary), _jsonTypeMapping)
+ : null;
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ToJsonbLoose(SqlExpression instance)
+ => instance.TypeMapping?.StoreType is "hstore"
+ ? _sqlExpressionFactory.Function(
+ "hstore_to_jsonb_loose", [instance], true, TrueArrays[1], typeof(Dictionary), _jsonbTypeMapping)
+ : null;
+
+ ///
+ /// 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.
+ ///
+ public SqlExpression? ToHstore(SqlExpression instance)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "hstore" => instance,
+ "jsonb" => ToHstoreFromJsonb(instance),
+ "json" => ToHstoreFromJson(instance),
+ _ => null
+ };
+
+ private SqlExpression? ToHstoreOrJsonb(SqlExpression instance)
+ => instance.TypeMapping?.StoreType switch
+ {
+ "hstore" => instance,
+ "jsonb" => instance,
+ "json" => ConvertToJsonb(instance),
+ _ => null
+ };
+
+ private SqlExpression? ToHstoreStringArrayStringOrJsonb(SqlExpression instance)
+ => instance.Type == StringType ? instance : instance.TypeMapping?.StoreType switch
+ {
+ "hstore" => instance,
+ "jsonb" => instance,
+ "text[]" => instance,
+ "json" => ConvertToJsonb(instance),
+ _ => null
+ };
+
+ private (SqlExpression, SqlExpression)? CoerceToSameStoreType(SqlExpression left, SqlExpression right, bool allowJson)
+ {
+ if (left.TypeMapping is null || right.TypeMapping is null)
+ {
+ return null;
+ }
+
+ if (left.TypeMapping.StoreType == right.TypeMapping.StoreType)
+ {
+ return !allowJson && left.TypeMapping.StoreType == "json" ? null : (left, right);
+ }
+
+ if ((left.TypeMapping.StoreType == "hstore" && right.TypeMapping.StoreType is "json" or "jsonb")
+ || (right.TypeMapping.StoreType == "hstore" && left.TypeMapping.StoreType is "json" or "jsonb"))
+ {
+ return (ToHstore(left)!, ToHstore(right)!);
+ }
+
+ if ((left.TypeMapping.StoreType == "json" && right.TypeMapping.StoreType is "jsonb")
+ || (right.TypeMapping.StoreType == "json" && left.TypeMapping.StoreType is "jsonb"))
+ {
+ return (ToHstoreOrJsonb(left)!, ToHstoreOrJsonb(right)!);
+ }
+
+ return null;
+ }
+
+ private ScalarSubqueryExpression ToHstoreFromJsonb(SqlExpression instance, bool immutable = false)
+ => ToHstore(instance, "jsonb_each_text", immutable);
+
+ private ScalarSubqueryExpression ToHstoreFromJson(SqlExpression instance, bool immutable = false)
+ => ToHstore(instance, "json_each_text", immutable);
+
+ private SqlExpression ConvertToJsonb(SqlExpression instance)
+ => instance is SqlConstantExpression or SqlParameterExpression
+ ? _sqlExpressionFactory.ApplyTypeMapping(instance, _jsonbTypeMapping)
+ : _sqlExpressionFactory.Convert(instance, instance.Type, _jsonbTypeMapping);
+
+ private static bool IsDictionaryStore(SqlExpression expr)
+ => expr.TypeMapping?.StoreType is "json" or "jsonb" or "hstore";
+
+ private static bool IsDictionaryMethod(MethodInfo method)
+ => IsDictionaryType(method.DeclaringType!);
+
+ private static bool IsDictionaryType(Type type)
+ => type.IsGenericType
+ && (type.GetGenericTypeDefinition() == typeof(Dictionary<,>)
+ || type.GetGenericTypeDefinition() == typeof(ImmutableDictionary<,>));
+
+ private static Type GetDictionaryValueType(Type dictionaryType)
+ => dictionaryType.GetGenericArguments()[1];
+
+ private SqlExpression JsonObjectValueForKey(SqlExpression instance, SqlExpression key, RelationalTypeMapping jsonTypeMapping)
+ {
+ var valueType = GetDictionaryValueType(instance.Type);
+ if (valueType == StringType)
+ {
+ return _sqlExpressionFactory.MakePostgresBinary(
+ PgExpressionType.JsonValueForKeyAsText, instance, key, _stringTypeMapping);
+ }
+ return _sqlExpressionFactory.JsonTraversal(instance, [key], false, valueType, jsonTypeMapping);
+ }
+
+#pragma warning disable EF1001 // SelectExpression constructors are currently internal
+
+ private SqlExpression JsonObjectKeys(SqlExpression instance, string jsonObjectKeysFn)
+ => _sqlExpressionFactory.Function(
+ "array", [
+ new ScalarSubqueryExpression(
+ new(
+ null,
+ [],
+ null, [], null,
+ [
+ new(
+ _sqlExpressionFactory.Function(
+ jsonObjectKeysFn, [instance],
+ true, TrueArrays[1], StringType, _stringTypeMapping),
+ string.Empty)
+ ], false, [], null, null))
+ ], true, TrueArrays[1], StringListType, _stringListTypeMapping);
+
+ private ScalarSubqueryExpression JsonObjectValues(SqlExpression instance, bool jsonb, SqlExpression? predicate = null)
+ {
+ var valueType = GetDictionaryValueType(instance.Type);
+ var jsonTypeMapping = jsonb ? _jsonbTypeMapping : _jsonTypeMapping;
+ var isStringValue = valueType == StringType;
+ var valueNeedsConversion = !isStringValue && valueType == BoolType || valueType.IsNumeric();
+ var valueTypeMapping = isStringValue ? _stringTypeMapping :
+ valueNeedsConversion ? _typeMappingSource.FindMapping(valueType, _model) :
+ jsonTypeMapping;
+ return new(
+ new(
+ null,
+ [
+ new TableValuedFunctionExpression(
+ "j1", isStringValue ? (jsonb ? "jsonb_each_text" : "json_each_text") : (jsonb ? "jsonb_each" : "json_each"),
+ [instance])
+ ],
+ predicate, [], null,
+ [
+ new(
+ _sqlExpressionFactory.Function(
+ "array_agg",
+ [
+ valueNeedsConversion
+ ? _sqlExpressionFactory.Convert(
+ new ColumnExpression(
+ "value", "j1", valueType, jsonTypeMapping, false), valueType, valueTypeMapping)
+ : new ColumnExpression(
+ "value", "j1", valueType, valueTypeMapping, false)
+ ],
+ true, TrueArrays[1],
+ isStringValue ? StringListType : GenericListType.MakeGenericType(valueType),
+ isStringValue ? _stringListTypeMapping :
+ valueNeedsConversion ? _typeMappingSource.FindMapping(GenericListType.MakeGenericType(valueType), _model) :
+ _typeMappingSource.FindMapping(GenericListType.MakeGenericType(valueType), jsonb ? "jsonb[]" : "json[]")),
+ string.Empty)
+ ], false, [], null, null));
+ }
+
+ private ScalarSubqueryExpression JsonObjectValuesForKeys(
+ SqlExpression instance,
+ SqlExpression keys,
+ bool jsonb)
+ => JsonObjectValues(
+ instance, jsonb, _sqlExpressionFactory.Any(
+ new ColumnExpression(
+ "key", "j1", StringType, _stringTypeMapping, false), keys, PgAnyOperatorType.Equal));
+
+ private ScalarSubqueryExpression ToHstore(SqlExpression instance, string jsonEachTextFn, bool immutable)
+ => new(
+ new(
+ null,
+ [new TableValuedFunctionExpression("j1", jsonEachTextFn, [instance])],
+ null, [], null,
+ [
+ new(
+ _sqlExpressionFactory.Function(
+ "hstore",
+ [
+ _sqlExpressionFactory.Function(
+ "array_agg", [new ColumnExpression("key", "j1", StringType, _stringTypeMapping, false)],
+ true, TrueArrays[1], StringListType, _stringListTypeMapping),
+ _sqlExpressionFactory.Function(
+ "array_agg", [new ColumnExpression("value", "j1", StringType, _stringTypeMapping, true)],
+ true, TrueArrays[1], StringListType, _stringListTypeMapping)
+ ],
+ true, TrueArrays[2], immutable ? ImmutableStringDictionaryType : StringDictionaryType,
+ immutable ? _immutableStringDictionaryMapping : _stringDictionaryMapping), string.Empty)
+ ], false, [], null, null));
+#pragma warning restore EF1001
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
index 28ab9785a..250e9e370 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
@@ -25,7 +25,8 @@ public NpgsqlMemberTranslatorProvider(
RelationalMemberTranslatorProviderDependencies dependencies,
IModel model,
IRelationalTypeMappingSource typeMappingSource,
- IDbContextOptions contextOptions)
+ IDbContextOptions contextOptions
+ )
: base(dependencies)
{
var npgsqlOptions = contextOptions.FindExtension() ?? new NpgsqlOptionsExtension();
@@ -40,10 +41,11 @@ public NpgsqlMemberTranslatorProvider(
new NpgsqlDateTimeMemberTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlJsonDomTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlLTreeTranslator(typeMappingSource, sqlExpressionFactory, model),
+ new DictionaryTranslator(typeMappingSource, sqlExpressionFactory, model),
JsonPocoTranslator,
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
- new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
+ new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
]);
}
}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
index 63843eab3..7188d15ee 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
@@ -61,7 +61,8 @@ public NpgsqlMethodCallTranslatorProvider(
new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory),
new NpgsqlRowValueTranslator(sqlExpressionFactory),
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
- new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)
+ new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
+ new DictionaryTranslator(typeMappingSource, sqlExpressionFactory, model)
]);
}
}
diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
index 03387b988..695b30476 100644
--- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
+++ b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
@@ -135,6 +135,7 @@ protected override void Print(ExpressionPrinter expressionPrinter)
PgExpressionType.JsonExists => "?",
PgExpressionType.JsonExistsAny => "?|",
PgExpressionType.JsonExistsAll => "?&",
+ PgExpressionType.JsonValueForKeyAsText => "->>",
PgExpressionType.LTreeMatches
when Right.TypeMapping is { StoreType: "lquery" } or NpgsqlArrayTypeMapping
@@ -151,6 +152,14 @@ protected override void Print(ExpressionPrinter expressionPrinter)
PgExpressionType.Distance => "<->",
+ PgExpressionType.DictionaryContainsAnyKey => "?|",
+ PgExpressionType.DictionaryContainsAllKeys => "?&",
+
+ PgExpressionType.DictionaryContainsKey => "?",
+ PgExpressionType.DictionaryValueForKey => "->",
+ PgExpressionType.DictionaryConcat => "||",
+ PgExpressionType.DictionarySubtract => "-",
+
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
})
.Append(" ");
diff --git a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
index 270a67e01..03e886ec5 100644
--- a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
+++ b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
@@ -129,6 +129,11 @@ public enum PgExpressionType
///
JsonExistsAll, // ?<@
+ ///
+ /// Represents a PostgreSQL operator for retrieving a field from a JSON object or element from JSON array as `text`.
+ ///
+ JsonValueForKeyAsText, // ->>
+
#endregion JSON
#region LTree
@@ -159,4 +164,38 @@ public enum PgExpressionType
LTreeFirstMatches, // ?~ or ?@
#endregion LTree
+
+ #region Dictionary
+
+ ///
+ /// Represents a PostgreSQL operator for accessing a hstore, json or bson value for a given key
+ ///
+ DictionaryValueForKey, // ->
+
+ ///
+ /// Represents a PostgreSQL operator for checking if a hstore contains the given key
+ ///
+ DictionaryContainsKey, // ?
+
+ ///
+ /// Represents a PostgreSQL operator for determining if a hstore or json column contains any of an array of keys
+ ///
+ DictionaryContainsAnyKey, // ?|
+
+ ///
+ /// Represents a PostgreSQL operator for determining if a hstore or json column contains all of an array of keys
+ ///
+ DictionaryContainsAllKeys, // ?&
+
+ ///
+ /// Represents a PostgreSQL operator for subtracting hstore or jsonb values
+ ///
+ DictionarySubtract, // -
+
+ ///
+ /// Represents a PostgreSQL operator for concatenating hstores
+ ///
+ DictionaryConcat, // ||
+
+ #endregion Dictionary
}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
index 5c1ed24f1..5755298b2 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
@@ -510,6 +510,7 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp
PgExpressionType.JsonExists => "?",
PgExpressionType.JsonExistsAny => "?|",
PgExpressionType.JsonExistsAll => "?&",
+ PgExpressionType.JsonValueForKeyAsText => "->>",
PgExpressionType.LTreeMatches
when binaryExpression.Right.TypeMapping.StoreType == "lquery"
@@ -527,6 +528,14 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp
PgExpressionType.Distance => "<->",
+ PgExpressionType.DictionaryContainsAnyKey => "?|",
+ PgExpressionType.DictionaryContainsAllKeys => "?&",
+
+ PgExpressionType.DictionaryContainsKey => "?",
+ PgExpressionType.DictionaryValueForKey => "->",
+ PgExpressionType.DictionaryConcat => "||",
+ PgExpressionType.DictionarySubtract => "-",
+
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
})
.Append(" ");
diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
index 84fac6794..7b10770c9 100644
--- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
+++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
@@ -307,6 +307,9 @@ public virtual SqlExpression MakePostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
+ case PgExpressionType.DictionaryContainsAnyKey:
+ case PgExpressionType.DictionaryContainsAllKeys:
+ case PgExpressionType.DictionaryContainsKey:
returnType = typeof(bool);
break;
@@ -773,6 +776,9 @@ private SqlExpression ApplyTypeMappingOnPostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
+ case PgExpressionType.DictionaryContainsAnyKey:
+ case PgExpressionType.DictionaryContainsAllKeys:
+ case PgExpressionType.DictionaryContainsKey:
{
// TODO: For networking, this probably needs to be cleaned up, i.e. we know where the CIDR and INET are
// based on operator type?
@@ -823,6 +829,19 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No
break;
}
+ case PgExpressionType.DictionaryValueForKey:
+ case PgExpressionType.DictionaryConcat:
+ case PgExpressionType.DictionarySubtract:
+ case PgExpressionType.JsonValueForKeyAsText:
+ {
+ return new PgBinaryExpression(
+ operatorType,
+ ApplyDefaultTypeMapping(left),
+ ApplyDefaultTypeMapping(right),
+ typeMapping!.ClrType,
+ typeMapping);
+ }
+
default:
throw new InvalidOperationException(
$"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}");
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs
index 3d6eef2f0..0b8b5f0dc 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs
@@ -5,7 +5,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
///
/// The type mapping for the PostgreSQL hstore type. Supports both
-/// and over strings.
+/// and where TKey and TValue are both strings.
///
///
/// See: https://www.postgresql.org/docs/current/static/hstore.html
diff --git a/test/EFCore.PG.FunctionalTests/Query/DictionaryQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/DictionaryQueryTest.cs
new file mode 100644
index 000000000..5cdfb248c
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/DictionaryQueryTest.cs
@@ -0,0 +1,1072 @@
+using System.Collections.Immutable;
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
+// ReSharper disable ConvertToConstant.Local
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
+
+public class DictionaryEntity
+{
+ public int Id { get; set; }
+
+ public Dictionary Dictionary { get; set; } = null!;
+
+ public ImmutableDictionary ImmutableDictionary { get; set; } = null!;
+ public Dictionary JsonDictionary { get; set; } = null!;
+ public ImmutableDictionary JsonbDictionary { get; set; } = null!;
+ public Dictionary IntDictionary { get; set; } = null!;
+ public Dictionary> NestedDictionary { get; set; }
+
+}
+
+public class DictionaryQueryContext(DbContextOptions options) : PoolableDbContext(options)
+{
+ public DbSet SomeEntities { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ var entity = modelBuilder.Entity();
+ entity.Property(_ => _.JsonDictionary).HasColumnType("json").IsRequired();
+ entity.Property(_ => _.JsonbDictionary).HasColumnType("jsonb").IsRequired();
+ entity.Property(_ => _.IntDictionary).HasColumnType("jsonb").IsRequired();
+ entity.Property(_ => _.NestedDictionary).HasColumnType("jsonb").IsRequired();
+ }
+
+ public static async Task SeedAsync(DictionaryQueryContext context)
+ {
+ var arrayEntities = DictionaryQueryData.CreateDictionaryEntities();
+
+ context.SomeEntities.AddRange(arrayEntities);
+ await context.SaveChangesAsync();
+ }
+}
+
+public class DictionaryQueryData : ISetSource
+{
+ public IReadOnlyList DictionaryEntities { get; } = CreateDictionaryEntities();
+
+ public IQueryable Set()
+ where TEntity : class
+ {
+ if (typeof(TEntity) == typeof(DictionaryEntity))
+ {
+ return (IQueryable)DictionaryEntities.AsQueryable();
+ }
+
+ throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity));
+ }
+
+ public static IReadOnlyList CreateDictionaryEntities()
+ =>
+ [
+ new()
+ {
+ Id = 1,
+ Dictionary = new() { ["key"] = "value" },
+ ImmutableDictionary = new Dictionary { ["key2"] = "value2" }.ToImmutableDictionary(),
+ JsonDictionary = new() { ["jkey"] = "value" },
+ JsonbDictionary = new Dictionary { ["jkey2"] = "value" }.ToImmutableDictionary(),
+ IntDictionary = new() { ["ikey"] = 1},
+ NestedDictionary = new() { ["key"] = new() { ["nested"] = "value"}}
+ },
+ new()
+ {
+ Id = 2,
+ Dictionary = new() { ["key"] = "value" },
+ ImmutableDictionary = new Dictionary { ["key3"] = "value3" }.ToImmutableDictionary(),
+ JsonDictionary = new() { ["jkey"] = "value" },
+ JsonbDictionary = new Dictionary { ["jkey2"] = "value2" }.ToImmutableDictionary(),
+ IntDictionary = new() { ["ikey"] = 2},
+ NestedDictionary = new() { ["key"] = new() { ["nested2"] = "value2"}}
+ }
+ ];
+}
+
+public class DictionaryQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory
+{
+ static DictionaryQueryFixture()
+ {
+ // TODO: Switch to using NpgsqlDataSource
+#pragma warning disable CS0618 // Type or member is obsolete
+ NpgsqlConnection.GlobalTypeMapper.EnableDynamicJson();
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ protected override string StoreName
+ => "HstoreQueryTest";
+
+ protected override ITestStoreFactory TestStoreFactory
+ => NpgsqlTestStoreFactory.Instance;
+
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ private DictionaryQueryData _expectedData;
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Ignore(CoreEventId.CollectionWithoutComparer));
+
+ protected override Task SeedAsync(DictionaryQueryContext context)
+ => DictionaryQueryContext.SeedAsync(context);
+
+ public Func GetContextCreator()
+ => CreateContext;
+
+ public ISetSource GetExpectedData()
+ => _expectedData ??= new DictionaryQueryData();
+
+ public IReadOnlyDictionary EntitySorters
+ => new Dictionary>
+ {
+ { typeof(DictionaryEntity), e => ((DictionaryEntity)e)?.Id }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+
+ public IReadOnlyDictionary EntityAsserters
+ => new Dictionary>
+ {
+ {
+ typeof(DictionaryEntity), (e, a) =>
+ {
+ Assert.Equal(e is null, a is null);
+ if (a is not null)
+ {
+ var ee = (DictionaryEntity)e;
+ var aa = (DictionaryEntity)a;
+
+ Assert.Equal(ee.Id, aa.Id);
+ Assert.Equal(ee.Dictionary, ee.Dictionary);
+ Assert.Equal(ee.ImmutableDictionary, ee.ImmutableDictionary);
+ Assert.Equal(ee.JsonDictionary, ee.JsonDictionary);
+ Assert.Equal(ee.JsonbDictionary, ee.JsonbDictionary);
+ Assert.Equal(ee.IntDictionary, ee.IntDictionary);
+ Assert.Equal(ee.NestedDictionary, ee.NestedDictionary);
+ }
+ }
+ }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+}
+
+public class DictionaryQueryTest : QueryTestBase
+{
+ public DictionaryQueryTest(DictionaryQueryFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_ContainsKey(bool async)
+ {
+ var keyToTest = "key";
+ await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.ContainsKey(keyToTest)));
+ AssertSql("""
+@__keyToTest_0='key'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."Dictionary" ? @__keyToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_ContainsKey(bool async)
+ {
+ var keyToTest = "jkey2";
+ await AssertQuery(async, ss => ss.Set().Where(s => s.JsonbDictionary.ContainsKey(keyToTest)));
+ AssertSql("""
+@__keyToTest_0='jkey2'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."JsonbDictionary" ? @__keyToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_ContainsKey(bool async)
+ {
+ var keyToTest = "key3";
+ await AssertQuery(async, ss => ss.Set().Where(s => s.ImmutableDictionary.ContainsKey(keyToTest)));
+ AssertSql(
+ """
+@__keyToTest_0='key3'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."ImmutableDictionary" ? @__keyToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_ContainsValue(bool async)
+ {
+ var valueToTest = "value";
+ await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.ContainsValue(valueToTest)));
+ AssertSql(
+ """
+@__valueToTest_0='value'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE @__valueToTest_0 = ANY (avals(s."Dictionary"))
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_ContainsValue(bool async)
+ {
+ var valueToTest = "value2";
+ await AssertQuery(async, ss => ss.Set().Where(s => s.ImmutableDictionary.ContainsValue(valueToTest)));
+ AssertSql(
+ """
+@__valueToTest_0='value2'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE @__valueToTest_0 = ANY (avals(s."ImmutableDictionary"))
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Keys_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.Keys.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT akeys(s."Dictionary")
+FROM "SomeEntities" AS s
+""");
+ }
+
+ // Note: There is no "Dictionary_Keys" or "Dictionary_Values" tests as they return a Dictionary.KeyCollection and Dictionary.ValueCollection
+ // which cannot be translated from a `List` which is what the `avals` and `akeys` functions returns. ImmutableDictionary.Keys and ImmutableDictionary.Values
+ // does have tests as they return an `IEnumerable` that `List` is compatible with
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Keys(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.Keys), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT akeys(s."ImmutableDictionary")
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_Keys(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonbDictionary.Keys), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT array((
+ SELECT jsonb_object_keys(s."JsonbDictionary")))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonDictionary_Keys_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonDictionary.Keys.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT array((
+ SELECT json_object_keys(s."JsonDictionary")))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_Keys_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonbDictionary.Keys.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT array((
+ SELECT jsonb_object_keys(s."JsonbDictionary")))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Keys_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.Keys.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT akeys(s."ImmutableDictionary")
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Values_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.Values.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT avals(s."Dictionary")
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Values(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.Values), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT avals(s."ImmutableDictionary")
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Values_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.Values.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT avals(s."ImmutableDictionary")
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonDictionary_Values_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonDictionary.Values.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT (
+ SELECT array_agg(j.value)
+ FROM json_each_text(s."JsonDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_Values_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonbDictionary.Values.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT (
+ SELECT array_agg(j.value)
+ FROM jsonb_each_text(s."JsonbDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task IntDictionary_Values_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss =>
+ ss.Set().Select(s => s.IntDictionary.Values.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT (
+ SELECT array_agg(j.value::int)
+ FROM jsonb_each(s."IntDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task NestedDictionary_Values_ToList(bool async)
+ {
+ await AssertQuery(
+ async, ss =>
+ ss.Set().Select(s => s.NestedDictionary.Values.ToList()), elementAsserter: Assert.Equal,
+ assertOrder: true);
+ AssertSql(
+ """
+SELECT (
+ SELECT array_agg(j.value)
+ FROM jsonb_each(s."NestedDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Item_equals(bool async)
+ {
+ var keyToTest = "key";
+ var valueToTest = "value";
+ await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__valueToTest_0='value'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."Dictionary" -> 'key' = @__valueToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "key2";
+ var valueToTest = "value2";
+ await AssertQuery(async, ss =>
+ ss.Set().Where(s => s.ImmutableDictionary[keyToTest] == valueToTest),
+ ss => ss.Set().Where(s =>
+ s.ImmutableDictionary.ContainsKey(keyToTest) && s.ImmutableDictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__keyToTest_0='key2'
+@__valueToTest_1='value2'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."ImmutableDictionary" -> @__keyToTest_0 = @__valueToTest_1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "jkey";
+ var valueToTest = "value";
+ await AssertQuery(async, ss =>
+ ss.Set().Where(s => s.JsonDictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__valueToTest_0='value'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."JsonDictionary" ->> 'jkey' = @__valueToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "jkey2";
+ var valueToTest = "value2";
+ await AssertQuery(async, ss =>
+ ss.Set().Where(s => s.JsonbDictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__keyToTest_0='jkey2'
+@__valueToTest_1='value2'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."JsonbDictionary" ->> @__keyToTest_0 = @__valueToTest_1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task IntDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "ikey";
+ var valueToTest = 1;
+ await AssertQuery(async, ss =>
+ ss.Set().Where(s => s.IntDictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__valueToTest_0='1' (DbType = Object)
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."IntDictionary" -> 'ikey' = @__valueToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task NestedDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "key";
+ var key2ToTest = "nested";
+ var valueToTest = "value";
+ await AssertQuery(
+ async, ss =>
+ ss.Set().Where(s => s.NestedDictionary[keyToTest][key2ToTest] == valueToTest),
+ ss => ss.Set().Where(
+ s => s.NestedDictionary.ContainsKey(keyToTest)
+ && s.NestedDictionary[keyToTest].ContainsKey(key2ToTest)
+ && s.NestedDictionary[keyToTest][key2ToTest] == valueToTest));
+ AssertSql(
+ """
+@__valueToTest_0='value'
+
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."NestedDictionary" -> 'key' ->> 'nested' = @__valueToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Where_Count(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.Count >= 1));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE cardinality(akeys(s."Dictionary")) >= 1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Select_Count(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Select(s => s.Dictionary.Count));
+ AssertSql(
+ """
+SELECT cardinality(akeys(s."Dictionary"))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Where_Count(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Where(s => s.ImmutableDictionary.Count >= 1));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE cardinality(akeys(s."ImmutableDictionary")) >= 1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Select_Count(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Select(s => s.ImmutableDictionary.Count));
+ AssertSql(
+ """
+SELECT cardinality(akeys(s."ImmutableDictionary"))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Enumerable_KeyValuePair_Count(bool async)
+ {
+ await AssertQuery(
+ // ReSharper disable once UseCollectionCountProperty
+ async, ss => ss.Set().Select(s => s.Dictionary.Count()));
+ AssertSql(
+ """
+SELECT cardinality(akeys(s."Dictionary"))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Where_IsEmpty(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Where(s => !s.ImmutableDictionary.IsEmpty));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE cardinality(akeys(s."ImmutableDictionary")) <> 0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_Where_IsEmpty(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Where(s => !s.JsonbDictionary.IsEmpty));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE s."JsonbDictionary" <> '{}'
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Remove(bool async)
+ {
+ var key = "key";
+ await AssertQuery(async, ss => ss.Set().Select(s => s.ImmutableDictionary.Remove(key)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+@__key_0='key'
+
+SELECT s."ImmutableDictionary" - @__key_0
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Where_Any(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.Any()));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE cardinality(akeys(s."Dictionary")) <> 0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Where_Any(bool async)
+ {
+ await AssertQuery(async, ss => ss.Set().Where(s => s.ImmutableDictionary.Any()));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."ImmutableDictionary", s."IntDictionary", s."JsonDictionary", s."JsonbDictionary", s."NestedDictionary"
+FROM "SomeEntities" AS s
+WHERE cardinality(akeys(s."ImmutableDictionary")) <> 0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_ToDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.ToDictionary()),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."Dictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonDictionary_ToDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonDictionary.ToDictionary()),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."JsonDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonDictionary_ToImmutableDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonDictionary.ToImmutableDictionary()),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."JsonDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_ToDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonbDictionary.ToDictionary()),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."JsonbDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_ToImmutableDictionary(bool async)
+ {
+ await AssertQuery(
+#pragma warning disable CA2009
+ async, ss => ss.Set().Select(s => s.JsonbDictionary.ToImmutableDictionary()),
+#pragma warning restore CA2009
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."JsonbDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_ToImmutableDictionary(bool async)
+ {
+ await AssertQuery(
+#pragma warning disable CA2009
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.ToImmutableDictionary()),
+#pragma warning restore CA2009
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."ImmutableDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_ToImmutableDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.ToImmutableDictionary()),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."Dictionary"::hstore
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_ToDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.ToDictionary()),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."ImmutableDictionary"::hstore
+FROM "SomeEntities" AS s
+""");
+
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Concat_Dictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.Concat(s.Dictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."ImmutableDictionary" || s."Dictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Concat_ImmutableDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.Concat(s.ImmutableDictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."Dictionary" || s."ImmutableDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_Concat_ImmutableDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonbDictionary.Concat(s.ImmutableDictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT (
+ SELECT hstore(array_agg(j.key), array_agg(j.value))
+ FROM jsonb_each_text(s."JsonbDictionary") AS j) || s."ImmutableDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonbDictionary_Concat_JsonbDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonbDictionary.Concat(s.JsonbDictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."JsonbDictionary" || s."JsonbDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task JsonDictionary_Concat_JsonbDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.JsonDictionary.Concat(s.JsonbDictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."JsonDictionary"::jsonb || s."JsonbDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Concat_JsonbDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.Concat(s.JsonbDictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."Dictionary" || (
+ SELECT hstore(array_agg(j.key), array_agg(j.value))
+ FROM jsonb_each_text(s."JsonbDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Concat_JsonDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.Concat(s.JsonDictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."Dictionary" || (
+ SELECT hstore(array_agg(j.key), array_agg(j.value))
+ FROM json_each_text(s."JsonDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Keys_Concat_ImmutableDictionary_Keys(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.Keys.Concat(s.ImmutableDictionary.Keys)),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT array_cat(akeys(s."Dictionary"), akeys(s."ImmutableDictionary"))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Values_Concat_Dictionary_Values(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.Values.Concat(s.Dictionary.Values)),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+SELECT array_cat(avals(s."ImmutableDictionary"), avals(s."Dictionary"))
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Except_Dictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.ImmutableDictionary.Except(s.Dictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."ImmutableDictionary" - s."Dictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Except_ImmutableDictionary(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => s.Dictionary.Except(s.ImmutableDictionary)),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT s."Dictionary" - s."ImmutableDictionary"
+FROM "SomeEntities" AS s
+""");
+ }
+
+ // [Theory]
+ // [MemberData(nameof(IsAsyncData))]
+ public async Task Extensions_ValuesForKeys(bool async)
+ {
+ string[] keys = ["key"];
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => keys.Select(key => s.Dictionary[key])),
+ ss => ss.Set().Select(s
+ => s.Dictionary.Where(d => keys.Contains(d.Key)).Select(d => d.Value).ToList()),
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+@__keys_1={ 'key' } (DbType = Object)
+
+SELECT s."Dictionary" -> @__keys_1
+FROM "SomeEntities" AS s
+""");
+ }
+
+ // [Theory]
+ // [MemberData(nameof(IsAsyncData))]
+ public async Task Extensions_ValuesForKeys_JsonDictionary(bool async)
+ {
+ string[] keys = ["key"];
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => keys.Select(key => s.JsonDictionary[key])),
+ ss => ss.Set().Select(
+ s
+ // ReSharper disable once CanSimplifyDictionaryLookupWithTryGetValue
+#pragma warning disable CA1854
+ => keys.Select(key => s.JsonDictionary.ContainsKey(key) ? s.JsonDictionary[key] : null)),
+#pragma warning restore CA1854
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+@__keys_1={ 'key' } (DbType = Object)
+
+SELECT (
+ SELECT hstore(array_agg(j.key), array_agg(j.value))
+ FROM json_each_text(s."JsonDictionary") AS j) -> @__keys_1
+FROM "SomeEntities" AS s
+""");
+ }
+
+ // [Theory]
+ // [MemberData(nameof(IsAsyncData))]
+ public async Task Extensions_ValuesForKeys_JsonbDictionary(bool async)
+ {
+ string[] keys = ["key"];
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => keys.Select(key => s.JsonbDictionary[key])),
+ ss => ss.Set().Select(
+ s
+ // ReSharper disable once CanSimplifyDictionaryLookupWithTryGetValue
+#pragma warning disable CA1854
+ => keys.Select(key => s.JsonbDictionary.ContainsKey(key) ? s.JsonbDictionary[key] : null)),
+#pragma warning restore CA1854
+ elementAsserter: Assert.Equal, assertOrder: true);
+ AssertSql(
+ """
+@__keys_1={ 'key' } (DbType = Object)
+
+SELECT (
+ SELECT hstore(array_agg(j.key), array_agg(j.value))
+ FROM jsonb_each_text(s."JsonbDictionary") AS j) -> @__keys_1
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Extensions_FromJson(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => EF.Functions.ToHstore(s.JsonDictionary)),
+ ss => ss.Set().Select(s => s.JsonDictionary),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql("""
+SELECT (
+ SELECT hstore(array_agg(j.key), array_agg(j.value))
+ FROM json_each_text(s."JsonDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Extensions_ToHstore(bool async)
+ {
+ await AssertQuery(
+ async, ss => ss.Set().Select(s => EF.Functions.ToHstore(s.JsonbDictionary)),
+ ss => ss.Set().Select(s => s.JsonbDictionary.ToDictionary()),
+ elementAsserter: AssertEqualsIgnoringOrder, assertOrder: true);
+ AssertSql(
+ """
+SELECT (
+ SELECT hstore(array_agg(j.key), array_agg(j.value))
+ FROM jsonb_each_text(s."JsonbDictionary") AS j)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ // ReSharper disable twice PossibleMultipleEnumeration
+ private static void AssertEqualsIgnoringOrder(IEnumerable left, IEnumerable right)
+ {
+ Console.WriteLine(left);
+ Console.WriteLine(right);
+ Assert.Empty(left.Except(right));
+ Assert.Empty(right.Except(left));
+ }
+
+ protected void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}