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..33f875eb3
--- /dev/null
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs
@@ -0,0 +1,179 @@
+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
+{
+ private static readonly Type DictionaryType = typeof(Dictionary);
+ private static readonly Type ImmutableDictionaryType = typeof(ImmutableDictionary);
+
+ 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 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,
+ [typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!
+ .MakeGenericMethod(typeof(KeyValuePair));
+
+ private static readonly MethodInfo Enumerable_ToList =
+ typeof(Enumerable).GetMethod(
+ nameof(Enumerable.ToList), BindingFlags.Public | BindingFlags.Static,
+ [typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!
+ .MakeGenericMethod(typeof(string));
+
+ 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))!;
+
+ private readonly RelationalTypeMapping _stringListTypeMapping;
+ private readonly RelationalTypeMapping _stringTypeMapping;
+ private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
+ ///
+ /// 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)
+ {
+ _sqlExpressionFactory = sqlExpressionFactory;
+ _stringListTypeMapping = typeMappingSource.FindMapping(typeof(List))!;
+ _stringTypeMapping = typeMappingSource.FindMapping(typeof(string))!;
+ }
+
+ ///
+ /// 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 (method == Enumerable_Any && arguments[0].TypeMapping?.StoreType == "hstore")
+ {
+ return _sqlExpressionFactory.NotEqual(Count(arguments[0]), _sqlExpressionFactory.Constant(0));
+ }
+
+ if (method == Enumerable_ToList && arguments[0] is SqlFunctionExpression { Arguments: [{ TypeMapping.StoreType: "hstore" }] })
+ {
+ return arguments[0];
+ }
+
+ if (instance?.TypeMapping?.StoreType != "hstore")
+ {
+ return null;
+ }
+
+ if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
+ {
+ return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
+ }
+
+ if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
+ {
+ return _sqlExpressionFactory.Any(arguments[0], Values(instance), PgAnyOperatorType.Equal);
+ }
+
+ if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
+ {
+ return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0], _stringTypeMapping);
+ }
+
+ 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;
+ }
+
+ private SqlExpression Keys(SqlExpression instance)
+ => _sqlExpressionFactory.Function(
+ "akeys", [instance], true, TrueArrays[1], typeof(List), _stringListTypeMapping);
+
+ private SqlExpression Values(SqlExpression instance)
+ => _sqlExpressionFactory.Function(
+ "avals", [instance], true, TrueArrays[1], typeof(List), _stringListTypeMapping);
+
+ private SqlExpression Count(SqlExpression instance, bool nullable = false)
+ => _sqlExpressionFactory.Function("cardinality", [Keys(instance)], nullable, TrueArrays[1], typeof(int));
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
index 28ab9785a..62859b1c4 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
@@ -43,7 +43,8 @@ public NpgsqlMemberTranslatorProvider(
JsonPocoTranslator,
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
- new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
+ new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
+ new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
]);
}
}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
index 63843eab3..785f67a57 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)
]);
}
}
diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
index 03387b988..040bee0fd 100644
--- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
+++ b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
@@ -151,6 +151,9 @@ protected override void Print(ExpressionPrinter expressionPrinter)
PgExpressionType.Distance => "<->",
+ PgExpressionType.HStoreContainsKey => "?",
+ PgExpressionType.HStoreValueForKey => "->",
+
_ => 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..3a167d59e 100644
--- a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
+++ b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
@@ -159,4 +159,18 @@ public enum PgExpressionType
LTreeFirstMatches, // ?~ or ?@
#endregion LTree
+
+ #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, // ->
+
+ #endregion HStore
}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
index 3418d5045..73fec4157 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
@@ -527,6 +527,9 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp
PgExpressionType.Distance => "<->",
+ PgExpressionType.HStoreContainsKey => "?",
+ PgExpressionType.HStoreValueForKey => "->",
+
_ => 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..7ffd21594 100644
--- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
+++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
@@ -16,6 +16,7 @@ public class NpgsqlSqlExpressionFactory : SqlExpressionFactory
{
private readonly NpgsqlTypeMappingSource _typeMappingSource;
private readonly RelationalTypeMapping _boolTypeMapping;
+ private readonly RelationalTypeMapping _stringTypeMapping;
private static Type? _nodaTimeDurationType;
private static Type? _nodaTimePeriodType;
@@ -29,6 +30,7 @@ public NpgsqlSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
{
_typeMappingSource = (NpgsqlTypeMappingSource)dependencies.TypeMappingSource;
_boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool), dependencies.Model)!;
+ _stringTypeMapping = _typeMappingSource.FindMapping(typeof(string), dependencies.Model)!;
}
#region Expression factory methods
@@ -307,6 +309,7 @@ public virtual SqlExpression MakePostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
+ case PgExpressionType.HStoreContainsKey:
returnType = typeof(bool);
break;
@@ -773,6 +776,7 @@ private SqlExpression ApplyTypeMappingOnPostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
+ 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 +827,16 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No
break;
}
+ case PgExpressionType.HStoreValueForKey:
+ {
+ return new PgBinaryExpression(
+ operatorType,
+ ApplyDefaultTypeMapping(left),
+ ApplyDefaultTypeMapping(right),
+ typeof(string),
+ _stringTypeMapping);
+ }
+
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..14c4a91b5
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs
@@ -0,0 +1,386 @@
+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_Contains_key(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_Contains_key(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_Contains_value(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 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), 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 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
+""");
+ }
+
+ protected void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}