diff --git a/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs b/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs index 41ae480b7f0..98193c73d2b 100644 --- a/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs +++ b/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs @@ -818,6 +818,23 @@ public static IEnumerable GetDataAnnotations( { FluentApiCodeFragment? root = null; + if (annotatable is IProperty property + && annotations.TryGetValue(RelationalAnnotationNames.DefaultValueSql, out _) + && annotations.TryGetValue(RelationalAnnotationNames.DefaultValue, out var parsedAnnotation)) + { + if (Equals(property.ClrType.GetDefaultValue(), parsedAnnotation.Value)) + { + // Default value is CLR default for property, so exclude it from scaffolded model + annotations.Remove(RelationalAnnotationNames.DefaultValueSql); + annotations.Remove(RelationalAnnotationNames.DefaultValue); + } + else + { + // SQL was parsed, so use parsed value and exclude raw value + annotations.Remove(RelationalAnnotationNames.DefaultValueSql); + } + } + foreach (var methodCall in annotationCodeGenerator.GenerateFluentApiCalls(annotatable, annotations)) { var fluentApiCall = FluentApiCodeFragment.From(methodCall); diff --git a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs index ae03e4f4143..3f45fc9d67b 100644 --- a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs +++ b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs @@ -453,6 +453,11 @@ protected virtual EntityTypeBuilder VisitColumns(EntityTypeBuilder builder, ICol property.ValueGeneratedOnAddOrUpdate(); } + if (column.DefaultValue != null) + { + property.HasDefaultValue(column.DefaultValue); + } + if (column.DefaultValueSql != null) { property.HasDefaultValueSql(column.DefaultValueSql); diff --git a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs index e3416290fcf..2798ba05a7f 100644 --- a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs +++ b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs @@ -32,6 +32,11 @@ public class DatabaseColumn : Annotatable /// public virtual string? StoreType { get; set; } + /// + /// The default value for the column, or if there is no default value or it could not be parsed. + /// + public virtual object? DefaultValue { get; set; } + /// /// The default constraint for the column, or if none. /// diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 78f93736fcf..90783024415 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -184,12 +184,6 @@ public override IReadOnlyList GenerateFluentApiCalls( return fragments; } - /// - public override IReadOnlyList GenerateFluentApiCalls( - IRelationalPropertyOverrides overrides, - IDictionary annotations) - => base.GenerateFluentApiCalls(overrides, annotations); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs index 57b4c066397..a7f34b2d071 100644 --- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs @@ -23,6 +23,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal; public class SqlServerDatabaseModelFactory : DatabaseModelFactory { private readonly IDiagnosticsLogger _logger; + private readonly IRelationalTypeMappingSource _typeMappingSource; private static readonly ISet DateTimePrecisionTypes = new HashSet { @@ -82,9 +83,12 @@ private static readonly Regex PartExtractor /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public SqlServerDatabaseModelFactory(IDiagnosticsLogger logger) + public SqlServerDatabaseModelFactory( + IDiagnosticsLogger logger, + IRelationalTypeMappingSource typeMappingSource) { _logger = logger; + _typeMappingSource = typeMappingSource; } /// @@ -788,7 +792,7 @@ FROM [sys].[views] v var scale = dataRecord.GetValueOrDefault("scale"); var nullable = dataRecord.GetValueOrDefault("is_nullable"); var isIdentity = dataRecord.GetValueOrDefault("is_identity"); - var defaultValue = dataRecord.GetValueOrDefault("default_sql"); + var defaultValueSql = dataRecord.GetValueOrDefault("default_sql"); var computedValue = dataRecord.GetValueOrDefault("computed_sql"); var computedIsPersisted = dataRecord.GetValueOrDefault("computed_is_persisted"); var comment = dataRecord.GetValueOrDefault("comment"); @@ -811,7 +815,7 @@ FROM [sys].[views] v scale, nullable, isIdentity, - defaultValue, + defaultValueSql, computedValue, computedIsPersisted); @@ -830,15 +834,14 @@ FROM [sys].[views] v systemTypeName = dataTypeName; } - defaultValue = FilterClrDefaults(systemTypeName, nullable, defaultValue); - var column = new DatabaseColumn { Table = table, Name = columnName, StoreType = storeType, IsNullable = nullable, - DefaultValueSql = defaultValue, + DefaultValue = TryParseClrDefault(systemTypeName, defaultValueSql), + DefaultValueSql = defaultValueSql, ComputedColumnSql = computedValue, IsStored = computedIsPersisted, Comment = comment, @@ -868,48 +871,110 @@ FROM [sys].[views] v } } - private static string? FilterClrDefaults(string dataTypeName, bool nullable, string? defaultValue) + private object? TryParseClrDefault(string dataTypeName, string? defaultValueSql) { - if (defaultValue == null - || defaultValue == "(NULL)") + defaultValueSql = defaultValueSql?.Trim(); + if (string.IsNullOrEmpty(defaultValueSql)) + { + return null; + } + + var mapping = _typeMappingSource.FindMapping(dataTypeName); + if (mapping == null) { return null; } - if (nullable) + Unwrap(); + if (defaultValueSql.StartsWith("CONVERT", StringComparison.OrdinalIgnoreCase)) { - return defaultValue; + defaultValueSql = defaultValueSql.Substring(defaultValueSql.IndexOf(',') + 1); + defaultValueSql = defaultValueSql.Substring(0, defaultValueSql.LastIndexOf(')')); + Unwrap(); } - if (defaultValue == "((0))" || defaultValue == "(0)") + if (defaultValueSql.Equals("NULL", StringComparison.OrdinalIgnoreCase)) { - if (dataTypeName is "bigint" or "bit" or "decimal" or "float" or "int" or "money" or "numeric" or "real" or "smallint" - or "smallmoney" or "tinyint") + return null; + } + + var type = mapping.ClrType; + if (type == typeof(bool) + && int.TryParse(defaultValueSql, out var intValue)) + { + return intValue != 0; + } + + if (type.IsNumeric()) + { + try + { + return Convert.ChangeType(defaultValueSql, type); + } + catch { + // Ignored return null; } } - else if (defaultValue == "((0.0))" || defaultValue == "(0.0)") + + if ((defaultValueSql.StartsWith('\'') || defaultValueSql.StartsWith("N'", StringComparison.OrdinalIgnoreCase)) + && defaultValueSql.EndsWith('\'')) { - if (dataTypeName is "decimal" or "float" or "money" or "numeric" or "real" or "smallmoney") + var startIndex = defaultValueSql.IndexOf('\''); + defaultValueSql = defaultValueSql.Substring(startIndex + 1, defaultValueSql.Length - (startIndex + 2)); + + if (type == typeof(string)) { - return null; + return defaultValueSql; + } + + if (type == typeof(bool) + && bool.TryParse(defaultValueSql, out var boolValue)) + { + return boolValue; + } + + if (type == typeof(Guid) + && Guid.TryParse(defaultValueSql, out var guid)) + { + return guid; + } + + if (type == typeof(DateTime) + && DateTime.TryParse(defaultValueSql, out var dateTime)) + { + return dateTime; + } + + if (type == typeof(DateOnly) + && DateOnly.TryParse(defaultValueSql, out var dateOnly)) + { + return dateOnly; + } + + if (type == typeof(TimeOnly) + && TimeOnly.TryParse(defaultValueSql, out var timeOnly)) + { + return timeOnly; + } + + if (type == typeof(DateTimeOffset) + && DateTimeOffset.TryParse(defaultValueSql, out var dateTimeOffset)) + { + return dateTimeOffset; } } - else if ((defaultValue == "(CONVERT([real],(0)))" && dataTypeName == "real") - || (defaultValue == "((0.0000000000000000e+000))" && dataTypeName == "float") - || (defaultValue == "(0.0000000000000000e+000)" && dataTypeName == "float") - || (defaultValue == "('0001-01-01')" && dataTypeName == "date") - || (defaultValue == "('1900-01-01T00:00:00.000')" && (dataTypeName == "datetime" || dataTypeName == "smalldatetime")) - || (defaultValue == "('0001-01-01T00:00:00.000')" && dataTypeName == "datetime2") - || (defaultValue == "('0001-01-01T00:00:00.000+00:00')" && dataTypeName == "datetimeoffset") - || (defaultValue == "('00:00:00')" && dataTypeName == "time") - || (defaultValue == "('00000000-0000-0000-0000-000000000000')" && dataTypeName == "uniqueidentifier")) + + return null; + + void Unwrap() { - return null; + while (defaultValueSql.StartsWith('(') && defaultValueSql.EndsWith(')')) + { + defaultValueSql = (defaultValueSql.Substring(1, defaultValueSql.Length - 2)).Trim(); + } } - - return defaultValue; } private static string GetStoreType(string dataTypeName, int maxLength, int precision, int scale) @@ -1190,11 +1255,11 @@ private void GetForeignKeys(DbConnection connection, IReadOnlyList TestAsync( + serviceProvider => serviceProvider.GetService().Create( + BuildModelWithColumn("nvarchar(max)", null, "Hot"), new ModelReverseEngineerOptions()), + new ModelCodeGenerationOptions(), + code => Assert.Contains($".HasDefaultValue(\"Hot\")", code.ContextFile.Code), + model => + { + var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); + Assert.Equal("Hot", property.GetDefaultValue()); + Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql)); + }); + + [ConditionalFact] + public Task Column_with_default_value_sql_only_uses_default_value_sql() + => TestAsync( + serviceProvider => serviceProvider.GetService().Create( + BuildModelWithColumn("nvarchar(max)", "('Hot')", null), new ModelReverseEngineerOptions()), + new ModelCodeGenerationOptions(), + code => Assert.Contains($".HasDefaultValueSql(\"('Hot')\")", code.ContextFile.Code), + model => + { + var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); + Assert.Equal("('Hot')", property.GetDefaultValueSql()); + Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + }); + + [ConditionalFact] + public Task Column_with_default_value_sql_and_default_value_uses_default_value() + => TestAsync( + serviceProvider => serviceProvider.GetService().Create( + BuildModelWithColumn("nvarchar(max)", "('Hot')", "Hot"), new ModelReverseEngineerOptions()), + new ModelCodeGenerationOptions(), + code => Assert.Contains($".HasDefaultValue(\"Hot\")", code.ContextFile.Code), + model => + { + var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); + Assert.Equal("Hot", property.GetDefaultValue()); + Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql)); + }); + + [ConditionalFact] + public Task Column_with_default_value_sql_and_default_value_where_value_is_CLR_default_uses_neither() + => TestAsync( + serviceProvider => serviceProvider.GetService().Create( + BuildModelWithColumn("int", "((0))", 0), new ModelReverseEngineerOptions()), + new ModelCodeGenerationOptions(), + code => Assert.DoesNotContain("HasDefaultValue", code.ContextFile.Code), + model => + { + var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); + Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql)); + }); + [ConditionalFact] public Task IsUnicode_works() => TestAsync( diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs index 529de90dc7d..ae1cc54c2d0 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; @@ -19,7 +19,7 @@ protected ModelCodeGeneratorTestBase(ModelCodeGeneratorTestFixture fixture, ITes _output = output; } - protected async Task TestAsync( + protected Task TestAsync( Action buildModel, ModelCodeGenerationOptions options, Action assertScaffold, @@ -37,8 +37,37 @@ protected async Task TestAsync( var services = CreateServices(); AddScaffoldingServices(services); - var generators = services.BuildServiceProvider(validateScopes: true) - .GetServices(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); + + return TestAsync(serviceProvider, model, options, assertScaffold, assertModel, skipBuild); + } + + protected Task TestAsync( + Func buildModel, + ModelCodeGenerationOptions options, + Action assertScaffold, + Action assertModel, + bool skipBuild = false) + { + var designServices = new ServiceCollection(); + AddModelServices(designServices); + var services = CreateServices(); + AddScaffoldingServices(services); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); + var model = buildModel(serviceProvider); + + return TestAsync(serviceProvider, model, options, assertScaffold, assertModel, skipBuild); + } + + protected async Task TestAsync( + IServiceProvider serviceProvider, + IModel model, + ModelCodeGenerationOptions options, + Action assertScaffold, + Action assertModel, + bool skipBuild = false) + { + var generators = serviceProvider.GetServices(); var generator = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || Random.Shared.Next() % 12 != 0 @@ -87,6 +116,37 @@ protected async Task TestAsync( } } + protected static DatabaseModel BuildModelWithColumn(string storeType, string sql, object expected) + { + var dbModel = new DatabaseModel + { + Tables = + { + new DatabaseTable + { + Database = new DatabaseModel(), + Name = "Table", + Columns = + { + new DatabaseColumn + { + Name = "Column", + StoreType = storeType, + DefaultValueSql = sql, + DefaultValue = expected + } + } + } + } + }; + + var table = dbModel.Tables.Single(); + table.Database = dbModel; + table.Columns.Single().Table = table; + + return dbModel; + } + protected IServiceCollection CreateServices() { var testAssembly = MockAssembly.Create(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs index ed735bcb305..420b5e19ecc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs @@ -1417,72 +1417,611 @@ ComputedValue AS GETDATE(), }, "DROP TABLE DefaultComputedValues;"); - [ConditionalFact] - public void Default_value_matching_clr_default_is_not_stored() - { - Fixture.TestStore.ExecuteNonQuery( - @" -CREATE TYPE datetime2Alias FROM datetime2(6); -CREATE TYPE datetimeoffsetAlias FROM datetimeoffset(6); -CREATE TYPE decimalAlias FROM decimal(17, 0); -CREATE TYPE numericAlias FROM numeric(17, 0); -CREATE TYPE timeAlias FROM time(6);"); - - Test( - @" -CREATE TABLE DefaultValues ( - IgnoredDefault1 int DEFAULT NULL, - IgnoredDefault2 int NOT NULL DEFAULT NULL, - IgnoredDefault3 bigint NOT NULL DEFAULT 0, - IgnoredDefault4 bit NOT NULL DEFAULT 0, - IgnoredDefault5 decimal NOT NULL DEFAULT 0, - IgnoredDefault6 decimalAlias NOT NULL DEFAULT 0, - IgnoredDefault7 float NOT NULL DEFAULT 0, - IgnoredDefault9 int NOT NULL DEFAULT 0, - IgnoredDefault10 money NOT NULL DEFAULT 0, - IgnoredDefault11 numeric NOT NULL DEFAULT 0, - IgnoredDefault12 numericAlias NOT NULL DEFAULT 0, - IgnoredDefault13 real NOT NULL DEFAULT 0, - IgnoredDefault14 smallint NOT NULL DEFAULT 0, - IgnoredDefault15 smallmoney NOT NULL DEFAULT 0, - IgnoredDefault16 tinyint NOT NULL DEFAULT 0, - IgnoredDefault17 decimal NOT NULL DEFAULT 0.0, - IgnoredDefault18 float NOT NULL DEFAULT 0.0, - IgnoredDefault19 money NOT NULL DEFAULT 0.0, - IgnoredDefault20 numeric NOT NULL DEFAULT 0.0, - IgnoredDefault21 real NOT NULL DEFAULT 0.0, - IgnoredDefault22 smallmoney NOT NULL DEFAULT 0.0, - IgnoredDefault23 real NOT NULL DEFAULT CAST(0 AS real), - IgnoredDefault24 float NOT NULL DEFAULT 0.0E0, - IgnoredDefault25 date NOT NULL DEFAULT '0001-01-01', - IgnoredDefault26 datetime NOT NULL DEFAULT '1900-01-01T00:00:00.000', - IgnoredDefault27 smalldatetime NOT NULL DEFAULT '1900-01-01T00:00:00.000', - IgnoredDefault28 datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.000', - IgnoredDefault29 datetime2Alias NOT NULL DEFAULT '0001-01-01T00:00:00.000', - IgnoredDefault30 datetimeoffset NOT NULL DEFAULT '0001-01-01T00:00:00.000+00:00', - IgnoredDefault31 datetimeoffsetAlias NOT NULL DEFAULT '0001-01-01T00:00:00.000+00:00', - IgnoredDefault32 time NOT NULL DEFAULT '00:00:00', - IgnoredDefault33 timeAlias NOT NULL DEFAULT '00:00:00', - IgnoredDefault34 uniqueidentifier NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000' + [ConditionalFact] + public void Non_literal_bool_default_values_are_passed_through() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A bit DEFAULT (CHOOSE(1, 0, 1, 2)), + B bit DEFAULT ((CONVERT([bit],(CHOOSE(1, 0, 1, 2))))), );", - Enumerable.Empty(), - Enumerable.Empty(), - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.All( - columns, - t => Assert.Null(t.DefaultValueSql)); - }, - @" -DROP TABLE DefaultValues; -DROP TYPE datetime2Alias; -DROP TYPE datetimeoffsetAlias; -DROP TYPE decimalAlias; -DROP TYPE numericAlias; -DROP TYPE timeAlias;"); - } + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("(choose((1),(0),(1),(2)))", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(CONVERT([bit],choose((1),(0),(1),(2))))", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_int_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A int DEFAULT -1, + B int DEFAULT 0, + C int DEFAULT (0), + D int DEFAULT (-2), + E int DEFAULT ( 2), + F int DEFAULT (3 ), + G int DEFAULT ((4)), + H int DEFAULT CONVERT([int],(6)), + I int DEFAULT CONVERT(""int"",(-7)), + J int DEFAULT ( ( CONVERT([int],((-8))))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((-1))", column.DefaultValueSql); + Assert.Equal(-1, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal(0, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal(0, column.DefaultValue); + + column = columns.Single(c => c.Name == "D"); + Assert.Equal("((-2))", column.DefaultValueSql); + Assert.Equal(-2, column.DefaultValue); + + column = columns.Single(c => c.Name == "E"); + Assert.Equal("((2))", column.DefaultValueSql); + Assert.Equal(2, column.DefaultValue); + + column = columns.Single(c => c.Name == "F"); + Assert.Equal("((3))", column.DefaultValueSql); + Assert.Equal(3, column.DefaultValue); + + column = columns.Single(c => c.Name == "G"); + Assert.Equal("((4))", column.DefaultValueSql); + Assert.Equal(4, column.DefaultValue); + + column = columns.Single(c => c.Name == "H"); + Assert.Equal("(CONVERT([int],(6)))", column.DefaultValueSql); + Assert.Equal(6, column.DefaultValue); + + column = columns.Single(c => c.Name == "I"); + Assert.Equal("(CONVERT([int],(-7)))", column.DefaultValueSql); + Assert.Equal(-7, column.DefaultValue); + + column = columns.Single(c => c.Name == "J"); + Assert.Equal("(CONVERT([int],(-8)))", column.DefaultValueSql); + Assert.Equal(-8, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_short_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A smallint DEFAULT -1, + B smallint DEFAULT (0), + C smallint DEFAULT ((CONVERT ( ""smallint"", ( (-7) ) ))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((-1))", column.DefaultValueSql); + Assert.Equal((short)-1, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal((short)0, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("(CONVERT([smallint],(-7)))", column.DefaultValueSql); + Assert.Equal((short)-7, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_long_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A bigint DEFAULT -1, + B bigint DEFAULT (0), + C bigint DEFAULT ((CONVERT ( ""bigint"", ( (-7) ) ))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((-1))", column.DefaultValueSql); + Assert.Equal((long)-1, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal((long)0, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("(CONVERT([bigint],(-7)))", column.DefaultValueSql); + Assert.Equal((long)-7, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_byte_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A tinyint DEFAULT 1, + B tinyint DEFAULT (0), + C tinyint DEFAULT ((CONVERT ( ""tinyint"", ( (7) ) ))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((1))", column.DefaultValueSql); + Assert.Equal((byte)1, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal((byte)0, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("(CONVERT([tinyint],(7)))", column.DefaultValueSql); + Assert.Equal((byte)7, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Non_literal_intl_default_values_are_passed_through() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A int DEFAULT (CHOOSE(1, 0, 1, 2)), + B int DEFAULT ((CONVERT([int],(CHOOSE(1, 0, 1, 2))))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("(choose((1),(0),(1),(2)))", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(CONVERT([int],choose((1),(0),(1),(2))))", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_double_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A float DEFAULT -1.1111, + B float DEFAULT (0.0), + C float DEFAULT (1.1000000000000001e+000), + D float DEFAULT ((CONVERT ( ""float"", ( (1.1234) ) ))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((-1.1111))", column.DefaultValueSql); + Assert.Equal(-1.1111, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((0.0))", column.DefaultValueSql); + Assert.Equal((double)0, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("((1.1000000000000001e+000))", column.DefaultValueSql); + Assert.Equal(1.1000000000000001e+000, column.DefaultValue); + + column = columns.Single(c => c.Name == "D"); + Assert.Equal("(CONVERT([float],(1.1234)))", column.DefaultValueSql); + Assert.Equal(1.1234, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_float_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A real DEFAULT -1.1111, + B real DEFAULT (0.0), + C real DEFAULT (1.1000000000000001e+000), + D real DEFAULT ((CONVERT ( ""real"", ( (1.1234) ) ))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((-1.1111))", column.DefaultValueSql); + Assert.Equal((float)-1.1111, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((0.0))", column.DefaultValueSql); + Assert.Equal((float)0, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("((1.1000000000000001e+000))", column.DefaultValueSql); + Assert.Equal((float)1.1000000000000001e+000, column.DefaultValue); + + column = columns.Single(c => c.Name == "D"); + Assert.Equal("(CONVERT([real],(1.1234)))", column.DefaultValueSql); + Assert.Equal((float)1.1234, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_decimal_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A decimal DEFAULT -1.1111, + B decimal DEFAULT (0.0), + C decimal DEFAULT (0), + D decimal DEFAULT ((CONVERT ( ""decimal"", ( (1.1234) ) ))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((-1.1111))", column.DefaultValueSql); + Assert.Equal((decimal)-1.1111, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((0.0))", column.DefaultValueSql); + Assert.Equal((decimal)0, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal((decimal)0, column.DefaultValue); + + column = columns.Single(c => c.Name == "D"); + Assert.Equal("(CONVERT([decimal],(1.1234)))", column.DefaultValueSql); + Assert.Equal((decimal)1.1234, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_bool_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A bit DEFAULT 0, + B bit DEFAULT 1, + C bit DEFAULT (0), + D bit DEFAULT (1), + E bit DEFAULT ('FaLse'), + F bit DEFAULT ('tRuE'), + G bit DEFAULT ((CONVERT ( ""bit"", ( ('tRUE') ) ))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal(false, column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("((1))", column.DefaultValueSql); + Assert.Equal(true, column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("((0))", column.DefaultValueSql); + Assert.Equal(false, column.DefaultValue); + + column = columns.Single(c => c.Name == "D"); + Assert.Equal("((1))", column.DefaultValueSql); + Assert.Equal(true, column.DefaultValue); + + column = columns.Single(c => c.Name == "E"); + Assert.Equal("('FaLse')", column.DefaultValueSql); + Assert.Equal(false, column.DefaultValue); + + column = columns.Single(c => c.Name == "F"); + Assert.Equal("('tRuE')", column.DefaultValueSql); + Assert.Equal(true, column.DefaultValue); + + column = columns.Single(c => c.Name == "G"); + Assert.Equal("(CONVERT([bit],'tRUE'))", column.DefaultValueSql); + Assert.Equal(true, column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_DateTime_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A datetime DEFAULT '1973-09-03T12:00:01.0020000', + B datetime2 DEFAULT ('1968-10-23'), + C datetime2 DEFAULT (CONVERT ([datetime2],('1973-09-03T01:02:03'))), + D datetime DEFAULT (CONVERT(datetime,'12:12:12')), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("('1973-09-03T12:00:01.0020000')", column.DefaultValueSql); + Assert.Equal(new DateTime(1973, 9, 3, 12, 0, 1, 2, DateTimeKind.Unspecified), column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("('1968-10-23')", column.DefaultValueSql); + Assert.Equal(new DateTime(1968, 10, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("(CONVERT([datetime2],'1973-09-03T01:02:03'))", column.DefaultValueSql); + Assert.Equal(new DateTime(1973, 9, 3, 1, 2, 3, 0, DateTimeKind.Unspecified), column.DefaultValue); + + column = columns.Single(c => c.Name == "D"); + Assert.Equal("(CONVERT([datetime],'12:12:12'))", column.DefaultValueSql); + Assert.Equal(12, ((DateTime)column.DefaultValue!).Hour); + Assert.Equal(12, ((DateTime)column.DefaultValue!).Minute); + Assert.Equal(12, ((DateTime)column.DefaultValue!).Second); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Non_literal_or_non_parsable_DateTime_default_values_are_passed_through() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A datetime2 DEFAULT (CONVERT([datetime2],(getdate()))), + B datetime DEFAULT getdate(), + C datetime2 DEFAULT ((CONVERT([datetime2],('12-01-16 12:32')))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("(CONVERT([datetime2],getdate()))", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(getdate())", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("(CONVERT([datetime2],'12-01-16 12:32'))", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_DateOnly_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A date DEFAULT ('1968-10-23'), + B date DEFAULT (CONVERT([date],('1973-09-03T01:02:03'))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("('1968-10-23')", column.DefaultValueSql); + Assert.Equal(new DateOnly(1968, 10, 23), column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(CONVERT([date],'1973-09-03T01:02:03'))", column.DefaultValueSql); + Assert.Equal(new DateOnly(1973, 9, 3), column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_TimeOnly_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A time DEFAULT ('12:00:01.0020000'), + B time DEFAULT (CONVERT([time],('1973-09-03T01:02:03'))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("('12:00:01.0020000')", column.DefaultValueSql); + Assert.Equal(new TimeOnly(12, 0, 1, 2), column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(CONVERT([time],'1973-09-03T01:02:03'))", column.DefaultValueSql); + Assert.Equal(new TimeOnly(1, 2, 3), column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_DateTimeOffset_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A datetimeoffset DEFAULT ('1973-09-03T12:00:01.0000000+10:00'), + B datetimeoffset DEFAULT (CONVERT([datetimeoffset],('1973-09-03T01:02:03'))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("('1973-09-03T12:00:01.0000000+10:00')", column.DefaultValueSql); + Assert.Equal( + new DateTimeOffset(new DateTime(1973, 9, 3, 12, 0, 1, 0, DateTimeKind.Unspecified), new TimeSpan(0, 10, 0, 0, 0)), + column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(CONVERT([datetimeoffset],'1973-09-03T01:02:03'))", column.DefaultValueSql); + Assert.Equal( + new DateTime(1973, 9, 3, 1, 2, 3, 0, DateTimeKind.Unspecified), + ((DateTimeOffset)column.DefaultValue!).DateTime); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_Guid_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A uniqueidentifier DEFAULT ('0E984725-C51C-4BF4-9960-E1C80E27ABA0'), + B uniqueidentifier DEFAULT (CONVERT([uniqueidentifier],('0E984725-C51C-4BF4-9960-E1C80E27ABA0'))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("('0E984725-C51C-4BF4-9960-E1C80E27ABA0')", column.DefaultValueSql); + Assert.Equal(new Guid("0E984725-C51C-4BF4-9960-E1C80E27ABA0"), column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(CONVERT([uniqueidentifier],'0E984725-C51C-4BF4-9960-E1C80E27ABA0'))", column.DefaultValueSql); + Assert.Equal(new Guid("0E984725-C51C-4BF4-9960-E1C80E27ABA0"), column.DefaultValue); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Non_literal_Guid_default_values_are_passed_through() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A uniqueidentifier DEFAULT (CONVERT([uniqueidentifier],(newid()))), + B uniqueidentifier DEFAULT NEWSEQUENTIALID(), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("(CONVERT([uniqueidentifier],newid()))", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("(newsequentialid())", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_string_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A nvarchar(max) DEFAULT 'Hot', + B varchar(max) DEFAULT ('Buttered'), + C character(100) DEFAULT (''), + D text DEFAULT (N''), + E nvarchar(100) DEFAULT ( N' Toast! ') , + F nvarchar(20) DEFAULT (CONVERT([nvarchar](20),('Scones'))) , + G varchar(max) DEFAULT (CONVERT(character varying(max),('Toasted teacakes'))), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("('Hot')", column.DefaultValueSql); + Assert.Equal("Hot", column.DefaultValue); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("('Buttered')", column.DefaultValueSql); + Assert.Equal("Buttered", column.DefaultValue); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("('')", column.DefaultValueSql); + Assert.Equal("", column.DefaultValue); + + column = columns.Single(c => c.Name == "D"); + Assert.Equal("(N'')", column.DefaultValueSql); + Assert.Equal("", column.DefaultValue); + + column = columns.Single(c => c.Name == "E"); + Assert.Equal("(N' Toast! ')", column.DefaultValueSql); + Assert.Equal(" Toast! ", column.DefaultValue); + + column = columns.Single(c => c.Name == "F"); + Assert.Equal("(CONVERT([nvarchar](20),'Scones'))", column.DefaultValueSql); + Assert.Equal("Scones", column.DefaultValue); + + column = columns.Single(c => c.Name == "G"); + Assert.Equal("(CONVERT([varchar](max),'Toasted teacakes'))", column.DefaultValueSql); + Assert.Equal("Toasted teacakes", column.DefaultValue); + }, + "DROP TABLE MyTable;"); [ConditionalFact] public void ValueGenerated_is_set_for_identity_and_computed_column() diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDatabaseCleaner.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDatabaseCleaner.cs index 1e96689222a..050192bf665 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDatabaseCleaner.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDatabaseCleaner.cs @@ -1,24 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; -using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; -using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal; namespace Microsoft.EntityFrameworkCore.TestUtilities; public class SqlServerDatabaseCleaner : RelationalDatabaseCleaner { protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) - => new SqlServerDatabaseModelFactory( - new DiagnosticsLogger( - loggerFactory, - new LoggingOptions(), - new DiagnosticListener("Fake"), - new SqlServerLoggingDefinitions(), - new NullDbContextLogger())); + { + var services = new ServiceCollection(); + services.AddEntityFrameworkSqlServer(); + + new SqlServerDesignTimeServices().ConfigureDesignTimeServices(services); + + return services + .BuildServiceProvider() // No scope validation; cleaner violates scopes, but only resolve services once. + .GetRequiredService(); + } protected override bool AcceptTable(DatabaseTable table) => !(table is DatabaseView); diff --git a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs index b0a07b941e1..0e188b9df3b 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; using Microsoft.EntityFrameworkCore.Sqlite.Design.Internal; -using Microsoft.EntityFrameworkCore.Sqlite.Diagnostics.Internal; namespace Microsoft.EntityFrameworkCore.TestUtilities; @@ -13,19 +11,9 @@ public class SqliteDatabaseCleaner : RelationalDatabaseCleaner { protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) { - // NOTE: You may need to update AddEntityFrameworkDesignTimeServices() too - var services = new ServiceCollection() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(new DiagnosticListener(DbLoggerCategory.Name)) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(typeof(IDiagnosticsLogger<>), typeof(DiagnosticsLogger<>)) - .AddSingleton() - .AddSingleton() - .AddLogging(); + var services = new ServiceCollection(); + services.AddEntityFrameworkSqlite(); + new SqliteDesignTimeServices().ConfigureDesignTimeServices(services); return services