Skip to content

Commit 36d5508

Browse files
committed
Rewrite NpgsqlDatabaseCreator.Exists() to do SELECT 1 (#3648)
Fixes #3646 (cherry picked from commit 3e175da)
1 parent 536141b commit 36d5508

File tree

2 files changed

+79
-73
lines changed

2 files changed

+79
-73
lines changed

src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs

Lines changed: 77 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
1313
public class NpgsqlDatabaseCreator(
1414
RelationalDatabaseCreatorDependencies dependencies,
1515
INpgsqlRelationalConnection connection,
16-
IRawSqlCommandBuilder rawSqlCommandBuilder,
17-
IRelationalConnectionDiagnosticsLogger connectionLogger)
16+
IRawSqlCommandBuilder rawSqlCommandBuilder)
1817
: RelationalDatabaseCreator(dependencies)
1918
{
2019
/// <summary>
@@ -174,7 +173,41 @@ private IReadOnlyList<MigrationCommand> CreateCreateOperations()
174173
/// doing so can result in application failures when updating to a new Entity Framework Core release.
175174
/// </summary>
176175
public override bool Exists()
177-
=> Exists(async: false).GetAwaiter().GetResult();
176+
=> Dependencies.ExecutionStrategy.Execute(() =>
177+
{
178+
using var _ = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
179+
var opened = false;
180+
181+
try
182+
{
183+
connection.Open(errorsExpected: true);
184+
opened = true;
185+
186+
rawSqlCommandBuilder
187+
.Build("SELECT 1")
188+
.ExecuteNonQuery(
189+
new RelationalCommandParameterObject(
190+
connection,
191+
parameterValues: null,
192+
readerColumns: null,
193+
Dependencies.CurrentContext.Context,
194+
Dependencies.CommandLogger,
195+
CommandSource.Migrations));
196+
197+
return true;
198+
}
199+
catch (Exception e) when (IsDoesNotExist(e))
200+
{
201+
return false;
202+
}
203+
finally
204+
{
205+
if (opened)
206+
{
207+
connection.Close();
208+
}
209+
}
210+
});
178211

179212
/// <summary>
180213
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -183,87 +216,61 @@ public override bool Exists()
183216
/// doing so can result in application failures when updating to a new Entity Framework Core release.
184217
/// </summary>
185218
public override Task<bool> ExistsAsync(CancellationToken cancellationToken = default)
186-
=> Exists(async: true, cancellationToken);
187-
188-
private async Task<bool> Exists(bool async, CancellationToken cancellationToken = default)
189-
{
190-
var logger = connectionLogger;
191-
var startTime = DateTimeOffset.UtcNow;
192-
193-
var interceptionResult = async
194-
? await logger.ConnectionOpeningAsync(connection, startTime, cancellationToken).ConfigureAwait(false)
195-
: logger.ConnectionOpening(connection, startTime);
196-
197-
if (interceptionResult.IsSuppressed)
219+
=> Dependencies.ExecutionStrategy.ExecuteAsync(async ct =>
198220
{
199-
// If the connection attempt was suppressed by an interceptor, assume that the interceptor took care of all the opening
200-
// details, and the database exists.
201-
return true;
202-
}
221+
using var _ = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
222+
var opened = false;
203223

204-
// When checking whether a database exists, pooling must be off, otherwise we may
205-
// attempt to reuse a pooled connection, which may be broken (this happened in the tests).
206-
// If Pooling is off, but Multiplexing is on - NpgsqlConnectionStringBuilder.Validate will throw,
207-
// so we turn off Multiplexing as well.
208-
var unpooledCsb = new NpgsqlConnectionStringBuilder(connection.ConnectionString) { Pooling = false, Multiplexing = false };
224+
try
225+
{
226+
await connection.OpenAsync(cancellationToken, errorsExpected: true).ConfigureAwait(false);
227+
opened = true;
209228

210-
using var _ = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
211-
var unpooledRelationalConnection =
212-
await connection.CloneWith(unpooledCsb.ToString(), async, cancellationToken).ConfigureAwait(false);
229+
await rawSqlCommandBuilder
230+
.Build("SELECT 1")
231+
.ExecuteNonQueryAsync(
232+
new RelationalCommandParameterObject(
233+
connection,
234+
parameterValues: null,
235+
readerColumns: null,
236+
Dependencies.CurrentContext.Context,
237+
Dependencies.CommandLogger,
238+
CommandSource.Migrations),
239+
cancellationToken)
240+
.ConfigureAwait(false);
213241

214-
try
215-
{
216-
if (async)
242+
return true;
243+
}
244+
catch (Exception e) when (IsDoesNotExist(e))
217245
{
218-
await unpooledRelationalConnection.OpenAsync(errorsExpected: true, cancellationToken: cancellationToken)
219-
.ConfigureAwait(false);
246+
return false;
220247
}
221-
else
248+
finally
222249
{
223-
unpooledRelationalConnection.Open(errorsExpected: true);
250+
if (opened)
251+
{
252+
await connection.CloseAsync().ConfigureAwait(false);
253+
}
224254
}
255+
}, cancellationToken);
225256

226-
return true;
227-
}
228-
catch (PostgresException e)
257+
private static bool IsDoesNotExist(Exception exception)
258+
=> exception switch
229259
{
230-
if (IsDoesNotExist(e))
231-
{
232-
return false;
233-
}
260+
// Login failed is thrown when database does not exist (See Issue #776)
261+
PostgresException { SqlState: "3D000" }
262+
=> true,
234263

235-
throw;
236-
}
237-
catch (NpgsqlException e) when (
238264
// This can happen when Npgsql attempts to connect to multiple hosts
239-
e.InnerException is AggregateException ae && ae.InnerExceptions.Any(ie => ie is PostgresException pe && IsDoesNotExist(pe)))
240-
{
241-
return false;
242-
}
243-
catch (NpgsqlException e) when (
244-
e.InnerException is IOException { InnerException: SocketException { SocketErrorCode: SocketError.ConnectionReset } })
245-
{
265+
NpgsqlException { InnerException: AggregateException ae } when ae.InnerExceptions.Any(ie => ie is PostgresException { SqlState: "3D000" })
266+
=> true,
267+
246268
// Pretty awful hack around #104
247-
return false;
248-
}
249-
finally
250-
{
251-
if (async)
252-
{
253-
await unpooledRelationalConnection.CloseAsync().ConfigureAwait(false);
254-
await unpooledRelationalConnection.DisposeAsync().ConfigureAwait(false);
255-
}
256-
else
257-
{
258-
unpooledRelationalConnection.Close();
259-
unpooledRelationalConnection.Dispose();
260-
}
261-
}
262-
}
269+
NpgsqlException { InnerException: IOException { InnerException: SocketException { SocketErrorCode: SocketError.ConnectionReset } } }
270+
=> true,
263271

264-
// Login failed is thrown when database does not exist (See Issue #776)
265-
private static bool IsDoesNotExist(PostgresException exception)
266-
=> exception.SqlState == "3D000";
272+
_ => false
273+
};
267274

268275
/// <summary>
269276
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,9 +632,8 @@ public class Blog
632632
public class TestDatabaseCreator(
633633
RelationalDatabaseCreatorDependencies dependencies,
634634
INpgsqlRelationalConnection connection,
635-
IRawSqlCommandBuilder rawSqlCommandBuilder,
636-
IRelationalConnectionDiagnosticsLogger connectionLogger)
637-
: NpgsqlDatabaseCreator(dependencies, connection, rawSqlCommandBuilder, connectionLogger)
635+
IRawSqlCommandBuilder rawSqlCommandBuilder)
636+
: NpgsqlDatabaseCreator(dependencies, connection, rawSqlCommandBuilder)
638637
{
639638
public bool HasTablesBase()
640639
=> HasTables();

0 commit comments

Comments
 (0)