diff --git a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs b/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs index 7684ad5..661d43b 100644 --- a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs +++ b/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs @@ -705,6 +705,35 @@ namespace Microsoft.EntityFrameworkCore.Migrations } } } + + protected override void ColumnDefinition( + string schema, + string table, + string name, + ColumnOperation operation, + IModel model, + MigrationCommandListBuilder builder) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (operation.ComputedColumnSql != null) + { + ComputedColumnDefinition(schema, table, name, operation, model, builder); + return; + } + + var columnType = GetColumnType(schema, table, name, operation, model); + builder + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) + .Append(" ") + .Append(columnType); + + builder.Append(operation.IsNullable ? " NULL" : " NOT NULL"); + + DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder); + } protected override string GetColumnType( [CanBeNull] string schema, @@ -713,26 +742,30 @@ namespace Microsoft.EntityFrameworkCore.Migrations [NotNull] ColumnOperation operation, [CanBeNull] IModel model) { - var storeType = base.GetColumnType(schema, table, name, operation, model); - - var identity = operation[JetAnnotationNames.Identity] as string; - if (identity != null - || operation[JetAnnotationNames.ValueGenerationStrategy] as JetValueGenerationStrategy? - == JetValueGenerationStrategy.IdentityColumn) + var storeType = operation.ColumnType; + + if (IsIdentity(operation) && + (storeType == null || Dependencies.TypeMappingSource.FindMapping(storeType) is JetIntTypeMapping)) { - if (string.Equals(storeType, "counter", StringComparison.OrdinalIgnoreCase) || - string.Equals(storeType, "identity", StringComparison.OrdinalIgnoreCase) || - string.Equals(storeType, "autoincrement", StringComparison.OrdinalIgnoreCase) || - string.Equals(storeType, "integer", StringComparison.OrdinalIgnoreCase)) - { - storeType = "counter"; + // This column represents the actual identity. + storeType = "counter"; + } + else if (storeType != null && + IsExplicitIdentityColumnType(storeType)) + { + // While this column uses an identity type (e.g. counter), it is not an actual identity column, because + // it was not marked as one. + storeType = "integer"; + } - if (!string.IsNullOrEmpty(identity) - && identity != "1, 1") - { - storeType += $"({identity})"; - } - } + storeType ??= base.GetColumnType(schema, table, name, operation, model); + + if (string.Equals(storeType, "counter", StringComparison.OrdinalIgnoreCase) && + operation[JetAnnotationNames.Identity] is string identity && + !string.IsNullOrEmpty(identity) && + identity != "1, 1") + { + storeType += $"({identity})"; } return storeType; @@ -963,6 +996,11 @@ namespace Microsoft.EntityFrameworkCore.Migrations || operation[JetAnnotationNames.ValueGenerationStrategy] as JetValueGenerationStrategy? == JetValueGenerationStrategy.IdentityColumn; + private static bool IsExplicitIdentityColumnType(string columnType) + => string.Equals("counter", columnType, StringComparison.OrdinalIgnoreCase) || + string.Equals("identity", columnType, StringComparison.OrdinalIgnoreCase) || + string.Equals("autoincrement", columnType, StringComparison.OrdinalIgnoreCase); + #region Schemas not supported protected override void Generate(EnsureSchemaOperation operation, IModel model, MigrationCommandListBuilder builder) diff --git a/src/EFCore.Jet/Storage/Internal/JetIntTypeMapping.cs b/src/EFCore.Jet/Storage/Internal/JetIntTypeMapping.cs new file mode 100644 index 0000000..725ca5c --- /dev/null +++ b/src/EFCore.Jet/Storage/Internal/JetIntTypeMapping.cs @@ -0,0 +1,24 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.Jet.Storage.Internal +{ + public class JetIntTypeMapping : IntTypeMapping + { + public JetIntTypeMapping([NotNull] string storeType) + : base(storeType, System.Data.DbType.Int32) + { + } + + protected JetIntTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + // JetIntTypeMapping is also used for an explicit counter type, because we actually want it to be integer unless + // the value generation type is also OnAdd. + // We therefore lock the store type to its original value (which should be "integer"). + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new JetIntTypeMapping(parameters.WithStoreTypeAndSize(Parameters.StoreType, parameters.Size)); + } +} \ No newline at end of file diff --git a/src/EFCore.Jet/Storage/Internal/JetTypeMappingSource.cs b/src/EFCore.Jet/Storage/Internal/JetTypeMappingSource.cs index f52a1f8..c2d4e3c 100644 --- a/src/EFCore.Jet/Storage/Internal/JetTypeMappingSource.cs +++ b/src/EFCore.Jet/Storage/Internal/JetTypeMappingSource.cs @@ -25,11 +25,13 @@ namespace EntityFrameworkCore.Jet.Storage.Internal private readonly JetBoolTypeMapping _bit = new JetBoolTypeMapping("bit"); // JET bits are not nullable private readonly JetBoolTypeMapping _bool = new JetBoolTypeMapping("smallint"); - private readonly IntTypeMapping _counter = new IntTypeMapping("counter", DbType.Int32); + // We just map counter etc. to integer. Whether an integer property/column is actually a counter + // is determined by the value generation type. + private readonly IntTypeMapping _counter = new JetIntTypeMapping("integer"); private readonly ByteTypeMapping _byte = new ByteTypeMapping("byte", DbType.Byte); // unsigned, there is no signed byte in Jet private readonly ShortTypeMapping _smallint = new ShortTypeMapping("smallint", DbType.Int16); - private readonly IntTypeMapping _integer = new IntTypeMapping("integer", DbType.Int32); + private readonly IntTypeMapping _integer = new JetIntTypeMapping("integer"); // private readonly JetDecimalTypeMapping _bigint = new JetDecimalTypeMapping("decimal", DbType.Decimal, precision: 28, scale: 0, StoreTypePostfix.PrecisionAndScale); private readonly JetFloatTypeMapping _single = new JetFloatTypeMapping("single"); @@ -97,7 +99,7 @@ namespace EntityFrameworkCore.Jet.Storage.Internal {"logical", new[] {_bit}}, {"logical1", new[] {_bit}}, {"yesno", new[] {_bit}}, - + {"counter", new[] {_counter}}, {"identity", new[] {_counter}}, {"autoincrement", new[] {_counter}}, @@ -114,7 +116,7 @@ namespace EntityFrameworkCore.Jet.Storage.Internal {"long", new[] {_integer}}, {"int", new[] {_integer}}, {"integer4", new[] {_integer}}, - + {"single", new[] {_single}}, {"real", new[] {_single}}, {"float4", new[] {_single}}, diff --git a/test/EFCore.Jet.Tests/JetMigrationTest.cs b/test/EFCore.Jet.Tests/JetMigrationTest.cs index ffcbee4..9c6094c 100644 --- a/test/EFCore.Jet.Tests/JetMigrationTest.cs +++ b/test/EFCore.Jet.Tests/JetMigrationTest.cs @@ -33,8 +33,321 @@ namespace EntityFrameworkCore.Jet Assert.Single(cookies); Assert.Equal(new DateTime(2021, 12, 31), cookies[0].BestServedBefore); + + AssertSql( + $@"CREATE TABLE `Cookie` ( + `CookieId` counter NOT NULL, + `Name` longchar NULL, + `BestServedBefore` datetime NOT NULL DEFAULT #2021-12-31#, + CONSTRAINT `PK_Cookie` PRIMARY KEY (`CookieId`) +); + + +INSERT INTO `Cookie` (`CookieId`, `Name`) +VALUES (1, 'Basic'); + + +SELECT `c`.`CookieId`, `c`.`BestServedBefore`, `c`.`Name` +FROM `Cookie` AS `c`"); + } + + [ConditionalFact] + public virtual void Create_many_to_many_table_with_explicit_counter_column_type() + { + using var context = CreateContext( + model: builder => + { + builder.Entity(entity => + { + entity.Property(e => e.CookieId) + .HasColumnType("counter"); + + entity.HasData( + new Cookie { CookieId = 1, Name = "Chocolate Chip" }); + }); + + builder.Entity(entity => + { + entity.Property(e => e.BackeryId) + .HasColumnType("counter"); + + entity.HasData( + new Backery { BackeryId = 1, Name = "Bread & Cookies" }); + }); + + builder.Entity(entity => + { + entity.HasKey(e => new { e.CookieId, e.BackeryId }); + + entity.HasOne(d => d.Cookie) + .WithMany() + .HasForeignKey(d => d.CookieId); + + entity.HasOne(d => d.Backery) + .WithMany() + .HasForeignKey(d => d.BackeryId); + + entity.HasData( + new CookieBackery { CookieId = 1, BackeryId = 1 }); + }); + }); + + var cookieBackeries = context.Set() + .Include(cb => cb.Cookie) + .Include(cb => cb.Backery) + .ToList(); + + Assert.Single(cookieBackeries); + Assert.Equal(1, cookieBackeries[0].Cookie.CookieId); + Assert.Equal(1, cookieBackeries[0].Backery.BackeryId); + + AssertSql( + $@"CREATE TABLE `Backery` ( + `BackeryId` counter NOT NULL, + `Name` longchar NULL, + CONSTRAINT `PK_Backery` PRIMARY KEY (`BackeryId`) +); + + +CREATE TABLE `Cookie` ( + `CookieId` counter NOT NULL, + `Name` longchar NULL, + `BestServedBefore` datetime NOT NULL, + CONSTRAINT `PK_Cookie` PRIMARY KEY (`CookieId`) +); + + +CREATE TABLE `CookieBackery` ( + `CookieId` integer NOT NULL, + `BackeryId` integer NOT NULL, + CONSTRAINT `PK_CookieBackery` PRIMARY KEY (`CookieId`, `BackeryId`), + CONSTRAINT `FK_CookieBackery_Backery_BackeryId` FOREIGN KEY (`BackeryId`) REFERENCES `Backery` (`BackeryId`) ON DELETE CASCADE, + CONSTRAINT `FK_CookieBackery_Cookie_CookieId` FOREIGN KEY (`CookieId`) REFERENCES `Cookie` (`CookieId`) ON DELETE CASCADE +); + + +INSERT INTO `Backery` (`BackeryId`, `Name`) +VALUES (1, 'Bread & Cookies'); + + +INSERT INTO `Cookie` (`CookieId`, `BestServedBefore`, `Name`) +VALUES (1, #1899-12-30#, 'Chocolate Chip'); + + +INSERT INTO `CookieBackery` (`BackeryId`, `CookieId`) +VALUES (1, 1); + + +CREATE INDEX `IX_CookieBackery_BackeryId` ON `CookieBackery` (`BackeryId`); + + +SELECT `c`.`CookieId`, `c`.`BackeryId`, `c0`.`CookieId`, `c0`.`BestServedBefore`, `c0`.`Name`, `b`.`BackeryId`, `b`.`Name` +FROM (`CookieBackery` AS `c` +INNER JOIN `Cookie` AS `c0` ON `c`.`CookieId` = `c0`.`CookieId`) +INNER JOIN `Backery` AS `b` ON `c`.`BackeryId` = `b`.`BackeryId`"); } + [ConditionalFact] + public virtual void Create_many_to_many_table_with_explicit_int_column_type() + { + using var context = CreateContext( + model: builder => + { + builder.Entity(entity => + { + entity.Property(e => e.CookieId) + .HasColumnType("int"); + + entity.HasData( + new Cookie { CookieId = 1, Name = "Chocolate Chip" }); + }); + + builder.Entity(entity => + { + entity.Property(e => e.BackeryId) + .HasColumnType("int"); + + entity.HasData( + new Backery { BackeryId = 1, Name = "Bread & Cookies" }); + }); + + builder.Entity(entity => + { + entity.HasKey(e => new { e.CookieId, e.BackeryId }); + + entity.HasOne(d => d.Cookie) + .WithMany() + .HasForeignKey(d => d.CookieId); + + entity.HasOne(d => d.Backery) + .WithMany() + .HasForeignKey(d => d.BackeryId); + + entity.HasData( + new CookieBackery { CookieId = 1, BackeryId = 1 }); + }); + }); + + var cookieBackeries = context.Set() + .Include(cb => cb.Cookie) + .Include(cb => cb.Backery) + .ToList(); + + Assert.Single(cookieBackeries); + Assert.Equal(1, cookieBackeries[0].Cookie.CookieId); + Assert.Equal(1, cookieBackeries[0].Backery.BackeryId); + + AssertSql( + $@"CREATE TABLE `Backery` ( + `BackeryId` counter NOT NULL, + `Name` longchar NULL, + CONSTRAINT `PK_Backery` PRIMARY KEY (`BackeryId`) +); + + +CREATE TABLE `Cookie` ( + `CookieId` counter NOT NULL, + `Name` longchar NULL, + `BestServedBefore` datetime NOT NULL, + CONSTRAINT `PK_Cookie` PRIMARY KEY (`CookieId`) +); + + +CREATE TABLE `CookieBackery` ( + `CookieId` integer NOT NULL, + `BackeryId` integer NOT NULL, + CONSTRAINT `PK_CookieBackery` PRIMARY KEY (`CookieId`, `BackeryId`), + CONSTRAINT `FK_CookieBackery_Backery_BackeryId` FOREIGN KEY (`BackeryId`) REFERENCES `Backery` (`BackeryId`) ON DELETE CASCADE, + CONSTRAINT `FK_CookieBackery_Cookie_CookieId` FOREIGN KEY (`CookieId`) REFERENCES `Cookie` (`CookieId`) ON DELETE CASCADE +); + + +INSERT INTO `Backery` (`BackeryId`, `Name`) +VALUES (1, 'Bread & Cookies'); + + +INSERT INTO `Cookie` (`CookieId`, `BestServedBefore`, `Name`) +VALUES (1, #1899-12-30#, 'Chocolate Chip'); + + +INSERT INTO `CookieBackery` (`BackeryId`, `CookieId`) +VALUES (1, 1); + + +CREATE INDEX `IX_CookieBackery_BackeryId` ON `CookieBackery` (`BackeryId`); + + +SELECT `c`.`CookieId`, `c`.`BackeryId`, `c0`.`CookieId`, `c0`.`BestServedBefore`, `c0`.`Name`, `b`.`BackeryId`, `b`.`Name` +FROM (`CookieBackery` AS `c` +INNER JOIN `Cookie` AS `c0` ON `c`.`CookieId` = `c0`.`CookieId`) +INNER JOIN `Backery` AS `b` ON `c`.`BackeryId` = `b`.`BackeryId`"); + } + + [ConditionalFact] + public virtual void Create_many_to_many_table_with_inappropriate_counter_column_type() + { + using var context = CreateContext( + model: builder => + { + builder.Entity(entity => + { + entity.Property(e => e.CookieId) + .HasColumnType("int"); + + entity.HasData( + new Cookie { CookieId = 1, Name = "Chocolate Chip" }); + }); + + builder.Entity(entity => + { + entity.Property(e => e.BackeryId) + .HasColumnType("int"); + + entity.HasData( + new Backery { BackeryId = 1, Name = "Bread & Cookies" }); + }); + + builder.Entity(entity => + { + entity.HasKey(e => new { e.CookieId, e.BackeryId }); + + entity.Property(e => e.CookieId) + .HasColumnType("counter"); + + entity.Property(e => e.BackeryId) + .HasColumnType("counter"); + + entity.HasOne(d => d.Cookie) + .WithMany() + .HasForeignKey(d => d.CookieId); + + entity.HasOne(d => d.Backery) + .WithMany() + .HasForeignKey(d => d.BackeryId); + + entity.HasData( + new CookieBackery { CookieId = 1, BackeryId = 1 }); + }); + }); + + var cookieBackeries = context.Set() + .Include(cb => cb.Cookie) + .Include(cb => cb.Backery) + .ToList(); + + Assert.Single(cookieBackeries); + Assert.Equal(1, cookieBackeries[0].Cookie.CookieId); + Assert.Equal(1, cookieBackeries[0].Backery.BackeryId); + + AssertSql( + $@"CREATE TABLE `Backery` ( + `BackeryId` counter NOT NULL, + `Name` longchar NULL, + CONSTRAINT `PK_Backery` PRIMARY KEY (`BackeryId`) +); + + +CREATE TABLE `Cookie` ( + `CookieId` counter NOT NULL, + `Name` longchar NULL, + `BestServedBefore` datetime NOT NULL, + CONSTRAINT `PK_Cookie` PRIMARY KEY (`CookieId`) +); + + +CREATE TABLE `CookieBackery` ( + `CookieId` integer NOT NULL, + `BackeryId` integer NOT NULL, + CONSTRAINT `PK_CookieBackery` PRIMARY KEY (`CookieId`, `BackeryId`), + CONSTRAINT `FK_CookieBackery_Backery_BackeryId` FOREIGN KEY (`BackeryId`) REFERENCES `Backery` (`BackeryId`) ON DELETE CASCADE, + CONSTRAINT `FK_CookieBackery_Cookie_CookieId` FOREIGN KEY (`CookieId`) REFERENCES `Cookie` (`CookieId`) ON DELETE CASCADE +); + + +INSERT INTO `Backery` (`BackeryId`, `Name`) +VALUES (1, 'Bread & Cookies'); + + +INSERT INTO `Cookie` (`CookieId`, `BestServedBefore`, `Name`) +VALUES (1, #1899-12-30#, 'Chocolate Chip'); + + +INSERT INTO `CookieBackery` (`BackeryId`, `CookieId`) +VALUES (1, 1); + + +CREATE INDEX `IX_CookieBackery_BackeryId` ON `CookieBackery` (`BackeryId`); + + +SELECT `c`.`CookieId`, `c`.`BackeryId`, `c0`.`CookieId`, `c0`.`BestServedBefore`, `c0`.`Name`, `b`.`BackeryId`, `b`.`Name` +FROM (`CookieBackery` AS `c` +INNER JOIN `Cookie` AS `c0` ON `c`.`CookieId` = `c0`.`CookieId`) +INNER JOIN `Backery` AS `b` ON `c`.`BackeryId` = `b`.`BackeryId`"); + } + + private void AssertSql(string expected) + => Assert.Equal(expected.Replace("\r\n", "\n"), Sql.Replace("\r\n", "\n")); + public class Cookie { public int CookieId { get; set; } @@ -42,6 +355,21 @@ namespace EntityFrameworkCore.Jet public DateTime BestServedBefore { get; set; } } + public class Backery + { + public int BackeryId { get; set; } + public string Name { get; set; } + } + + public class CookieBackery + { + public int CookieId { get; set; } + public int BackeryId { get; set; } + + public virtual Cookie Cookie { get; set; } + public virtual Backery Backery { get; set; } + } + public class Context : ContextBase { }