提交 3da008e8 编写于 作者: S Shay Rojansky

Fix PG extension mechanism + generated UUID

上级 b1bf34f1
......@@ -78,6 +78,11 @@ public static IServiceCollection AddEntityFrameworkNpgsql([NotNull] this IServic
{
Check.NotNull(services, nameof(services));
services.TryAdd(new ServiceCollection()
.AddScoped<NpgsqlMigrationsModelDiffer>()
.AddScoped<IMigrationsModelDiffer, NpgsqlMigrationsModelDiffer>()
);
services.AddRelational();
services.TryAddEnumerable(ServiceDescriptor
......
......@@ -33,15 +33,16 @@ protected virtual void EnsureUuidExtensionIfNeeded(IModel model)
from e in model.GetEntityTypes()
from p in e.GetProperties()
where p.ClrType.UnwrapNullableType() == typeof(Guid)
let defaultValueSql = (string)_relationalExtensions.For(p).DefaultValueSql
where defaultValueSql != null && defaultValueSql.Contains("uuid_generate")
let defaultValueSql = _relationalExtensions.For(p).DefaultValueSql
where _relationalExtensions.For(p).DefaultValueSql?.StartsWith("uuid_generate") == true ||
p.ValueGenerated == ValueGenerated.OnAdd
select p
).FirstOrDefault();
if (generatedUuidProperty != null &&
model.Npgsql().PostgresExtensions.All(e => e.Name != "uuid-ossp"))
{
ShowError(@"Database-generated UUIDs require the PostgreSQL uuid-ossp extension. Add .HasPostgresExtension(""uuid-ossp"") to your context's OnModelCreating.");
ShowError($@"Property {generatedUuidProperty.Name} on type {generatedUuidProperty.DeclaringEntityType.Name} is a database-generated uuid, which requires the PostgreSQL uuid-ossp extension. Add .HasPostgresExtension(""uuid-ossp"") to your context's OnModelCreating.");
}
}
}
......
#region License
// The PostgreSQL License
//
// Copyright (C) 2016 The Npgsql Development Team
//
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Storage;
namespace Microsoft.EntityFrameworkCore.Migrations.Internal
{
public class NpgsqlMigrationsModelDiffer : MigrationsModelDiffer
{
public NpgsqlMigrationsModelDiffer([NotNull] IRelationalTypeMapper typeMapper, [NotNull] IRelationalAnnotationProvider annotations, [NotNull] IMigrationsAnnotationProvider migrationsAnnotations)
: base(typeMapper, annotations, migrationsAnnotations)
{}
protected override IReadOnlyList<MigrationOperation> Sort(
[NotNull] IEnumerable<MigrationOperation> operations,
[NotNull] DiffContext diffContext)
{
var ops = base.Sort(operations, diffContext);
// base.Sort will leave operations it doesn't recognize (Npgsql-specific)
// at the end, go get these
if (ops.Count > 0 && !ops[ops.Count - 1].IsNpgsqlSpecific())
return ops;
var newOps = new List<MigrationOperation>();
newOps.AddRange(ops.SkipWhile(o => !o.IsNpgsqlSpecific()));
newOps.AddRange(ops.TakeWhile(o => !o.IsNpgsqlSpecific()));
return newOps;
}
protected override IEnumerable<MigrationOperation> Diff(
[CanBeNull] IModel source,
[CanBeNull] IModel target,
[NotNull] DiffContext diffContext)
=> (
source != null && target != null
? Diff(source.Npgsql().PostgresExtensions, target.Npgsql().PostgresExtensions)
: source == null
? target.Npgsql().PostgresExtensions.SelectMany(Add)
: source.Npgsql().PostgresExtensions.SelectMany(Remove)
).Concat(base.Diff(source, target, diffContext));
protected virtual IEnumerable<MigrationOperation> Diff(
[NotNull] IEnumerable<IPostgresExtension> source,
[NotNull] IEnumerable<IPostgresExtension> target)
=> DiffCollection(
source, target,
Diff, Add, Remove,
(s, t) => s.Name == t.Name);
protected virtual IEnumerable<MigrationOperation> Diff([NotNull] IPostgresExtension source, [NotNull] IPostgresExtension target)
=> Enumerable.Empty<MigrationOperation>();
protected virtual IEnumerable<MigrationOperation> Add([NotNull] IPostgresExtension target)
{
yield return new NpgsqlCreatePostgresExtensionOperation
{
Schema = target.Schema,
Name = target.Name,
Version = target.Version
};
}
protected virtual IEnumerable<MigrationOperation> Remove([NotNull] IPostgresExtension source)
{
yield return new NpgsqlDropPostgresExtensionOperation { Name = source.Name };
}
}
static class MigrationOperationExtensions
{
internal static bool IsNpgsqlSpecific(this MigrationOperation op)
=> op is NpgsqlCreatePostgresExtensionOperation ||
op is NpgsqlDropPostgresExtensionOperation;
}
}
......@@ -341,6 +341,8 @@ public virtual void Generate(NpgsqlDropDatabaseOperation operation, IModel model
.Append(dbName);
}
#region PostgreSQL extensions
public virtual void Generate(NpgsqlCreatePostgresExtensionOperation operation, IModel model, RelationalCommandListBuilder builder)
{
Check.NotNull(operation, nameof(operation));
......@@ -365,6 +367,18 @@ public virtual void Generate(NpgsqlCreatePostgresExtensionOperation operation, I
}
}
public virtual void Generate(NpgsqlDropPostgresExtensionOperation operation, IModel model, RelationalCommandListBuilder builder)
{
Check.NotNull(operation, nameof(operation));
Check.NotNull(builder, nameof(builder));
builder
.Append("DROP EXTENSION ")
.Append(SqlGenerationHelper.DelimitIdentifier(operation.Name));
}
#endregion PostgreSQL extensions
protected override void Generate(DropIndexOperation operation, IModel model, RelationalCommandListBuilder builder)
{
Check.NotNull(operation, nameof(operation));
......
#region License
// The PostgreSQL License
//
// Copyright (C) 2016 The Npgsql Development Team
//
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion
using JetBrains.Annotations;
namespace Microsoft.EntityFrameworkCore.Migrations.Operations
{
public class NpgsqlDropPostgresExtensionOperation : MigrationOperation
{
public virtual string Name { get; [param: NotNull] set; }
}
}
......@@ -70,12 +70,14 @@
<Compile Include="Metadata\NpgsqlAnnotationProvider.cs" />
<Compile Include="Metadata\NpgsqlModelAnnotations.cs" />
<Compile Include="Metadata\PostgresExtension.cs" />
<Compile Include="Migrations\Internal\NpgsqlMigrationsModelDiffer.cs" />
<Compile Include="Migrations\Operations\NpgsqlCreatePostgresExtensionOperation.cs" />
<Compile Include="Migrations\Operations\NpgsqlCreateDatabaseOperation.cs" />
<Compile Include="Migrations\Operations\NpgsqlDropDatabaseOperation.cs" />
<Compile Include="Migrations\Internal\NpgsqlHistoryRepository.cs" />
<Compile Include="Migrations\Internal\NpgsqlMigrationsAnnotationProvider.cs" />
<Compile Include="Migrations\NpgsqlMigrationsSqlGenerator.cs" />
<Compile Include="Migrations\Operations\NpgsqlDropPostgresExtensionOperation.cs" />
<Compile Include="NpgsqlModelBuilderExtensions.cs" />
<Compile Include="NpgsqlPostgresExtensionBuilder.cs" />
<Compile Include="Query\ExpressionTranslators\Internal\NpgsqlCompositeMemberTranslator.cs" />
......
......@@ -21,13 +21,16 @@
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Utilities;
using Npgsql;
......@@ -36,9 +39,10 @@ namespace Microsoft.EntityFrameworkCore.Storage.Internal
{
public class NpgsqlDatabaseCreator : RelationalDatabaseCreator
{
private readonly NpgsqlRelationalConnection _connection;
private readonly IMigrationsSqlGenerator _migrationsSqlGenerator;
private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder;
readonly NpgsqlRelationalConnection _connection;
readonly IMigrationsSqlGenerator _migrationsSqlGenerator;
readonly IRawSqlCommandBuilder _rawSqlCommandBuilder;
readonly IMigrationsModelDiffer _modelDiffer;
public NpgsqlDatabaseCreator(
[NotNull] NpgsqlRelationalConnection connection,
......@@ -51,6 +55,7 @@ public class NpgsqlDatabaseCreator : RelationalDatabaseCreator
Check.NotNull(rawSqlCommandBuilder, nameof(rawSqlCommandBuilder));
_connection = connection;
_modelDiffer = modelDiffer;
_migrationsSqlGenerator = migrationsSqlGenerator;
_rawSqlCommandBuilder = rawSqlCommandBuilder;
}
......@@ -63,24 +68,6 @@ public override void Create()
ClearPool();
}
var postCreateOperations = CreatePostCreateOperations();
postCreateOperations.ExecuteNonQuery(_connection);
if (postCreateOperations.Any())
{
// The post-creation operations may have create new types (e.g. extension),
// reload type definitions
_connection.Open();
try
{
((NpgsqlConnection)_connection.DbConnection).ReloadTypes();
}
finally
{
_connection.Close();
}
}
}
public override async Task CreateAsync(CancellationToken cancellationToken = default(CancellationToken))
......@@ -91,25 +78,6 @@ public override async Task CreateAsync(CancellationToken cancellationToken = def
ClearPool();
}
var postCreateOperations = CreatePostCreateOperations();
await postCreateOperations.ExecuteNonQueryAsync(_connection);
if (postCreateOperations.Any())
{
// The post-creation operations may have create new types (e.g. extension),
// reload type definitions
_connection.Open();
try
{
// TODO: This is a non-async operation...
((NpgsqlConnection)_connection.DbConnection).ReloadTypes();
}
finally
{
_connection.Close();
}
}
}
protected override bool HasTables()
......@@ -129,23 +97,6 @@ FROM information_schema.tables
IEnumerable<IRelationalCommand> CreateCreateOperations()
=> _migrationsSqlGenerator.Generate(new[] { new NpgsqlCreateDatabaseOperation { Name = _connection.DbConnection.Database, Template = Model.Npgsql().DatabaseTemplate } });
/// <summary>
/// Creates migration operations that should take place immediately after creating the database,
/// e.g. PostgreSQL extension setup
/// </summary>
List<IRelationalCommand> CreatePostCreateOperations()
{
var operations = new List<MigrationOperation>();
foreach (var extension in Model.Npgsql().PostgresExtensions)
operations.Add(new NpgsqlCreatePostgresExtensionOperation
{
Name = extension.Name,
Schema = extension.Schema,
Version = extension.Version
});
return _migrationsSqlGenerator.Generate(operations).ToList();
}
public override bool Exists()
{
try
......@@ -185,7 +136,7 @@ public override async Task<bool> ExistsAsync(CancellationToken cancellationToken
}
// Login failed is thrown when database does not exist (See Issue #776)
private static bool IsDoesNotExist(PostgresException exception) => exception.SqlState == "3D000";
static bool IsDoesNotExist(PostgresException exception) => exception.SqlState == "3D000";
public override void Delete()
{
......@@ -207,7 +158,56 @@ public override async Task DeleteAsync(CancellationToken cancellationToken = def
}
}
private IEnumerable<IRelationalCommand> CreateDropCommands()
public override void CreateTables()
{
var operations = _modelDiffer.GetDifferences(null, Model);
var commands = _migrationsSqlGenerator.Generate(operations, Model);
// Adding a PostgreSQL extension might define new types (e.g. hstore), which we
// Npgsql to reload
var reloadTypes = operations.Any(o => o is NpgsqlCreatePostgresExtensionOperation);
using (var transaction = Connection.BeginTransaction())
{
commands.ExecuteNonQuery(Connection);
transaction.Commit();
}
if (reloadTypes)
{
var npgsqlConn = (NpgsqlConnection)Connection.DbConnection;
if (npgsqlConn.FullState == ConnectionState.Open)
npgsqlConn.ReloadTypes();
}
}
public override async Task CreateTablesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
var operations = _modelDiffer.GetDifferences(null, Model);
var commands = _migrationsSqlGenerator.Generate(operations, Model);
// Adding a PostgreSQL extension might define new types (e.g. hstore), which we
// Npgsql to reload
var reloadTypes = operations.Any(o => o is NpgsqlCreatePostgresExtensionOperation);
using (var transaction = await Connection.BeginTransactionAsync(cancellationToken))
{
await commands.ExecuteNonQueryAsync(Connection, cancellationToken);
transaction.Commit();
}
// TODO: Not async
if (reloadTypes)
{
var npgsqlConn = (NpgsqlConnection)Connection.DbConnection;
if (npgsqlConn.FullState == ConnectionState.Open)
npgsqlConn.ReloadTypes();
}
}
IEnumerable<IRelationalCommand> CreateDropCommands()
{
var operations = new MigrationOperation[]
{
......
......@@ -92,6 +92,8 @@ public BloggingContext(IServiceProvider serviceProvider, DbContextOptions option
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("uuid-ossp");
modelBuilder.Entity<Blog>(b =>
{
//b.Property(e => e.Id).HasDefaultValueSql("NEWID()");
......
......@@ -28,41 +28,20 @@ public BuiltInDataTypesNpgsqlFixture()
{
_testStore = NpgsqlTestStore.CreateScratch();
/*
// TODO: find a better way to do this
_testStore.ExecuteNonQuery("CREATE TYPE some_composite AS (some_number int, some_text text)");
var npgsqlConn = (NpgsqlConnection)_testStore.Connection;
npgsqlConn.ReloadTypes();
npgsqlConn.MapComposite<SomeComposite>();
*/
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkNpgsql()
.AddSingleton(TestNpgsqlModelSource.GetFactory(OnModelCreating))
.BuildServiceProvider();
// We need the database to be created with NpgsqlDatabaseCreator for the
// extension migration operations to take place. Drop the database created
// above by NpgsqlTestStore and recreate.
var tempOptions = new DbContextOptionsBuilder()
.UseNpgsql(_testStore.Connection)
.UseInternalServiceProvider(serviceProvider)
.Options;
// Close the test store's connection because the database is about to
// get dropped (via a different master connection)
_testStore.Connection.Close();
using (var context = new DbContext(tempOptions))
context.Database.EnsureDeleted();
_options = new DbContextOptionsBuilder()
.UseNpgsql(_testStore.ConnectionString)
.UseNpgsql(_testStore.Connection)
.UseInternalServiceProvider(serviceProvider)
.Options;
using (var context = new DbContext(_options))
{
context.Database.EnsureCreated();
}
}
public override DbContext CreateContext()
......
......@@ -8,6 +8,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Specification.Tests;
using Microsoft.EntityFrameworkCore.Specification.Tests.TestModels.GearsOfWarModel;
using Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests.TestModels;
using Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests.Utilities;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests
......@@ -68,5 +69,12 @@ public override GearsOfWarContext CreateContext(NpgsqlTestStore testStore)
return context;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("uuid-ossp");
base.OnModelCreating(modelBuilder);
}
}
}
......@@ -32,6 +32,13 @@ public GraphUpdatesNpgsqlFixture()
.BuildServiceProvider();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("uuid-ossp");
base.OnModelCreating(modelBuilder);
}
public override NpgsqlTestStore CreateTestStore()
{
return NpgsqlTestStore.GetOrCreateShared(DatabaseName, () =>
......
......@@ -22,7 +22,8 @@ public void Postgres_extension_is_created()
using (var context = new ExtensionContext(connString))
{
var creator = context.GetService<IRelationalDatabaseCreator>();
creator.Create();
creator.EnsureDeleted();
creator.EnsureCreated();
try
{
......
......@@ -64,6 +64,8 @@ public class BlogContext : ContextBase
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder
.Entity<Blog>()
.Property(e => e.Id)
......@@ -98,6 +100,8 @@ public class BlogContext : ContextBase
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder
.Entity<Blog>()
.Property(e => e.OtherId)
......@@ -132,7 +136,7 @@ public class BlogContext : ContextBase
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("uuid-ossp");
base.OnModelCreating(modelBuilder);
modelBuilder
.Entity<GuidBlog>()
......@@ -196,6 +200,11 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
var name = GetType().FullName.Substring((GetType().Namespace + nameof(NpgsqlValueGenerationScenariosTest)).Length + 2);
optionsBuilder.UseNpgsql(NpgsqlTestStore.CreateConnectionString(name));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("uuid-ossp");
}
}
public class TestBase<TContext>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册