66/// any release. You should only use it directly in your code with extreme caution and knowing that
77/// doing so can result in application failures when updating to a new Entity Framework Core release.
88/// </summary>
9- public class NpgsqlHistoryRepository : HistoryRepository
9+ public class NpgsqlHistoryRepository : HistoryRepository , IHistoryRepository
1010{
1111 /// <summary>
1212 /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -19,60 +19,29 @@ public NpgsqlHistoryRepository(HistoryRepositoryDependencies dependencies)
1919 {
2020 }
2121
22- // TODO: We override Exists() as a workaround for https://github.com/dotnet/efcore/issues/34569; this should be fixed on the EF side
23- // before EF 9.0 is released
24-
25- /// <summary>
26- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
27- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
28- /// any release. You should only use it directly in your code with extreme caution and knowing that
29- /// doing so can result in application failures when updating to a new Entity Framework Core release.
30- /// </summary>
31- public override bool Exists ( )
32- => Dependencies . DatabaseCreator . Exists ( )
33- && InterpretExistsResult (
34- Dependencies . RawSqlCommandBuilder . Build ( ExistsSql ) . ExecuteScalar (
35- new RelationalCommandParameterObject (
36- Dependencies . Connection ,
37- null ,
38- null ,
39- Dependencies . CurrentContext . Context ,
40- Dependencies . CommandLogger , CommandSource . Migrations ) ) ) ;
41-
4222 /// <summary>
4323 /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
4424 /// the same compatibility standards as public APIs. It may be changed or removed without notice in
4525 /// any release. You should only use it directly in your code with extreme caution and knowing that
4626 /// doing so can result in application failures when updating to a new Entity Framework Core release.
4727 /// </summary>
48- public override async Task < bool > ExistsAsync ( CancellationToken cancellationToken = default )
49- => await Dependencies . DatabaseCreator . ExistsAsync ( cancellationToken ) . ConfigureAwait ( false )
50- && InterpretExistsResult (
51- await Dependencies . RawSqlCommandBuilder . Build ( ExistsSql ) . ExecuteScalarAsync (
52- new RelationalCommandParameterObject (
53- Dependencies . Connection ,
54- null ,
55- null ,
56- Dependencies . CurrentContext . Context ,
57- Dependencies . CommandLogger , CommandSource . Migrations ) ,
58- cancellationToken ) . ConfigureAwait ( false ) ) ;
28+ public override LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior . Transaction ;
5929
6030 /// <summary>
6131 /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
6232 /// the same compatibility standards as public APIs. It may be changed or removed without notice in
6333 /// any release. You should only use it directly in your code with extreme caution and knowing that
6434 /// doing so can result in application failures when updating to a new Entity Framework Core release.
6535 /// </summary>
66- public override IDisposable GetDatabaseLock ( )
36+ public override IMigrationsDatabaseLock AcquireDatabaseLock ( )
6737 {
68- // TODO: There are issues with the current lock implementation in EF - most importantly, the lock isn't acquired within a
69- // transaction so we can't use e.g. LOCK TABLE. This should be fixed for rc.1, see #34439.
38+ Dependencies . MigrationsLogger . AcquiringMigrationLock ( ) ;
7039
71- // Dependencies.RawSqlCommandBuilder
72- // .Build($"LOCK TABLE {Dependencies.SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} IN ACCESS EXCLUSIVE MODE")
73- // .ExecuteNonQuery(CreateRelationalCommandParameters());
40+ Dependencies . RawSqlCommandBuilder
41+ . Build ( $ "LOCK TABLE { Dependencies . SqlGenerationHelper . DelimitIdentifier ( TableName , TableSchema ) } IN ACCESS EXCLUSIVE MODE")
42+ . ExecuteNonQuery ( CreateRelationalCommandParameters ( ) ) ;
7443
75- return new DummyDisposable ( ) ;
44+ return new NpgsqlMigrationDatabaseLock ( this ) ;
7645 }
7746
7847 /// <summary>
@@ -81,19 +50,24 @@ public override IDisposable GetDatabaseLock()
8150 /// any release. You should only use it directly in your code with extreme caution and knowing that
8251 /// doing so can result in application failures when updating to a new Entity Framework Core release.
8352 /// </summary>
84- public override Task < IAsyncDisposable > GetDatabaseLockAsync ( CancellationToken cancellationToken = default )
53+ public override async Task < IMigrationsDatabaseLock > AcquireDatabaseLockAsync ( CancellationToken cancellationToken = default )
8554 {
86- // TODO: There are issues with the current lock implementation in EF - most importantly, the lock isn't acquired within a
87- // transaction so we can't use e.g. LOCK TABLE. This should be fixed for rc.1, see #34439.
88-
89- // await Dependencies.RawSqlCommandBuilder
90- // .Build($"LOCK TABLE {Dependencies.SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} IN ACCESS EXCLUSIVE MODE")
91- // .ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken)
92- // .ConfigureAwait(false);
55+ await Dependencies . RawSqlCommandBuilder
56+ . Build ( $ "LOCK TABLE { Dependencies . SqlGenerationHelper . DelimitIdentifier ( TableName , TableSchema ) } IN ACCESS EXCLUSIVE MODE")
57+ . ExecuteNonQueryAsync ( CreateRelationalCommandParameters ( ) , cancellationToken )
58+ . ConfigureAwait ( false ) ;
9359
94- return Task . FromResult < IAsyncDisposable > ( new DummyDisposable ( ) ) ;
60+ return new NpgsqlMigrationDatabaseLock ( this ) ;
9561 }
9662
63+ private RelationalCommandParameterObject CreateRelationalCommandParameters ( )
64+ => new (
65+ Dependencies . Connection ,
66+ null ,
67+ null ,
68+ Dependencies . CurrentContext . Context ,
69+ Dependencies . CommandLogger , CommandSource . Migrations ) ;
70+
9771 /// <summary>
9872 /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
9973 /// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -127,6 +101,48 @@ SELECT 1 FROM pg_catalog.pg_class c
127101 protected override bool InterpretExistsResult ( object ? value )
128102 => ( bool ? ) value == true ;
129103
104+ bool IHistoryRepository . CreateIfNotExists ( )
105+ {
106+ // In PG, doing CREATE TABLE IF NOT EXISTS concurrently can result in a unique constraint violation
107+ // (duplicate key value violates unique constraint "pg_type_typname_nsp_index"). We catch this and report that the table wasn't
108+ // created.
109+ try
110+ {
111+ return Dependencies . MigrationCommandExecutor . ExecuteNonQuery (
112+ GetCreateIfNotExistsCommands ( ) , Dependencies . Connection , new MigrationExecutionState ( ) , commitTransaction : true )
113+ != 0 ;
114+ }
115+ catch ( PostgresException e ) when ( e . SqlState == "23505" )
116+ {
117+ return false ;
118+ }
119+ }
120+
121+ async Task < bool > IHistoryRepository . CreateIfNotExistsAsync ( CancellationToken cancellationToken )
122+ {
123+ // In PG, doing CREATE TABLE IF NOT EXISTS concurrently can result in a unique constraint violation
124+ // (duplicate key value violates unique constraint "pg_type_typname_nsp_index"). We catch this and report that the table wasn't
125+ // created.
126+ try
127+ {
128+ return ( await Dependencies . MigrationCommandExecutor . ExecuteNonQueryAsync (
129+ GetCreateIfNotExistsCommands ( ) , Dependencies . Connection , new MigrationExecutionState ( ) , commitTransaction : true ,
130+ cancellationToken : cancellationToken ) . ConfigureAwait ( false ) )
131+ != 0 ;
132+ }
133+ catch ( PostgresException e ) when ( e . SqlState == "23505" )
134+ {
135+ return false ;
136+ }
137+ }
138+
139+ private IReadOnlyList < MigrationCommand > GetCreateIfNotExistsCommands ( )
140+ => Dependencies . MigrationsSqlGenerator . Generate ( [ new SqlOperation
141+ {
142+ Sql = GetCreateIfNotExistsScript ( ) ,
143+ SuppressTransaction = true
144+ } ] ) ;
145+
130146 /// <summary>
131147 /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
132148 /// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -136,7 +152,7 @@ protected override bool InterpretExistsResult(object? value)
136152 public override string GetCreateIfNotExistsScript ( )
137153 {
138154 var script = GetCreateScript ( ) ;
139- return script . Insert ( script . IndexOf ( "CREATE TABLE" , StringComparison . Ordinal ) + 12 , " IF NOT EXISTS") ;
155+ return script . Replace ( "CREATE TABLE" , "CREATE TABLE IF NOT EXISTS") ;
140156 }
141157
142158 /// <summary>
@@ -178,8 +194,16 @@ public override string GetEndIfScript()
178194END $EF$;
179195""" ;
180196
181- private sealed class DummyDisposable : IDisposable , IAsyncDisposable
197+ private sealed class NpgsqlMigrationDatabaseLock ( IHistoryRepository historyRepository ) : IMigrationsDatabaseLock
182198 {
199+ /// <summary>
200+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
201+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
202+ /// any release. You should only use it directly in your code with extreme caution and knowing that
203+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
204+ /// </summary>
205+ public IHistoryRepository HistoryRepository => historyRepository ;
206+
183207 public void Dispose ( )
184208 {
185209 }
0 commit comments