diff --git a/Directory.Build.props b/Directory.Build.props index cd7fb9c90..db8d6dd1f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ 9.0.0-rc.2 latest - true + false latest NU5105 true @@ -54,4 +54,4 @@ - + \ No newline at end of file diff --git a/global.json b/global.json index 22cf64c12..a143424dc 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100-rc.1.24452.12", + "version": "9.0.100-preview.7.24407.12", "rollForward": "latestMajor", "allowPrerelease": true } 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..15fe003a2 --- /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 = 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..9cb74f437 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/DictionaryQueryTest.cs @@ -0,0 +1,1006 @@ +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 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); +}