diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs index b00d537e3fe..a4b19a30b36 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs @@ -1119,6 +1119,39 @@ private void WriteBatchResolver( parameter.Key); break; + case ResolverParameterKind.Selection: + Writer.WriteIndentedLine( + "var args{0} = contexts[0].Selection;", + i); + break; + + case ResolverParameterKind.QueryContext: + var entityType = parameter.TypeParameters[0].ToFullyQualified(); + Writer.WriteIndentedLine("var args{0}_selection = contexts[0].Selection;", i); + Writer.WriteIndentedLine("var args{0}_filter = global::{1}.GetFilterContext(contexts[0]);", + i, + WellKnownTypes.FilterContextResolverContextExtensions); + Writer.WriteIndentedLine("var args{0}_sorting = global::{1}.GetSortingContext(contexts[0]);", + i, + WellKnownTypes.SortingContextResolverContextExtensions); + Writer.WriteIndentedLine( + "var args{0} = new global::{1}<{2}>(", + i, + WellKnownTypes.QueryContext, + entityType); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "global::{0}.AsSelector<{1}>(args{2}_selection, contexts[0].IncludeFlags),", + WellKnownTypes.HotChocolateExecutionSelectionExtensions, + entityType, + i); + Writer.WriteIndentedLine("args{0}_filter?.AsPredicate<{1}>(),", i, entityType); + Writer.WriteIndentedLine("args{0}_sorting?.AsSortDefinition<{1}>());", i, entityType); + } + + break; + default: // Fallback for any other kind: extract from first context Writer.WriteIndentedLine( @@ -1653,6 +1686,12 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet WellKnownTypes.ConnectionFlagsHelper); break; + case ResolverParameterKind.Selection: + Writer.WriteIndentedLine( + "var args{0} = context.Selection;", + i); + break; + case ResolverParameterKind.Unknown: Writer.WriteIndentedLine( "var args{0} = _binding_{1}_{2}.Execute<{3}>(context);", diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CompilationExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CompilationExtensions.cs index 6757b3c8ebf..7d9201448b9 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CompilationExtensions.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CompilationExtensions.cs @@ -417,6 +417,11 @@ public static ResolverParameterKind GetParameterKind( return ResolverParameterKind.ConnectionFlags; } + if (parameter.IsSelection()) + { + return ResolverParameterKind.Selection; + } + return ResolverParameterKind.Unknown; } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs index 643364463ec..0839159bbc5 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs @@ -777,6 +777,9 @@ public static bool IsPagingArguments(this IParameterSymbol parameter) => parameter.Type is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.ToDisplayString().StartsWith(WellKnownTypes.PagingArguments); + public static bool IsSelection(this IParameterSymbol parameter) + => parameter.Type.ToDisplayString() == WellKnownTypes.ISelection; + public static bool IsGlobalState( this IParameterSymbol parameter, [NotNullWhen(true)] out string? key) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs index 31d36b3ecb1..96415bea0c0 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs @@ -88,7 +88,8 @@ ResolverParameterKind.EventMessage or ResolverParameterKind.FieldNode or ResolverParameterKind.OutputField or ResolverParameterKind.ClaimsPrincipal or - ResolverParameterKind.ConnectionFlags; + ResolverParameterKind.ConnectionFlags or + ResolverParameterKind.Selection; public bool RequiresBinding => Kind == ResolverParameterKind.Unknown; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs index a7c653e5d41..daa5641882a 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs @@ -23,5 +23,6 @@ public enum ResolverParameterKind Argument, QueryContext, PagingArguments, - ConnectionFlags + ConnectionFlags, + Selection } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs index 6e175e895b1..cabce26320e 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs @@ -110,6 +110,7 @@ public static class WellKnownTypes public const string ImmutableArrayOfMiddlewareContext = "System.Collections.Immutable.ImmutableArray"; public const string IList = "System.Collections.IList"; public const string MiddlewareContext = "HotChocolate.Resolvers.IMiddlewareContext"; + public const string ISelection = "HotChocolate.Execution.ISelection"; public static HashSet TypeClass { get; } = [ diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs index 2adb62deef6..88db6484848 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs @@ -12,6 +12,8 @@ public class CatalogContext(DbContextOptions options) : DbContex public DbSet Brands => Set(); + public DbSet Suppliers => Set(); + public DbSet SingleProperties => Set(); protected override void OnModelCreating(ModelBuilder builder) @@ -19,5 +21,6 @@ protected override void OnModelCreating(ModelBuilder builder) builder.ApplyConfiguration(new BrandEntityTypeConfiguration()); builder.ApplyConfiguration(new ProductTypeEntityTypeConfiguration()); builder.ApplyConfiguration(new ProductEntityTypeConfiguration()); + builder.ApplyConfiguration(new SupplierEntityTypeConfiguration()); } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/EntityConfigurations/BrandEntityTypeConfiguration.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/EntityConfigurations/BrandEntityTypeConfiguration.cs index 5e6faa50f93..045c0253b78 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/EntityConfigurations/BrandEntityTypeConfiguration.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/EntityConfigurations/BrandEntityTypeConfiguration.cs @@ -14,5 +14,10 @@ public void Configure(EntityTypeBuilder builder) builder .Property(cb => cb.Name) .HasMaxLength(100); + + builder + .HasOne(cb => cb.Supplier) + .WithMany(s => s.Brands) + .HasForeignKey(cb => cb.SupplierId); } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/EntityConfigurations/SupplierEntityTypeConfiguration.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/EntityConfigurations/SupplierEntityTypeConfiguration.cs new file mode 100644 index 00000000000..15217fd3d07 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/EntityConfigurations/SupplierEntityTypeConfiguration.cs @@ -0,0 +1,26 @@ +using HotChocolate.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace HotChocolate.Data.Data.EntityConfigurations; + +internal sealed class SupplierEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("Suppliers"); + + builder + .Property(s => s.Name) + .HasMaxLength(100); + + builder + .Property(s => s.Website) + .HasMaxLength(256); + + builder + .Property(s => s.ContactEmail) + .HasMaxLength(256); + } +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs index a87a83a2813..8bae4aa495f 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs @@ -125,6 +125,33 @@ public async Task Query_Brands_With_BatchResolver_ProductCount() MatchSnapshot(result, interceptor); } + [Fact] + public async Task Query_Brands_With_BatchResolver_Supplier() + { + // arrange + using var interceptor = new TestQueryInterceptor(); + + // act + var result = await ExecuteAsync( + """ + { + brands(first: 5) { + nodes { + id + name + supplier { + name + website + } + } + } + } + """); + + // assert + MatchSnapshot(result, interceptor); + } + [Fact] public async Task Query_Brands_First_2_And_Products_First_2_Name_Desc() { diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs index 746429809a2..3149df74225 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs @@ -16,9 +16,27 @@ public async Task SeedAsync(CatalogContext context) var sourceJson = FileResource.Open("catalog.json"); var sourceItems = JsonSerializer.Deserialize(sourceJson)!; + // Seed suppliers first (brands will reference them). + context.Suppliers.RemoveRange(context.Suppliers); + var suppliers = new[] + { + new Supplier { Name = "Global Supply Co.", Website = "https://globalsupply.example.com", ContactEmail = "info@globalsupply.example.com" }, + new Supplier { Name = "Prime Distribution", Website = "https://primedist.example.com", ContactEmail = "sales@primedist.example.com" }, + new Supplier { Name = "Atlas Logistics", Website = "https://atlaslogistics.example.com", ContactEmail = "contact@atlaslogistics.example.com" } + }; + await context.Suppliers.AddRangeAsync(suppliers); + await context.SaveChangesAsync(); + + var supplierIds = await context.Suppliers.Select(s => s.Id).ToListAsync(); + context.Brands.RemoveRange(context.Brands); + var brandNames = sourceItems.Select(x => x.Brand).Distinct().ToList(); await context.Brands.AddRangeAsync( - sourceItems.Select(x => x.Brand).Distinct().Select(brandName => new Brand { Name = brandName })); + brandNames.Select((brandName, i) => new Brand + { + Name = brandName, + SupplierId = supplierIds[i % supplierIds.Count] + })); context.ProductTypes.RemoveRange(context.ProductTypes); await context.ProductTypes.AddRangeAsync( diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Brand.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Brand.cs index 5acecb76d5a..3d7ba745bbe 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Brand.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Brand.cs @@ -11,5 +11,9 @@ public sealed class Brand [Required] public string Name { get; set; } = null!; + public int SupplierId { get; set; } + + public Supplier? Supplier { get; set; } + public ICollection Products { get; set; } = []; } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Supplier.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Supplier.cs new file mode 100644 index 00000000000..e47de99ba97 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Supplier.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace HotChocolate.Data.Models; + +public sealed class Supplier +{ + public int Id { get; set; } + + [Required] + public string Name { get; set; } = null!; + + public string? Website { get; set; } + + public string? ContactEmail { get; set; } + + public ICollection Brands { get; set; } = []; +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs index 12cceaf20e9..99aba01a60f 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs @@ -50,4 +50,25 @@ public static async Task> GetProductCountAsync( return brands.Select(b => counts.GetValueOrDefault(b.Id, 0)).ToList(); } + + [BindMember(nameof(Brand.SupplierId))] + [BatchResolver] + public static async Task> GetSupplierAsync( + [Parent(requires: nameof(Brand.SupplierId))] List brands, + QueryContext query, + [Service] CatalogContext context, + CancellationToken cancellationToken) + { + var supplierIds = brands.Select(b => b.SupplierId).Distinct().ToList(); + + var queryable = context.Suppliers + .Where(s => supplierIds.Contains(s.Id)) + .With(query.Include(s => s.Id)); + PagingQueryInterceptor.Publish(queryable); + + var suppliers = await queryable + .ToDictionaryAsync(s => s.Id, cancellationToken); + + return brands.Select(b => suppliers.GetValueOrDefault(b.SupplierId)).ToList(); + } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql index dcfd7f1f998..8fab1f72f31 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql @@ -19,6 +19,7 @@ type BillingStatementTransaction implements StatementTransaction { type Brand implements Node { products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): BrandProductsConnection! @listSize(assumedSize: 50, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 10, sizedFields: ["edges", "nodes"], requireOneSlicingArgument: false) @cost(weight: "10") productCount: Int! @cost(weight: "10") + supplier: Supplier @cost(weight: "10") id: ID! name: String! } @@ -182,6 +183,14 @@ type SingleProperty { id: String! } +type Supplier { + id: Int! + name: String! + website: String + contactEmail: String + brands: [Brand!]! +} + input BooleanOperationFilterInput { eq: Boolean @cost(weight: "10") neq: Boolean @cost(weight: "10") @@ -192,6 +201,7 @@ input BrandFilterInput { or: [BrandFilterInput!] id: IntOperationFilterInput name: StringOperationFilterInput + supplier: SupplierFilterInput products: ListFilterInputTypeOfProductFilterInput } @@ -235,6 +245,13 @@ input IntOperationFilterInput { nlte: Int @cost(weight: "10") } +input ListFilterInputTypeOfBrandFilterInput { + all: BrandFilterInput @cost(weight: "10") + none: BrandFilterInput @cost(weight: "10") + some: BrandFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + input ListFilterInputTypeOfProductFilterInput { all: ProductFilterInput @cost(weight: "10") none: ProductFilterInput @cost(weight: "10") @@ -289,6 +306,16 @@ input StringOperationFilterInput { nendsWith: String @cost(weight: "20") } +input SupplierFilterInput { + and: [SupplierFilterInput!] + or: [SupplierFilterInput!] + id: IntOperationFilterInput + name: StringOperationFilterInput + website: StringOperationFilterInput + contactEmail: StringOperationFilterInput + brands: ListFilterInputTypeOfBrandFilterInput +} + "Defines the possible serialization types for GraphQL scalar values." enum ScalarSerializationType { "The scalar serializes to a string value." @@ -329,7 +356,7 @@ type User @internal { id: ID! name: String! } - + directive @internal on OBJECT | FIELD_DEFINITION """ directive @internal on OBJECT | FIELD_DEFINITION @@ -350,11 +377,11 @@ directive @serializeAs("The primitive type a scalar is serialized to." type: [Sc By default, only a single source schema is allowed to contribute a particular field to an object type. - + This prevents source schemas from inadvertently defining similarly named fields that are not semantically equivalent. - + Fields must be explicitly marked as @shareable to allow multiple source schemas to define them, ensuring that the decision to serve a field from more than one source schema is intentional and coordinated. diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET10_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET10_0.md new file mode 100644 index 00000000000..ccad50f6aab --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET10_0.md @@ -0,0 +1,73 @@ +# Query_Brands_With_BatchResolver_Supplier + +## Result + +```json +{ + "data": { + "brands": { + "nodes": [ + { + "id": "QnJhbmQ6MTE=", + "name": "Zephyr", + "supplier": { + "name": "Prime Distribution", + "website": "https://primedist.example.com" + } + }, + { + "id": "QnJhbmQ6MTM=", + "name": "XE", + "supplier": { + "name": "Global Supply Co.", + "website": "https://globalsupply.example.com" + } + }, + { + "id": "QnJhbmQ6Mw==", + "name": "WildRunner", + "supplier": { + "name": "Atlas Logistics", + "website": "https://atlaslogistics.example.com" + } + }, + { + "id": "QnJhbmQ6Nw==", + "name": "Solstix", + "supplier": { + "name": "Global Supply Co.", + "website": "https://globalsupply.example.com" + } + }, + { + "id": "QnJhbmQ6Ng==", + "name": "Raptor Elite", + "supplier": { + "name": "Atlas Logistics", + "website": "https://atlaslogistics.example.com" + } + } + ] + } + } +} +``` + +## Query 1 + +```sql +-- @p='6' +SELECT b."Id", b."Name", b."SupplierId" +FROM "Brands" AS b +ORDER BY b."Name" DESC, b."Id" +LIMIT @p +``` + +## Query 2 + +```sql +-- @supplierIds={ '2', '1', '3' } (DbType = Object) +SELECT s."Name", s."Website", s."Id" +FROM "Suppliers" AS s +WHERE s."Id" = ANY (@supplierIds) +``` diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET8_0.md new file mode 100644 index 00000000000..4322c3e7471 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET8_0.md @@ -0,0 +1,73 @@ +# Query_Brands_With_BatchResolver_Supplier + +## Result + +```json +{ + "data": { + "brands": { + "nodes": [ + { + "id": "QnJhbmQ6MTE=", + "name": "Zephyr", + "supplier": { + "name": "Prime Distribution", + "website": "https://primedist.example.com" + } + }, + { + "id": "QnJhbmQ6MTM=", + "name": "XE", + "supplier": { + "name": "Global Supply Co.", + "website": "https://globalsupply.example.com" + } + }, + { + "id": "QnJhbmQ6Mw==", + "name": "WildRunner", + "supplier": { + "name": "Atlas Logistics", + "website": "https://atlaslogistics.example.com" + } + }, + { + "id": "QnJhbmQ6Nw==", + "name": "Solstix", + "supplier": { + "name": "Global Supply Co.", + "website": "https://globalsupply.example.com" + } + }, + { + "id": "QnJhbmQ6Ng==", + "name": "Raptor Elite", + "supplier": { + "name": "Atlas Logistics", + "website": "https://atlaslogistics.example.com" + } + } + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__p_0='6' +SELECT b."Id", b."Name", b."SupplierId" +FROM "Brands" AS b +ORDER BY b."Name" DESC, b."Id" +LIMIT @__p_0 +``` + +## Query 2 + +```sql +-- @__supplierIds_0={ '2', '1', '3' } (DbType = Object) +SELECT s."Name", s."Website", s."Id" +FROM "Suppliers" AS s +WHERE s."Id" = ANY (@__supplierIds_0) +``` diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET9_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET9_0.md new file mode 100644 index 00000000000..4322c3e7471 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_With_BatchResolver_Supplier_NET9_0.md @@ -0,0 +1,73 @@ +# Query_Brands_With_BatchResolver_Supplier + +## Result + +```json +{ + "data": { + "brands": { + "nodes": [ + { + "id": "QnJhbmQ6MTE=", + "name": "Zephyr", + "supplier": { + "name": "Prime Distribution", + "website": "https://primedist.example.com" + } + }, + { + "id": "QnJhbmQ6MTM=", + "name": "XE", + "supplier": { + "name": "Global Supply Co.", + "website": "https://globalsupply.example.com" + } + }, + { + "id": "QnJhbmQ6Mw==", + "name": "WildRunner", + "supplier": { + "name": "Atlas Logistics", + "website": "https://atlaslogistics.example.com" + } + }, + { + "id": "QnJhbmQ6Nw==", + "name": "Solstix", + "supplier": { + "name": "Global Supply Co.", + "website": "https://globalsupply.example.com" + } + }, + { + "id": "QnJhbmQ6Ng==", + "name": "Raptor Elite", + "supplier": { + "name": "Atlas Logistics", + "website": "https://atlaslogistics.example.com" + } + } + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__p_0='6' +SELECT b."Id", b."Name", b."SupplierId" +FROM "Brands" AS b +ORDER BY b."Name" DESC, b."Id" +LIMIT @__p_0 +``` + +## Query 2 + +```sql +-- @__supplierIds_0={ '2', '1', '3' } (DbType = Object) +SELECT s."Name", s."Website", s."Id" +FROM "Suppliers" AS s +WHERE s."Id" = ANY (@__supplierIds_0) +```