From 3d054cb912f9b448f6e4a686dba3535929f37d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20=C3=96berg?= Date: Sun, 30 Aug 2026 21:43:05 +0200 Subject: [PATCH] Fix relation enumeration naming for 0.10 Rename the keyed AsEnumerable member to AsKeyValuePairs on the relation interface, implementation, and existing mock. This restores standard LINQ AsEnumerable binding to rows while retaining explicit primary-key/row enumeration and its existing synchronous loading behavior. Extend relation compliance tests to cover interface and concrete row views, deferred loading, empty relations, keyed identity, and composite keys. This is an intentional source and binary breaking change for 0.10. --- .../EmployeesRelationAndThreadingTests.cs | 17 +++++++++ .../MultipleForeignKeyRelationTests.cs | 37 +++++++++++++++++-- src/DataLinq/Instances/ImmutableRelation.cs | 11 ++++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs b/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs index 7ee0e214..3a4d0318 100644 --- a/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs +++ b/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using DataLinq.Instances; using DataLinq.Tests.Models.Employees; using DataLinq.Testing; @@ -53,6 +54,22 @@ public async Task Relations_DepartmentManagersLazyLoad_ResolvesCollection(TestPr await Assert.That(department.Managers).IsNotEmpty(); await Assert.That(department.Managers.All(x => x.Department.DeptNo == department.Department.DeptNo)).IsTrue(); + + var relation = department.Department.Managers; + IEnumerable rows = relation.AsEnumerable(); + var expectedByKey = rows.ToDictionary(manager => manager.PrimaryKeys()); + + // Manager keys are composite; keyed enumeration must not use the parent department key. + relation.Clear(); + IEnumerable> keyedRows = relation.AsKeyValuePairs(); + var pairs = keyedRows.ToArray(); + await Assert.That(pairs.Length).IsEqualTo(expectedByKey.Count); + foreach (var pair in pairs) + { + await Assert.That(pair.Key).IsEqualTo(DataLinqKey.FromValues([pair.Value.dept_fk, pair.Value.emp_no])); + await Assert.That(ReferenceEquals(pair.Value, expectedByKey[pair.Key])).IsTrue(); + await Assert.That(ReferenceEquals(relation.Get(pair.Key), pair.Value)).IsTrue(); + } } [Test] diff --git a/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs b/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs index 171ba3ed..5b872f28 100644 --- a/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs +++ b/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DataLinq.Attributes; +using DataLinq.Diagnostics; using DataLinq.Instances; using DataLinq.Interfaces; using DataLinq.Metadata; @@ -12,6 +14,7 @@ namespace DataLinq.Tests.Compliance; public class MultipleForeignKeyRelationTests { [Test] + [NotInParallel] [Property(TestProviderAffinity.PropertyName, TestProviderAffinity.EveryProvider)] [MethodDataSource(typeof(TestProviderDataSources), nameof(TestProviderDataSources.ActiveProviders))] public async Task Transaction_MultipleForeignKeysToSameTable_LazyLoadsDistinctRelations(TestProviderDescriptor provider) @@ -40,18 +43,46 @@ public async Task Transaction_MultipleForeignKeysToSameTable_LazyLoadsDistinctRe await Assert.That(ReferenceEquals(createdBy, creator)).IsTrue(); await Assert.That(ReferenceEquals(approvedBy, approver)).IsTrue(); - var createdInvoices = creator.CreatedInvoices.ToArray(); + var relation = creator.CreatedInvoices; + var concreteRelation = (ImmutableRelation)relation; + var loadsBeforeRowViews = GetCollectionLoads(databaseScope.Database); + + // Both receiver types must bind to the standard row extension without loading. + IEnumerable rows = relation.AsEnumerable(); + IEnumerable concreteRows = concreteRelation.AsEnumerable(); + + await Assert.That(ReferenceEquals(rows, relation)).IsTrue(); + await Assert.That(ReferenceEquals(concreteRows, relation)).IsTrue(); + await Assert.That(GetCollectionLoads(databaseScope.Database)).IsEqualTo(loadsBeforeRowViews); + + var createdInvoices = rows.ToArray(); + await Assert.That(GetCollectionLoads(databaseScope.Database)).IsEqualTo(loadsBeforeRowViews + 1); var approvedInvoices = approver.ApprovedInvoices.ToArray(); await Assert.That(createdInvoices.Length).IsEqualTo(1); await Assert.That(approvedInvoices.Length).IsEqualTo(1); await Assert.That(ReferenceEquals(invoice, createdInvoices.Single())).IsTrue(); await Assert.That(ReferenceEquals(invoice, approvedInvoices.Single())).IsTrue(); - await Assert.That(creator.ApprovedInvoices).IsEmpty(); - await Assert.That(approver.CreatedInvoices).IsEmpty(); + + IEnumerable> keyedRows = relation.AsKeyValuePairs(); + var pair = keyedRows.Single(); + await Assert.That(pair.Key).IsEqualTo(DataLinqKey.FromValue(100)); + await Assert.That(ReferenceEquals(pair.Value, createdInvoices.Single())).IsTrue(); + await Assert.That(ReferenceEquals(relation.Get(pair.Key), pair.Value)).IsTrue(); + await Assert.That(ReferenceEquals(concreteRelation.AsKeyValuePairs().Single().Value, pair.Value)).IsTrue(); + + await Assert.That(creator.ApprovedInvoices.AsEnumerable()).IsEmpty(); + await Assert.That(creator.ApprovedInvoices.AsKeyValuePairs()).IsEmpty(); + await Assert.That(approver.CreatedInvoices.AsEnumerable()).IsEmpty(); + await Assert.That(approver.CreatedInvoices.AsKeyValuePairs()).IsEmpty(); transaction.Commit(); } + + private static long GetCollectionLoads(Database database) + => DataLinqMetrics.Snapshot().Providers + .Single(provider => provider.ProviderInstanceId == database.Provider.TelemetryInstanceId) + .Tables.Sum(table => table.Relations.CollectionLoads); } [Database("multiple_fk_relation")] diff --git a/src/DataLinq/Instances/ImmutableRelation.cs b/src/DataLinq/Instances/ImmutableRelation.cs index 7004ae04..a23a6cd7 100644 --- a/src/DataLinq/Instances/ImmutableRelation.cs +++ b/src/DataLinq/Instances/ImmutableRelation.cs @@ -21,7 +21,11 @@ public interface IImmutableRelation : IEnumerable where T : IModelInstance ImmutableArray Keys { get; } ImmutableArray Values { get; } - IEnumerable> AsEnumerable(); + /// + /// Returns relation rows paired with their primary keys. + /// Resolving the keyed collection may synchronously load the relation. + /// + IEnumerable> AsKeyValuePairs(); void Clear(); bool Any() => Count != 0; @@ -100,7 +104,7 @@ public ImmutableRelationMock(IEnumerable list) public ImmutableArray Values => throw new System.NotImplementedException(); - public IEnumerable> AsEnumerable() + public IEnumerable> AsKeyValuePairs() { throw new System.NotImplementedException(); } @@ -172,7 +176,8 @@ public class ImmutableRelation(TKey foreignKey, IDataSourceAccess dataS public ImmutableArray Keys => GetInstances().Keys; public int Count => GetValues().Length; public bool ContainsKey(DataLinqKey key) => GetInstances().ContainsKey(key); - public IEnumerable> AsEnumerable() => GetInstances().AsEnumerable(); + /// + public IEnumerable> AsKeyValuePairs() => GetInstances().AsEnumerable(); public FrozenDictionary ToFrozenDictionary() => GetInstances(); protected TableCache GetTableCache() => GetTableCache(GetDataSource());