Skip to content

Commit f85e8d7

Browse files
committed
Merge branch 'main' into efcore-dotnet-10
2 parents e17143a + dbdb211 commit f85e8d7

19 files changed

+484
-29
lines changed

Google.Cloud.EntityFrameworkCore.Spanner.Tests/EntityFrameworkSessionLeakMockServerTests.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,21 +2201,43 @@ await Repeat(() =>
22012201
}
22022202

22032203
[Fact]
2204-
public async Task OnlyDisposingReadOnlyTransactionWithoutCommitting_LeaksSession()
2204+
public async Task OnlyDisposingReadOnlyTransactionWithoutCommitting_DoesNotLeakSession()
22052205
{
22062206
AddFindSingerResult($"SELECT `s`.`SingerId`, `s`.`BirthDate`, `s`.`FirstName`, `s`.`FullName`," +
22072207
$" `s`.`LastName`, `s`.`Picture`{Environment.NewLine}FROM `Singers` AS `s`{Environment.NewLine}" +
22082208
$"WHERE `s`.`SingerId` = @p{Environment.NewLine}LIMIT 1");
22092209

2210-
var exception = await Assert.ThrowsAsync<SpannerException>(() => Repeat(async () =>
2210+
await Repeat(async () =>
22112211
{
22122212
using var db = CreateContext();
22132213
// NOTE: This transaction is being disposed, but it's not being committed or rolled back.
22142214
using var transaction = await db.Database.BeginReadOnlyTransactionAsync();
22152215
Assert.NotNull(await db.Singers.FindAsync(1L));
22162216
// Note: No Commit or Rollback
2217-
}));
2218-
Assert.Equal(ErrorCode.ResourceExhausted, exception.ErrorCode);
2217+
});
2218+
}
2219+
2220+
[Fact]
2221+
public async Task EmptyReadWriteTransactionWithTagDoesNotLeakSession()
2222+
{
2223+
await using var db = CreateContext();
2224+
await Repeat(async () =>
2225+
{
2226+
// Execute an empty read/write transaction with a tag.
2227+
await using var transaction = await db.Database.BeginTransactionAsync("some_tag");
2228+
await transaction.CommitAsync();
2229+
});
2230+
}
2231+
2232+
[Fact]
2233+
public async Task StartingTwoTransactionsOnSameConnectionFailsAndDoesNotLeakSession()
2234+
{
2235+
await using var db = CreateContext();
2236+
await Repeat(async () =>
2237+
{
2238+
await using var transaction1 = await db.Database.BeginTransactionAsync("some_tag1");
2239+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await db.Database.BeginTransactionAsync("some_tag2"));
2240+
});
22192241
}
22202242

22212243
private void AddFindSingerResult(string sql)

Google.Cloud.EntityFrameworkCore.Spanner.Tests/MigrationTests/GenerateCreateScriptTest.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ INTERLEAVE IN PARENT `Albums` ON DELETE NO ACTION
169169
170170
CREATE INDEX `AlbumsByAlbumTitle2` ON `Albums` (`Title`) STORING (`MarketingBudget`, `ReleaseDate`)
171171
172+
CREATE INDEX `AlbumsBySingerIdReleaseDateMarketingBudgetTitle` ON `Albums` (`SingerId`, `ReleaseDate` DESC, `MarketingBudget` DESC, `Title`)
173+
174+
CREATE INDEX `idx_concerts_singerId_startTime` ON `Concerts` (`SingerId`, `StartTime`),
175+
INTERLEAVE IN `Singers`
176+
172177
CREATE INDEX `Idx_Singers_FullName` ON `Singers` (`FullName`)
173178
174179
CREATE NULL_FILTERED INDEX `IDX_TableWithAllColumnTypes_ColDate_ColCommitTS` ON `TableWithAllColumnTypes` (`ColDate`, `ColCommitTS`)
@@ -279,6 +284,11 @@ INTERLEAVE IN PARENT `Albums` ON DELETE NO ACTION
279284
280285
CREATE INDEX `AlbumsByAlbumTitle2` ON `Albums` (`Title`) STORING (`MarketingBudget`, `ReleaseDate`)
281286
287+
CREATE INDEX `AlbumsBySingerIdReleaseDateMarketingBudgetTitle` ON `Albums` (`SingerId`, `ReleaseDate` DESC, `MarketingBudget` DESC, `Title`)
288+
289+
CREATE INDEX `idx_concerts_singerId_startTime` ON `Concerts` (`SingerId`, `StartTime`),
290+
INTERLEAVE IN `Singers`
291+
282292
CREATE INDEX `Idx_Singers_FullName` ON `Singers` (`FullName`)
283293
284294
CREATE NULL_FILTERED INDEX `IDX_TableWithAllColumnTypes_ColDate_ColCommitTS` ON `TableWithAllColumnTypes` (`ColDate`, `ColCommitTS`)
@@ -389,6 +399,11 @@ INTERLEAVE IN PARENT `Albums` ON DELETE NO ACTION
389399
390400
CREATE INDEX `AlbumsByAlbumTitle2` ON `Albums` (`Title`) STORING (`MarketingBudget`, `ReleaseDate`)
391401
402+
CREATE INDEX `AlbumsBySingerIdReleaseDateMarketingBudgetTitle` ON `Albums` (`SingerId`, `ReleaseDate` DESC, `MarketingBudget` DESC, `Title`)
403+
404+
CREATE INDEX `idx_concerts_singerId_startTime` ON `Concerts` (`SingerId`, `StartTime`),
405+
INTERLEAVE IN `Singers`
406+
392407
CREATE INDEX `Idx_Singers_FullName` ON `Singers` (`FullName`)
393408
394409
CREATE NULL_FILTERED INDEX `IDX_TableWithAllColumnTypes_ColDate_ColCommitTS` ON `TableWithAllColumnTypes` (`ColDate`, `ColCommitTS`)

Google.Cloud.EntityFrameworkCore.Spanner.Tests/MigrationTests/MigrationMockServerTests.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
using Microsoft.EntityFrameworkCore;
1717
using Microsoft.EntityFrameworkCore.Migrations;
1818
using System;
19+
using System.Collections.Generic;
20+
using System.Linq;
21+
using System.Threading.Tasks;
22+
using Google.Cloud.EntityFrameworkCore.Spanner.Extensions;
23+
using Google.Cloud.Spanner.V1;
1924
using Xunit;
2025

2126
namespace Google.Cloud.EntityFrameworkCore.Spanner.Tests.MigrationTests
@@ -33,6 +38,7 @@ public MigrationMockServerTests(SpannerMockServerFixture service)
3338
{
3439
_fixture = service;
3540
service.SpannerMock.Reset();
41+
service.DatabaseAdminMock.Reset();
3642
}
3743

3844
private string ConnectionString => $"Data Source=projects/p1/instances/i1/databases/d1;Host={_fixture.Host};Port={_fixture.Port}";
@@ -54,6 +60,14 @@ public void TestMigrateUsesDdlBatch()
5460
using var db = new MockMigrationSampleDbContext(ConnectionString);
5561
db.Database.Migrate();
5662

63+
var requests = _fixture.SpannerMock.Requests.ToList();
64+
// There is one BatchDmlRequest per migration.
65+
var batchDmlRequests = requests.OfType<ExecuteBatchDmlRequest>();
66+
Assert.Equal(2, batchDmlRequests.Count());
67+
// Each BatchDmlRequest is executed as a separate transaction.
68+
var commitRequests = requests.OfType<CommitRequest>().ToList();
69+
Assert.Equal(2, commitRequests.Count);
70+
5771
Assert.Collection(_fixture.DatabaseAdminMock.Requests,
5872
// The initial request will create an empty database and then create the migrations history table.
5973
request => Assert.IsType<CreateDatabaseRequest>(request),
@@ -101,5 +115,83 @@ public void TestMigrateUsesDdlBatch()
101115
}
102116
);
103117
}
118+
119+
[Fact]
120+
public async Task TestStartMigrateAsync()
121+
{
122+
var version = typeof(Migration).Assembly.GetName().Version ?? new Version();
123+
var formattedVersion = $"{version.Major}.{version.Minor}.{version.Build}";
124+
_fixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1", StatementResult.CreateSelect1ResultSet());
125+
_fixture.SpannerMock.AddOrUpdateStatementResult(
126+
"SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema = '' AND table_name = 'EFMigrationsHistory')",
127+
StatementResult.CreateSelectTrueResultSet());
128+
_fixture.SpannerMock.AddOrUpdateStatementResult($"SELECT `MigrationId`, `ProductVersion`{Environment.NewLine}" +
129+
$"FROM `EFMigrationsHistory`{Environment.NewLine}" +
130+
$"ORDER BY `MigrationId`",
131+
StatementResult.CreateResultSet(new List<Tuple<Cloud.Spanner.V1.TypeCode, string>>
132+
{
133+
Tuple.Create(Cloud.Spanner.V1.TypeCode.String, "MigrationId"),
134+
Tuple.Create(Cloud.Spanner.V1.TypeCode.String, "ProductVersion"),
135+
},
136+
new List<object[]>
137+
{
138+
new object[] { "20210309110233_Initial", formattedVersion },
139+
}));
140+
_fixture.SpannerMock.AddOrUpdateStatementResult(
141+
$"INSERT INTO `EFMigrationsHistory` (`MigrationId`, `ProductVersion`)\nVALUES ('''20210830_V2''', '''{formattedVersion}''')",
142+
StatementResult.CreateUpdateCount(1)
143+
);
144+
await using var db = new MockMigrationSampleDbContext(ConnectionString);
145+
// This starts an asynchronous database migration, but does not wait for the DDL operation to finish.
146+
await db.Database.StartMigrateAsync();
147+
148+
Assert.Collection(_fixture.DatabaseAdminMock.Requests,
149+
// Each migration will be executed as a separate DDL batch.
150+
request =>
151+
{
152+
var update = request as UpdateDatabaseDdlRequest;
153+
Assert.NotNull(update);
154+
Assert.Collection(update.Statements,
155+
// Entity Framework 10 executes this regardless whether database provider has already told it
156+
// that the table exists or not.
157+
sql => Assert.StartsWith("CREATE TABLE IF NOT EXISTS `EFMigrationsHistory`", sql)
158+
);
159+
}, request =>
160+
{
161+
var update = request as UpdateDatabaseDdlRequest;
162+
Assert.NotNull(update);
163+
Assert.Collection(update.Statements,
164+
sql => Assert.StartsWith(" DROP INDEX `IDX_TableWithAllColumnTypes_ColDate_ColCommitTS`", sql),
165+
sql => Assert.StartsWith("DROP TABLE `TableWithAllColumnTypes`", sql),
166+
sql => Assert.StartsWith("CREATE TABLE `OtherSequenceKind` (\n `Id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (NON_EXISTING_KIND),\n", sql),
167+
sql => Assert.StartsWith("CREATE TABLE `NoSequenceKind` (\n `Id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY,\n", sql),
168+
sql => Assert.StartsWith("CREATE TABLE `GenerationStrategyAlways` (\n `Id` INT64 NOT NULL GENERATED ALWAYS AS IDENTITY (BIT_REVERSED_POSITIVE),\n", sql),
169+
sql => Assert.StartsWith("CREATE TABLE `AutoIncrement` (\n `Id` INT64 NOT NULL AUTO_INCREMENT,\n", sql)
170+
);
171+
}
172+
);
173+
}
174+
175+
[Fact]
176+
public async Task TestStartDdlAsync()
177+
{
178+
await using var db = new MockMigrationSampleDbContext(ConnectionString);
179+
await db.Database.StartDdlAsync([
180+
"create table my_table (id int64 primary key, value string(max))",
181+
"create index my_index on my_table (value)",
182+
]);
183+
184+
Assert.Collection(_fixture.DatabaseAdminMock.Requests,
185+
request =>
186+
{
187+
var update = request as UpdateDatabaseDdlRequest;
188+
Assert.NotNull(update);
189+
Assert.Collection(update.Statements,
190+
sql => Assert.Equal("create table my_table (id int64 primary key, value string(max))", sql),
191+
sql => Assert.Equal("create index my_index on my_table (value)", sql)
192+
);
193+
}
194+
);
195+
}
104196
}
105197
}

Google.Cloud.EntityFrameworkCore.Spanner.Tests/MigrationTests/Models/SpannerMigrationSampleDbContext.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
5454
entity.HasIndex(e => e.Title)
5555
.HasDatabaseName("AlbumsByAlbumTitle2")
5656
.Storing(a => new { a.MarketingBudget, a.ReleaseDate });
57+
entity.HasIndex(e => new { e.SingerId, e.ReleaseDate, e.MarketingBudget, e.Title })
58+
.IsDescending(false, true, true, false)
59+
.HasDatabaseName("AlbumsBySingerIdReleaseDateMarketingBudgetTitle");
5760

5861
entity.HasOne(d => d.Singer)
5962
.WithMany(p => p.Albums)
@@ -76,6 +79,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
7679
.HasForeignKey(d => d.SingerId)
7780
.OnDelete(DeleteBehavior.Cascade)
7881
.HasConstraintName("FK_Concerts_Singers");
82+
entity.HasIndex(concert => new { concert.SingerId, concert.StartTime })
83+
.HasDatabaseName("idx_concerts_singerId_startTime")
84+
.InterleaveIn(typeof(Singers));
7985

8086
entity.HasOne(d => d.VenueCodeNavigation)
8187
.WithMany(p => p.Concerts)

Google.Cloud.EntityFrameworkCore.Spanner.Tests/MockSpannerServer.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ internal static StatementResult CreateSelect1ResultSet()
6868
return CreateSingleColumnResultSet(new V1.Type { Code = V1.TypeCode.Int64 }, "COL1", 1);
6969
}
7070

71+
internal static StatementResult CreateSelectTrueResultSet()
72+
{
73+
return CreateSingleColumnResultSet(new V1.Type { Code = V1.TypeCode.Bool }, "C", true);
74+
}
75+
7176
internal static StatementResult CreateSingleColumnResultSet(V1.Type type, string col, params object[] values)
7277
=> CreateSingleColumnResultSet(null, type, col, values);
7378

@@ -721,6 +726,8 @@ public class MockDatabaseAdminService : DatabaseAdmin.DatabaseAdminBase
721726
private readonly ConcurrentQueue<IMessage> _requests = new ConcurrentQueue<IMessage>();
722727

723728
public IEnumerable<IMessage> Requests => new List<IMessage>(_requests).AsReadOnly();
729+
730+
public void Reset() => _requests.Clear();
724731

725732
public override Task<Operation> CreateDatabase(CreateDatabaseRequest request, ServerCallContext context)
726733
{

Google.Cloud.EntityFrameworkCore.Spanner/Extensions/InterleaveInParentExtension.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,19 @@ public static EntityTypeBuilder<TEntity> InterleaveInParent<TEntity>(
3838
builder.Metadata.AddAnnotation(SpannerAnnotationNames.InterleaveInParentOnDelete, onDelete);
3939
return builder;
4040
}
41+
42+
/// <summary>
43+
/// Indicates that an index should be interleaved in a table.
44+
/// </summary>
45+
/// <param name="builder">The index builder to modify</param>
46+
/// <param name="interleaveInEntity">The entity (table) that the index should be interleaved in</param>
47+
/// <typeparam name="TEntity">The entity type that owns the index</typeparam>
48+
/// <returns> The same builder instance so that multiple configuration calls can be chained.</returns>
49+
public static IndexBuilder<TEntity> InterleaveIn<TEntity>(
50+
this IndexBuilder<TEntity> builder, System.Type interleaveInEntity) where TEntity : class
51+
{
52+
builder.Metadata.AddAnnotation(SpannerAnnotationNames.InterleaveIn, interleaveInEntity.FullName);
53+
return builder;
54+
}
4155
}
4256
}

0 commit comments

Comments
 (0)