diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/StringKeyValuePairDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/StringKeyValuePairDbFunctionsExtensions.cs new file mode 100644 index 000000000..e64f157ff --- /dev/null +++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/StringKeyValuePairDbFunctionsExtensions.cs @@ -0,0 +1,214 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods supporting `hstore` function translation for PostgreSQL. +/// +public static class StringKeyValuePairDbFunctionsExtensions +{ + /// + /// Returns values associated with given keys, or NULL if not present. + /// + /// Works with `hstore`, `json` and `jsonb` type columns + ///
+ /// SQL translation: input -> keys + /// Note: input will automatically translate to `hstore` if `jsonb` or `json` + ///
+ /// The instance. + /// The input hstore. + /// The keys to return the values of from the input hstore. + /// PostgreSQL documentation for 'hstore' functions. + public static List ValuesForKeys(this DbFunctions _, IEnumerable> input, IList keys) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ValuesForKeys))); + + /// + /// Does dictionary contain all the specified keys? + /// + /// Works with `hstore`, `json` and `jsonb` type columns + ///
+ /// SQL translation: input ?& keys + ///
+ /// The instance. + /// The input hstore. + /// The keys to check if the input store contains all of. + /// PostgreSQL documentation for 'hstore' functions. + public static bool ContainsAllKeys(this DbFunctions _, IEnumerable> input, IList keys) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsAllKeys))); + + /// + /// Does dictionary contain any of the specified keys? + /// + /// Works with `hstore`, `json` and `jsonb` type columns + ///
+ /// SQL translation: input ?| keys + ///
+ /// The instance. + /// The input hstore. + /// The keys to check if the input store contains any of. + /// PostgreSQL documentation for 'hstore' functions. + public static bool ContainsAnyKeys(this DbFunctions _, IEnumerable> input, IList keys) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsAnyKeys))); + + /// + /// Does left operand contain right? + /// + /// Works with `hstore`, `json` and `jsonb` type columns + ///
+ /// SQL translation: left @> right + ///
+ /// The instance. + /// The input left hstore. + /// The input right hstore. + /// PostgreSQL documentation for 'hstore' functions. + public static bool Contains(this DbFunctions _, IEnumerable> left, IEnumerable> right) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Is left operand contained by right? + ///
+ /// SQL translation: left <@ right + ///
+ /// The instance. + /// The input left hstore. + /// The input right hstore. + /// PostgreSQL documentation for 'hstore' functions. + public static bool ContainedBy(this DbFunctions _, IEnumerable> left, IEnumerable> right) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + /// + /// Deletes keys from input operand. + /// + /// Works with `hstore`, `json` and `jsonb` type columns + ///
+ /// SQL translation: input - key + ///
+ /// The instance. + /// The input hstore. + /// 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))); + + /// + /// Extracts a subset of a dictionary containing only the specified keys. + /// + /// For `jsonb` and `json` input they will be automatically converted to `hstore` but + /// the result will always be an `hstore` type + ///
+ /// SQL translation: slice(input, keys) + ///
+ /// The instance. + /// The input hstore. + /// The keys to extract the subset of. + /// PostgreSQL documentation for 'hstore' functions. + public static T Slice(this DbFunctions _, T input, IList keys) + where T : IEnumerable> + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Slice))); + + /// + /// Converts hstore to an array of alternating keys and values. + ///
+ /// SQL translation: hstore_to_array(input) + ///
+ /// The instance. + /// The input hstore. + /// PostgreSQL documentation for 'hstore' functions. + public static List ToKeysAndValues(this DbFunctions _, IEnumerable> input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToKeysAndValues))); + + /// + /// Constructs an hstore 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 FromKeysAndValues(this DbFunctions _, IList input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FromKeysAndValues))); + + /// + /// Constructs an hstore 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 FromKeysAndValues(this DbFunctions _, IList keys, IList values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FromKeysAndValues))); + + /// + /// Converts an hstore to a `json` type `Dictionary<string, string>` + ///
+ /// Can be used during a migration of changing a column's StoreType from 'hstore' to a 'json' with the `Using` clause + /// SQL translation: hstore_to_json(input) + ///
+ /// 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 a `json` 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 + /// SQL translation: select hstore(array_agg(key), array_agg(value)) FROM json_each_text(json) + ///
+ /// The instance. + /// The input hstore. + /// PostgreSQL documentation for 'hstore' functions. + public static Dictionary FromJson(this DbFunctions _, IEnumerable> input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FromJson))); + + /// + /// Converts an hstore to a `jsonb` type `Dictionary<string, string>` + /// + /// Can be used during a migration of changing a column's StoreType from 'hstore' to 'jsonb' with the `Using` clause + ///
+ /// SQL translation: hstore_to_jsonb(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))); + + /// + /// Converts a `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 'jsonb' to 'hstore' with the `Using` clause + /// SQL translation: select hstore(array_agg(key), array_agg(value)) FROM jsonb_each_text(json) + ///
+ /// The instance. + /// The input hstore. + /// PostgreSQL documentation for 'hstore' functions. + public static Dictionary FromJsonb(this DbFunctions _, IEnumerable> input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FromJsonb))); + + /// + /// 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))); +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs new file mode 100644 index 000000000..88a059651 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs @@ -0,0 +1,657 @@ +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 NpgsqlHstoreTranslator : IMethodCallTranslator, IMemberTranslator +{ + #region Types + + private static readonly Type DictionaryType = typeof(Dictionary); + private static readonly Type ImmutableDictionaryType = typeof(ImmutableDictionary); + private static readonly Type ExtensionsType = typeof(StringKeyValuePairDbFunctionsExtensions); + private static readonly Type KvpType = typeof(KeyValuePair); + private static readonly Type GenericKvpType = typeof(KeyValuePair<,>).MakeGenericType( + Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(1)); + private static readonly Type GenericEnumerableType = typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)); + + #endregion + + #region MethodInfo(s) + + private static readonly MethodInfo Dictionary_ContainsKey = + DictionaryType.GetMethod(nameof(Dictionary.ContainsKey))!; + + private static readonly MethodInfo ImmutableDictionary_ContainsKey = + ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary.ContainsKey))!; + + private static readonly MethodInfo Dictionary_ContainsValue = + DictionaryType.GetMethod(nameof(Dictionary.ContainsValue))!; + + private static readonly MethodInfo ImmutableDictionary_ContainsValue = + ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary.ContainsValue))!; + + private static readonly MethodInfo ImmutableDictionary_Remove = + ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary.Remove), + BindingFlags.Public | BindingFlags.Instance, [typeof(string)])!; + + private static readonly MethodInfo Dictionary_Item_Getter = + DictionaryType.FindIndexerProperty()!.GetMethod!; + + private static readonly MethodInfo ImmutableDictionary_Item_Getter = + ImmutableDictionaryType.FindIndexerProperty()!.GetMethod!; + + private static readonly MethodInfo Enumerable_Any = + typeof(Enumerable).GetMethod(nameof(Enumerable.Any), BindingFlags.Public | BindingFlags.Static, [GenericEnumerableType])! + .MakeGenericMethod(KvpType); + + private static readonly MethodInfo Enumerable_Count = + typeof(Enumerable).GetMethod(nameof(Enumerable.Count), BindingFlags.Public | BindingFlags.Static, [GenericEnumerableType])! + .MakeGenericMethod(KvpType); + + private static readonly MethodInfo Enumerable_ToList = + typeof(Enumerable).GetMethod(nameof(Enumerable.ToList), BindingFlags.Public | BindingFlags.Static, [GenericEnumerableType])! + .MakeGenericMethod(typeof(string)); + + private static readonly MethodInfo Enumerable_ToDictionary = + typeof(Enumerable).GetMethod( + nameof(Enumerable.ToDictionary), BindingFlags.Public | BindingFlags.Static, + [typeof(IEnumerable<>).MakeGenericType(GenericKvpType)])!.MakeGenericMethod(typeof(string), typeof(string)); + + private static readonly MethodInfo ImmutableDictionary_ToImmutableDictionary = + typeof(ImmutableDictionary).GetMethod( + nameof(ImmutableDictionary.ToImmutableDictionary), BindingFlags.Public | BindingFlags.Static, + [typeof(IEnumerable<>).MakeGenericType(GenericKvpType)])!.MakeGenericMethod(typeof(string), typeof(string)); + + private static readonly MethodInfo Enumerable_Concat = typeof(Enumerable).GetMethod( + nameof(Enumerable.Concat), BindingFlags.Public | BindingFlags.Static, + [GenericEnumerableType, GenericEnumerableType])!.MakeGenericMethod(KvpType); + + private static readonly MethodInfo Enumerable_Except = typeof(Enumerable).GetMethod( + nameof(Enumerable.Except), BindingFlags.Public | BindingFlags.Static, + [GenericEnumerableType, GenericEnumerableType])!.MakeGenericMethod(KvpType); + + private static readonly MethodInfo Enumerable_SequenceEqual = typeof(Enumerable).GetMethod( + nameof(Enumerable.SequenceEqual), BindingFlags.Public | BindingFlags.Static, + [GenericEnumerableType, GenericEnumerableType])!.MakeGenericMethod(KvpType); + + #endregion + + #region Extension MethodInfo(s) + + private static readonly MethodInfo Extension_ValuesForKeys = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ValuesForKeys))!; + + private static readonly MethodInfo Extension_ContainsAnyKeys = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ContainsAnyKeys))!; + + private static readonly MethodInfo Extension_ContainsAllKeys = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ContainsAllKeys))!; + + private static readonly MethodInfo Extension_FromJson = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.FromJson))!; + + private static readonly MethodInfo Extension_FromJsonb = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.FromJsonb))!; + + private static readonly MethodInfo Extension_ToJson = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ToJson))!; + + private static readonly MethodInfo Extension_ToJsonb = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ToJsonb))!; + + private static readonly MethodInfo Extension_ToJsonLoose = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ToJsonLoose))!; + + private static readonly MethodInfo Extension_ToJsonbLoose = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ToJsonbLoose))!; + + private static readonly MethodInfo Extension_Contains = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.Contains))!; + + private static readonly MethodInfo Extension_ContainedBy = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ContainedBy))!; + + private static readonly MethodInfo Extension_Remove = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.Remove))!; + + private static readonly MethodInfo Extension_Slice = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.Slice))!; + + private static readonly MethodInfo Extension_ToKeysAndValues = + ExtensionsType.GetMethod(nameof(StringKeyValuePairDbFunctionsExtensions.ToKeysAndValues))!; + + private static readonly MethodInfo Extension_FromKeysAndValues_List = + ExtensionsType.GetMethod( + nameof(StringKeyValuePairDbFunctionsExtensions.FromKeysAndValues), BindingFlags.Public | BindingFlags.Static, + null, [typeof(DbFunctions), typeof(IList)], null)!; + + private static readonly MethodInfo Extension_FromKeysAndValues_List_List = + ExtensionsType.GetMethod( + nameof(StringKeyValuePairDbFunctionsExtensions.FromKeysAndValues), BindingFlags.Public | BindingFlags.Static, + null, [typeof(DbFunctions), typeof(IList), typeof(IList)], null)!; + + #endregion + + #region Properties + + private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary.Count))!; + + private static readonly PropertyInfo ImmutableDictionary_Count = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.Count))!; + + private static readonly PropertyInfo ImmutableDictionary_IsEmpty = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.IsEmpty))!; + + private static readonly PropertyInfo Dictionary_Keys = DictionaryType.GetProperty(nameof(Dictionary.Keys))!; + + private static readonly PropertyInfo ImmutableDictionary_Keys = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.Keys))!; + + private static readonly PropertyInfo Dictionary_Values = DictionaryType.GetProperty(nameof(Dictionary.Values))!; + + private static readonly PropertyInfo ImmutableDictionary_Values = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.Values))!; + + #endregion + + #region Fields + + private readonly RelationalTypeMapping _stringListTypeMapping; + private readonly RelationalTypeMapping _stringTypeMapping; + private readonly RelationalTypeMapping _dictionaryMapping; + private readonly RelationalTypeMapping _jsonTypeMapping; + private readonly RelationalTypeMapping _jsonbTypeMapping; + private readonly RelationalTypeMapping _immutableDictionaryMapping; + private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; + + #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 NpgsqlHstoreTranslator( + IRelationalTypeMappingSource typeMappingSource, + NpgsqlSqlExpressionFactory sqlExpressionFactory, + IModel model) + { + _sqlExpressionFactory = sqlExpressionFactory; + _stringListTypeMapping = typeMappingSource.FindMapping(typeof(List), model)!; + _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), model)!; + _dictionaryMapping = typeMappingSource.FindMapping(DictionaryType, model)!; + _immutableDictionaryMapping = typeMappingSource.FindMapping(ImmutableDictionaryType, 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 (method == Extension_ContainsAnyKeys) + { + return _sqlExpressionFactory.MakePostgresBinary( + PgExpressionType.ContainsAnyKey, arguments[1], arguments[2]); + } + + if (method == Extension_ContainsAllKeys) + { + return _sqlExpressionFactory.MakePostgresBinary( + PgExpressionType.ContainsAllKeys, arguments[1], arguments[2]); + } + + if (method == Extension_Contains) + { + return _sqlExpressionFactory.MakePostgresBinary( + PgExpressionType.Contains, arguments[1], arguments[2]); + } + + if (method == Extension_ContainedBy) + { + return _sqlExpressionFactory.MakePostgresBinary( + PgExpressionType.ContainedBy, arguments[1], arguments[2]); + } + + if (method == Extension_Remove) + { + return Subtract(arguments[1], arguments[2]); + } + + if (method == Extension_FromKeysAndValues_List_List) + { + return FromKeysAndValues(arguments[1], arguments[2]); + } + + if (method == Extension_ValuesForKeys) + { + return ValuesForKeys(arguments[1], arguments[2]); + } + + if (method == Extension_Slice) + { + return Slice(CoerceToHstore(arguments[1])!, arguments[2]); + } + + return null; + } + + if (arguments.Count is 2) + { + if (method == Extension_FromJson) + { + return arguments[1].TypeMapping?.StoreType == "hstore" ? arguments[1] : FromJson(arguments[1]); + } + + if (method == Extension_FromJsonb) + { + return arguments[1].TypeMapping?.StoreType == "hstore" ? arguments[1] : FromJsonb(arguments[1]); + } + + if (method == Extension_ToJson) + { + return arguments[1].TypeMapping?.StoreType == "json" ? arguments[1] : ToJson(arguments[1]); + } + + if (method == Extension_ToJsonb) + { + return arguments[1].TypeMapping?.StoreType == "jsonb" ? arguments[1] : ToJsonb(arguments[1]); + } + + if (method == Extension_ToKeysAndValues) + { + return ToKeysAndValues(arguments[1]); + } + + if (method == Extension_FromKeysAndValues_List) + { + return FromKeysAndValues(arguments[1]); + } + + if (method == Extension_ToJsonLoose) + { + return ToJsonLoose(arguments[1]); + } + + if (method == Extension_ToJsonbLoose) + { + return ToJsonbLoose(arguments[1]); + } + + if (arguments[0].TypeMapping?.StoreType is not "json" and not "jsonb" and not "hstore" + || arguments[1].TypeMapping?.StoreType is not "json" and not "jsonb" and not "hstore") + { + return null; + } + + var sameStoreType = arguments[0].TypeMapping?.StoreType == arguments[1].TypeMapping?.StoreType; + var left = sameStoreType ? arguments[0] : CoerceToHstore(arguments[0])!; + var right = sameStoreType ? arguments[1] : CoerceToHstore(arguments[1])!; + + if (method == Enumerable_SequenceEqual) + { + return _sqlExpressionFactory.Equal(left, right); + } + + if (method == Enumerable_Concat) + { + return Concat(left, right); + } + + if (method == Enumerable_Except) + { + return Subtract(left, right, false); + } + + return null; + } + + if (arguments.Count is not 1) + { + return null; + } + + if (method == Enumerable_ToDictionary) + { + return arguments[0].Type == DictionaryType ? arguments[0] + : arguments[0].TypeMapping?.StoreType == "jsonb" ? FromJsonb(arguments[0]) + : arguments[0].TypeMapping?.StoreType == "json" ? FromJson(arguments[0]) + : _sqlExpressionFactory.Convert(arguments[0], DictionaryType, _dictionaryMapping); + } + + if (method == ImmutableDictionary_ToImmutableDictionary) + { + return arguments[0].Type == ImmutableDictionaryType ? arguments[0] + : arguments[0].TypeMapping?.StoreType == "jsonb" ? FromJsonb(arguments[0], true) + : arguments[0].TypeMapping?.StoreType == "json" ? FromJson(arguments[0], true) + : _sqlExpressionFactory.Convert(arguments[0], ImmutableDictionaryType, _immutableDictionaryMapping); + } + + if (arguments[0].TypeMapping?.StoreType == "hstore") + { + if (method == Enumerable_Any) + { + return _sqlExpressionFactory.NotEqual(Count(arguments[0]), _sqlExpressionFactory.Constant(0)); + } + + if (method == Enumerable_Count) + { + return Count(arguments[0]); + } + + return null; + } + + // store.Keys.ToList() => akeys(store) OR store.Values.ToList() -> avals(store) + if (method == Enumerable_ToList && arguments[0] is SqlFunctionExpression { Arguments: [{ TypeMapping.StoreType: "hstore" }] }) + { + return arguments[0]; + } + + return null; + } + + // store.Remove(key) => store - key (works with json, jsonb, and hstore) + if (method == ImmutableDictionary_Remove) + { + return Subtract(instance, arguments[0]); + } + + // store[key] => store -> key (works with json, jsonb, and hstore) + if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter) + { + return ValueForKey(instance, arguments[0]); + } + + // store.ContainsKey(key) => store ? key (works with json, jsonb, and hstore) + if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey) + { + return ContainsKey(instance, arguments[0]); + } + + // store.ContainsValue(value) => value ANY(avals(store)) (works with hstore only) + if (instance.TypeMapping?.StoreType == "hstore" && + (method == Dictionary_ContainsValue || method == ImmutableDictionary_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?.TypeMapping?.StoreType != "hstore") + { + return null; + } + + if (member == Dictionary_Count || member == ImmutableDictionary_Count) + { + return Count(instance, true); + } + + if (member == Dictionary_Keys || member == ImmutableDictionary_Keys) + { + return Keys(instance); + } + + if (member == Dictionary_Values || member == ImmutableDictionary_Values) + { + return Values(instance); + } + + if (member == ImmutableDictionary_IsEmpty) + { + return _sqlExpressionFactory.Equal(Count(instance), _sqlExpressionFactory.Constant(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. + /// + protected SqlExpression? CoerceToHstore(SqlExpression instance) + => instance.TypeMapping?.StoreType switch + { + "hstore" => instance, + "jsonb" => FromJsonb(instance), + "json" => FromJson(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. + /// + protected SqlExpression Keys(SqlExpression instance) + => _sqlExpressionFactory.Function( + "akeys", [instance], true, TrueArrays[1], typeof(List), _stringListTypeMapping); + + /// + /// 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. + /// + protected SqlExpression Values(SqlExpression instance) + => _sqlExpressionFactory.Function( + "avals", [instance], true, TrueArrays[1], typeof(List), _stringListTypeMapping); + + /// + /// 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. + /// + protected SqlExpression Count(SqlExpression instance, bool nullable = false) + => _sqlExpressionFactory.Function("cardinality", [Keys(instance)], nullable, TrueArrays[1], typeof(int)); + + /// + /// 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. + /// + protected SqlExpression Subtract(SqlExpression left, SqlExpression right, bool leftType = true) + => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreSubtract, 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. + /// + protected SqlExpression Concat(SqlExpression left, SqlExpression right) + => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreConcat, left, right, 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. + /// + protected SqlExpression FromKeysAndValues(params SqlExpression[] arguments) + => _sqlExpressionFactory.Function( + "hstore", arguments, true, TrueArrays[arguments.Length], DictionaryType, _dictionaryMapping); + + /// + /// 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. + /// + protected SqlExpression ToKeysAndValues(SqlExpression instance) + => _sqlExpressionFactory.Function( + "hstore_to_array", [CoerceToHstore(instance)!], true, TrueArrays[1], typeof(List), _stringListTypeMapping); + + /// + /// 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. + /// + protected SqlExpression ValuesForKeys(SqlExpression instance, SqlExpression keys) + => _sqlExpressionFactory.MakePostgresBinary( + PgExpressionType.HStoreValueForKey, CoerceToHstore(instance)!, keys, _stringListTypeMapping); + + /// + /// 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. + /// + protected SqlExpression ValueForKey(SqlExpression instance, SqlExpression key) + => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, key, _stringTypeMapping); + + /// + /// 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. + /// + protected SqlExpression Slice(SqlExpression instance, SqlExpression keys) + => _sqlExpressionFactory.Function( + "slice", [instance, keys], true, TrueArrays[2], instance.Type, instance.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. + /// + protected SqlExpression ContainsKey(SqlExpression instance, SqlExpression key) + => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, key); + + /// + /// 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. + /// + protected SqlExpression ContainsValue(SqlExpression instance, SqlExpression value) + => _sqlExpressionFactory.Any(value, Values(instance), PgAnyOperatorType.Equal); + + /// + /// 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. + /// + protected SqlExpression ToJsonb(SqlExpression instance) + => _sqlExpressionFactory.Function("hstore_to_jsonb", [instance], true, TrueArrays[1], DictionaryType, _jsonbTypeMapping); + + /// + /// 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. + /// + protected SqlExpression ToJson(SqlExpression instance) + => _sqlExpressionFactory.Function("hstore_to_json", [instance], true, TrueArrays[1], DictionaryType, _jsonTypeMapping); + + /// + /// 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. + /// + protected SqlExpression ToJsonLoose(SqlExpression instance) + => _sqlExpressionFactory.Function( + "hstore_to_json_loose", [instance], true, TrueArrays[1], typeof(Dictionary), _jsonTypeMapping); + + /// + /// 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. + /// + protected SqlExpression ToJsonbLoose(SqlExpression instance) + => _sqlExpressionFactory.Function( + "hstore_to_jsonb_loose", [instance], true, TrueArrays[1], typeof(Dictionary), _jsonbTypeMapping); + + /// + /// 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. + /// + protected SqlExpression FromJsonb(SqlExpression instance, bool immutable = false) + => FromJson(instance, "jsonb_each_text", immutable); + + /// + /// 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. + /// + protected SqlExpression FromJson(SqlExpression instance, bool immutable = false) + => FromJson(instance, "json_each_text", immutable); + + // Though it has the reverse, PostgreSQL does not have a built-in function to transform `json` or `jsonb` to `hstore`. + // The following function produces an expression that performs the same behavior. If some day PostgreSQL does + // add a supported function, this could be updated to use it. + private ScalarSubqueryExpression FromJson(SqlExpression instance, string jsonFunction, bool immutable) + { +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + return new( + new( + null, + [new TableValuedFunctionExpression("j1", jsonFunction, [instance])], + null, [], null, + [ + new( + _sqlExpressionFactory.Function( + "hstore", + [ + _sqlExpressionFactory.Function( + "array_agg", [new ColumnExpression("key", "j1", typeof(string), _stringTypeMapping, false)], + true, TrueArrays[1], typeof(List)), + _sqlExpressionFactory.Function( + "array_agg", [new ColumnExpression("value", "j1", typeof(string), _stringTypeMapping, true)], + true, TrueArrays[1], typeof(List)) + ], + true, TrueArrays[2], immutable ? ImmutableDictionaryType : DictionaryType, + immutable ? _immutableDictionaryMapping : _dictionaryMapping), 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..f31fe1812 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(); @@ -43,7 +44,8 @@ public NpgsqlMemberTranslatorProvider( JsonPocoTranslator, new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), new NpgsqlStringMemberTranslator(sqlExpressionFactory), - new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory) + new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory), + new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory, model) ]); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 63843eab3..75e797d56 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 NpgsqlHstoreTranslator(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..debad72c8 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs @@ -151,6 +151,14 @@ protected override void Print(ExpressionPrinter expressionPrinter) PgExpressionType.Distance => "<->", + PgExpressionType.ContainsAnyKey => "?|", + PgExpressionType.ContainsAllKeys => "?&", + + PgExpressionType.HStoreContainsKey => "?", + PgExpressionType.HStoreValueForKey => "->", + PgExpressionType.HStoreConcat => "||", + PgExpressionType.HStoreSubtract => "-", + _ => 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..e35dbb5a5 100644 --- a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs +++ b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs @@ -159,4 +159,42 @@ public enum PgExpressionType LTreeFirstMatches, // ?~ or ?@ #endregion LTree + + #region Dictionary + + /// + /// Represents a PostgreSQL operator for determining if a hstore or json column contains any of an array of keys + /// + ContainsAnyKey, // ?| + + /// + /// Represents a PostgreSQL operator for determining if a hstore or json column contains all of an array of keys + /// + ContainsAllKeys, // ?& + + #endregion + + #region HStore + + /// + /// Represents a PostgreSQL operator for checking if a hstore contains the given key + /// + HStoreContainsKey, // ? + + /// + /// Represents a PostgreSQL operator for accessing a hstore value for a given key + /// + HStoreValueForKey, // -> + + /// + /// Represents a PostgreSQL operator for concatenating hstores + /// + HStoreConcat, // || + + /// + /// Represents a PostgreSQL operator for subtracting hstores + /// + HStoreSubtract, // - + + #endregion HStore } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 3418d5045..d429fb088 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -527,6 +527,14 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp PgExpressionType.Distance => "<->", + PgExpressionType.ContainsAnyKey => "?|", + PgExpressionType.ContainsAllKeys => "?&", + + PgExpressionType.HStoreContainsKey => "?", + PgExpressionType.HStoreValueForKey => "->", + PgExpressionType.HStoreConcat => "||", + PgExpressionType.HStoreSubtract => "-", + _ => 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..8e92dc13d 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.ContainsAnyKey: + case PgExpressionType.ContainsAllKeys: + case PgExpressionType.HStoreContainsKey: returnType = typeof(bool); break; @@ -773,6 +776,9 @@ private SqlExpression ApplyTypeMappingOnPostgresBinary( case PgExpressionType.JsonExists: case PgExpressionType.JsonExistsAny: case PgExpressionType.JsonExistsAll: + case PgExpressionType.ContainsAnyKey: + case PgExpressionType.ContainsAllKeys: + case PgExpressionType.HStoreContainsKey: { // 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,18 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No break; } + case PgExpressionType.HStoreValueForKey: + case PgExpressionType.HStoreConcat: + case PgExpressionType.HStoreSubtract: + { + 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/HstoreQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs new file mode 100644 index 000000000..414fca792 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs @@ -0,0 +1,541 @@ +using System.Collections.Immutable; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +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 class DictionaryQueryContext(DbContextOptions options) : PoolableDbContext(options) +{ + public DbSet SomeEntities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + } + + 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(), + }, + new() + { + Id = 2, + Dictionary = new() { ["key"] = "value" }, + ImmutableDictionary = new Dictionary { ["key3"] = "value3" }.ToImmutableDictionary(), + } + ]; +} + +public class HstoreQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory +{ + 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); + + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); +} + +public class HstoreQueryTest : QueryTestBase +{ + public HstoreQueryTest(HstoreQueryFixture 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" +FROM "SomeEntities" AS s +WHERE s."Dictionary" ? @__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" +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" +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" +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 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" +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" +FROM "SomeEntities" AS s +WHERE s."ImmutableDictionary" -> @__keyToTest_0 = @__valueToTest_1 +"""); + } + + [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" +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" +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( + 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" +FROM "SomeEntities" AS s +WHERE cardinality(akeys(s."ImmutableDictionary")) <> 0 +"""); + } + + [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" +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" +FROM "SomeEntities" AS s +WHERE cardinality(akeys(s."ImmutableDictionary")) <> 0 +"""); + } + + [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 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 +"""); + } + + // ReSharper disable twice PossibleMultipleEnumeration + private static void AssertEqualsIgnoringOrder(IEnumerable left, IEnumerable right) + { + Assert.Empty(left.Except(right)); + Assert.Empty(right.Except(left)); + } + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}