From 08e531e67f1315cdad92808ee3e885edce0548b2 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Fri, 23 Jun 2023 17:27:29 +0100 Subject: [PATCH 1/2] Parse default value literals for SQLite Part of #13613 and #9632 --- .../Internal/SqliteDatabaseModelFactory.cs | 271 ++++++++--- .../SqlServerDatabaseModelFactoryTest.cs | 2 +- .../SqliteDatabaseModelFactoryTest.cs | 460 +++++++++++++++++- 3 files changed, 635 insertions(+), 98 deletions(-) diff --git a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs index 7a4fc2c378f..a1c5dd34387 100644 --- a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs +++ b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs @@ -17,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Scaffolding.Internal; /// 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 partial class SqliteDatabaseModelFactory : DatabaseModelFactory +public class SqliteDatabaseModelFactory : DatabaseModelFactory { private static readonly HashSet _defaultClrTypes = new() { @@ -26,6 +26,7 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory typeof(byte[]), typeof(double) }; + private static readonly HashSet _boolTypes = new(StringComparer.OrdinalIgnoreCase) { "BIT", @@ -34,6 +35,7 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "LOGICAL", "YESNO" }; + private static readonly HashSet _uintTypes = new(StringComparer.OrdinalIgnoreCase) { "MEDIUMUINT", @@ -41,6 +43,7 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "UINT32", "UNSIGNEDINTEGER32" }; + private static readonly HashSet _ulongTypes = new(StringComparer.OrdinalIgnoreCase) { "BIGUINT", @@ -49,6 +52,7 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "UNSIGNEDINTEGER", "UNSIGNEDINTEGER64" }; + private static readonly HashSet _byteTypes = new(StringComparer.OrdinalIgnoreCase) { "BYTE", @@ -56,6 +60,7 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "UINT8", "UNSIGNEDINTEGER8" }; + private static readonly HashSet _shortTypes = new(StringComparer.OrdinalIgnoreCase) { "INT16", @@ -63,6 +68,7 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "SHORT", "SMALLINT" }; + private static readonly HashSet _longTypes = new(StringComparer.OrdinalIgnoreCase) { "BIGINT", @@ -70,6 +76,7 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "INTEGER64", "LONG" }; + private static readonly HashSet _sbyteTypes = new(StringComparer.OrdinalIgnoreCase) { "INT8", @@ -77,10 +84,11 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "SBYTE", "TINYSINT" }; - private static readonly HashSet _floatTypes = new(StringComparer.OrdinalIgnoreCase) - { - "SINGLE" - }; + + private static readonly HashSet _floatTypes = new(StringComparer.OrdinalIgnoreCase) { "SINGLE" }; + + private static readonly HashSet _decimalTypes = new(StringComparer.OrdinalIgnoreCase) { "DECIMAL" }; + private static readonly HashSet _ushortTypes = new(StringComparer.OrdinalIgnoreCase) { "SMALLUINT", @@ -88,44 +96,43 @@ public partial class SqliteDatabaseModelFactory : DatabaseModelFactory "UNSIGNEDINTEGER16", "USHORT" }; - private static readonly HashSet _timeOnlyTypes = new(StringComparer.OrdinalIgnoreCase) - { - "TIMEONLY" - }; + + private static readonly HashSet _timeOnlyTypes = new(StringComparer.OrdinalIgnoreCase) { "TIMEONLY" }; + private static readonly Dictionary _typesByName = new Dictionary - { - { "CURRENCY", typeof(decimal) }, - { "DATE", typeof(DateTime) }, - { "DATEONLY", typeof(DateOnly) }, - { "DATETIME", typeof(DateTime) }, - { "DATETIME2", typeof(DateTime) }, - { "DATETIMEOFFSET", typeof(DateTimeOffset) }, - { "DECIMAL", typeof(decimal) }, - { "GUID", typeof(Guid) }, - { "JSON", typeof(string) }, - { "MONEY", typeof(decimal) }, - { "NUMBER", typeof(decimal) }, - { "NUMERIC", typeof(decimal) }, - { "SMALLDATE", typeof(DateTime) }, - { "SMALLMONEY", typeof(decimal) }, - { "STRING", typeof(string) }, - { "TIME", typeof(TimeSpan) }, - { "TIMESPAN", typeof(TimeSpan) }, - { "TIMESTAMP", typeof(DateTime) }, - { "UNIQUEIDENTIFIER", typeof(Guid) }, - { "UUID", typeof(Guid) }, - { "XML", typeof(string) } - } - .Concat(_boolTypes.Select(t => KeyValuePair.Create(t, typeof(bool)))) - .Concat(_byteTypes.Select(t => KeyValuePair.Create(t, typeof(byte)))) - .Concat(_shortTypes.Select(t => KeyValuePair.Create(t, typeof(short)))) - .Concat(_sbyteTypes.Select(t => KeyValuePair.Create(t, typeof(sbyte)))) - .Concat(_floatTypes.Select(t => KeyValuePair.Create(t, typeof(float)))) - .Concat(_timeOnlyTypes.Select(t => KeyValuePair.Create(t, typeof(TimeOnly)))) - .Concat(_ushortTypes.Select(t => KeyValuePair.Create(t, typeof(ushort)))) - .Concat(_uintTypes.Select(t => KeyValuePair.Create(t, typeof(uint)))) - .Concat(_ulongTypes.Select(t => KeyValuePair.Create(t, typeof(ulong)))) - .ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase); + { + { "CURRENCY", typeof(decimal) }, + { "DATE", typeof(DateTime) }, + { "DATEONLY", typeof(DateOnly) }, + { "DATETIME", typeof(DateTime) }, + { "DATETIME2", typeof(DateTime) }, + { "DATETIMEOFFSET", typeof(DateTimeOffset) }, + { "GUID", typeof(Guid) }, + { "JSON", typeof(string) }, + { "MONEY", typeof(decimal) }, + { "NUMBER", typeof(decimal) }, + { "NUMERIC", typeof(decimal) }, + { "SMALLDATE", typeof(DateTime) }, + { "SMALLMONEY", typeof(decimal) }, + { "STRING", typeof(string) }, + { "TIME", typeof(TimeSpan) }, + { "TIMESPAN", typeof(TimeSpan) }, + { "TIMESTAMP", typeof(DateTime) }, + { "UNIQUEIDENTIFIER", typeof(Guid) }, + { "UUID", typeof(Guid) }, + { "XML", typeof(string) } + } + .Concat(_boolTypes.Select(t => KeyValuePair.Create(t, typeof(bool)))) + .Concat(_byteTypes.Select(t => KeyValuePair.Create(t, typeof(byte)))) + .Concat(_shortTypes.Select(t => KeyValuePair.Create(t, typeof(short)))) + .Concat(_sbyteTypes.Select(t => KeyValuePair.Create(t, typeof(sbyte)))) + .Concat(_floatTypes.Select(t => KeyValuePair.Create(t, typeof(float)))) + .Concat(_decimalTypes.Select(t => KeyValuePair.Create(t, typeof(decimal)))) + .Concat(_timeOnlyTypes.Select(t => KeyValuePair.Create(t, typeof(TimeOnly)))) + .Concat(_ushortTypes.Select(t => KeyValuePair.Create(t, typeof(ushort)))) + .Concat(_uintTypes.Select(t => KeyValuePair.Create(t, typeof(uint)))) + .Concat(_ulongTypes.Select(t => KeyValuePair.Create(t, typeof(ulong)))) + .ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase); private readonly IDiagnosticsLogger _logger; private readonly IRelationalTypeMappingSource _typeMappingSource; @@ -219,7 +226,7 @@ private static bool HasGeometryColumns(DbConnection connection) { using var command = connection.CreateCommand(); command.CommandText = -""" + """ SELECT COUNT(*) FROM "sqlite_master" WHERE "name" = 'geometry_columns' AND "type" = 'table' @@ -247,7 +254,7 @@ private void GetTables(DbConnection connection, DatabaseModel databaseModel, IEn using (var command = connection.CreateCommand()) { command.CommandText = -$""" + $""" SELECT "name", "type" FROM "sqlite_master" WHERE "type" IN ('table', 'view') AND instr("name", 'sqlite_') <> 1 AND "name" NOT IN ( @@ -314,7 +321,7 @@ private void GetColumns(DbConnection connection, DatabaseTable table) { using var command = connection.CreateCommand(); command.CommandText = -""" + """ SELECT "name", "type", "notnull", "dflt_value", "hidden" FROM pragma_table_xinfo(@table) WHERE "hidden" IN (0, 2, 3) @@ -332,12 +339,10 @@ ORDER BY "cid" var columnName = reader.GetString(0); var dataType = reader.GetString(1); var notNull = reader.GetBoolean(2); - var defaultValue = !reader.IsDBNull(3) - ? FilterClrDefaults(dataType, notNull, reader.GetString(3)) - : null; + var defaultValueSql = !reader.IsDBNull(3) ? reader.GetString(3) : null; var hidden = reader.GetInt64(4); - _logger.ColumnFound(table.Name, columnName, dataType, notNull, defaultValue); + _logger.ColumnFound(table.Name, columnName, dataType, notNull, defaultValueSql); string? collation = null; var autoIncrement = 0; @@ -365,7 +370,7 @@ ORDER BY "cid" Name = columnName, StoreType = dataType, IsNullable = !notNull, - DefaultValueSql = defaultValue, + DefaultValueSql = defaultValueSql, ValueGenerated = autoIncrement != 0 ? ValueGenerated.OnAdd : default(ValueGenerated?), @@ -382,23 +387,110 @@ ORDER BY "cid" } InferClrTypes(connection, table); + + ParseClrDefaults(table); } - private string? FilterClrDefaults(string dataType, bool notNull, string defaultValue) + private void ParseClrDefaults(DatabaseTable table) { - if (string.Equals(defaultValue, "null", StringComparison.OrdinalIgnoreCase)) + foreach (var column in table.Columns) { - return null; - } + var defaultValueSql = column.DefaultValueSql; + defaultValueSql = defaultValueSql?.Trim(); + if (string.IsNullOrEmpty(defaultValueSql)) + { + continue; + } - if (notNull - && defaultValue == "0" - && _typeMappingSource.FindMapping(dataType)?.ClrType.IsNumeric() == true) - { - return null; - } + var type = (Type?)column["ClrType"]; + if (type == null) + { + if (string.Equals(column.StoreType, "REAL", StringComparison.OrdinalIgnoreCase)) + { + type = typeof(double); + } + else + { + type = _typeMappingSource.FindMapping(column.StoreType!)?.ClrType; + if (type == null) + { + continue; + } + } + } + + Unwrap(); - return defaultValue; + if (defaultValueSql.Equals("NULL", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (type == typeof(bool) + && int.TryParse(defaultValueSql, out var intValue)) + { + column.DefaultValue = intValue != 0; + } + else if (type.IsNumeric()) + { + try + { + column.DefaultValue = Convert.ChangeType(defaultValueSql, type); + } + catch + { + // Ignored + } + } + else if (defaultValueSql.StartsWith('\'') + && defaultValueSql.EndsWith('\'')) + { + defaultValueSql = defaultValueSql.Substring(1, defaultValueSql.Length - 2); + + if (type == typeof(string)) + { + column.DefaultValue = defaultValueSql; + } + else if (type == typeof(bool) + && bool.TryParse(defaultValueSql, out var boolValue)) + { + column.DefaultValue = boolValue; + } + else if (type == typeof(Guid) + && Guid.TryParse(defaultValueSql, out var guid)) + { + column.DefaultValue = guid; + } + else if (type == typeof(DateTime) + && DateTime.TryParse(defaultValueSql, out var dateTime)) + { + column.DefaultValue = dateTime; + } + else if (type == typeof(DateOnly) + && DateOnly.TryParse(defaultValueSql, out var dateOnly)) + { + column.DefaultValue = dateOnly; + } + else if (type == typeof(TimeOnly) + && TimeOnly.TryParse(defaultValueSql, out var timeOnly)) + { + column.DefaultValue = timeOnly; + } + else if (type == typeof(DateTimeOffset) + && DateTimeOffset.TryParse(defaultValueSql, out var dateTimeOffset)) + { + column.DefaultValue = dateTimeOffset; + } + } + + void Unwrap() + { + while (defaultValueSql.StartsWith('(') && defaultValueSql.EndsWith(')')) + { + defaultValueSql = (defaultValueSql.Substring(1, defaultValueSql.Length - 2)).Trim(); + } + } + } } /// @@ -494,6 +586,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "bool"); } + if (_byteTypes.Contains(baseColumnType)) { if (min >= byte.MinValue @@ -506,6 +599,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "byte"); } + if (_shortTypes.Contains(baseColumnType)) { if (min >= short.MinValue @@ -518,6 +612,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "short"); } + if (_longTypes.Contains(baseColumnType)) { if (defaultClrTpe != typeof(long)) @@ -527,6 +622,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl continue; } + if (_sbyteTypes.Contains(baseColumnType)) { if (min >= sbyte.MinValue @@ -539,6 +635,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "sbyte"); } + if (_ushortTypes.Contains(baseColumnType)) { if (min >= ushort.MinValue @@ -551,6 +648,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "ushort"); } + if (_uintTypes.Contains(baseColumnType)) { if (min >= uint.MinValue @@ -563,6 +661,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "uint"); } + if (_ulongTypes.Contains(baseColumnType)) { column["ClrType"] = typeof(ulong); @@ -585,6 +684,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl continue; } + if (string.Equals(valueType, "TEXT", StringComparison.OrdinalIgnoreCase)) { var min = reader.GetString(offset + 1); @@ -596,30 +696,39 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl continue; } + if (Regex.IsMatch(max, @"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,7})?$", default, TimeSpan.FromMilliseconds(1000.0))) { column["ClrType"] = typeof(DateTime); continue; } - if (Regex.IsMatch(max, @"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,7})?[-+]\d{2}:\d{2}$", default, TimeSpan.FromMilliseconds(1000.0))) + + if (Regex.IsMatch( + max, @"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,7})?[-+]\d{2}:\d{2}$", default, + TimeSpan.FromMilliseconds(1000.0))) { column["ClrType"] = typeof(DateTimeOffset); continue; } + if (Regex.IsMatch(max, @"^-?\d+\.\d{1,28}$", default, TimeSpan.FromMilliseconds(1000.0))) { column["ClrType"] = typeof(decimal); continue; } - if (Regex.IsMatch(max, @"^(\d|[A-F]){8}-(\d|[A-F]){4}-(\d|[A-F]){4}-(\d|[A-F]){4}-(\d|[A-F]){12}$", default, TimeSpan.FromMilliseconds(1000.0))) + + if (Regex.IsMatch( + max, @"^(\d|[A-F]){8}-(\d|[A-F]){4}-(\d|[A-F]){4}-(\d|[A-F]){4}-(\d|[A-F]){12}$", default, + TimeSpan.FromMilliseconds(1000.0))) { column["ClrType"] = typeof(Guid); continue; } + if (Regex.IsMatch(max, @"^-?(\d+\.)?\d{2}:\d{2}:\d{2}(\.\d{1,7})?$", default, TimeSpan.FromMilliseconds(1000.0))) { if (_timeOnlyTypes.Contains(baseColumnType)) @@ -679,6 +788,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl continue; } + if (string.Equals(valueType, "BLOB", StringComparison.OrdinalIgnoreCase)) { if (defaultClrTpe != typeof(byte[])) @@ -688,6 +798,7 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl continue; } + if (string.Equals(valueType, "REAL", StringComparison.OrdinalIgnoreCase)) { var min = reader.GetDouble(offset + 1); @@ -706,6 +817,13 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "float"); } + if (_decimalTypes.Contains(baseColumnType)) + { + column["ClrType"] = typeof(decimal); + + continue; + } + if (defaultClrTpe != typeof(double)) { column["ClrType"] = typeof(double); @@ -726,12 +844,11 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl continue; } + if (baseColumnType.Contains("INT", StringComparison.OrdinalIgnoreCase) && !_longTypes.Contains(baseColumnType)) { column["ClrType"] = typeof(int); - - continue; } } @@ -743,7 +860,7 @@ private void GetPrimaryKey(DbConnection connection, DatabaseTable table) { using var command = connection.CreateCommand(); command.CommandText = -""" + """ SELECT "name" FROM pragma_index_list(@table) WHERE "origin" = 'pk' @@ -764,14 +881,13 @@ ORDER BY "seq" var primaryKey = new DatabasePrimaryKey { - Table = table, - Name = name.StartsWith("sqlite_", StringComparison.Ordinal) ? string.Empty : name + Table = table, Name = name.StartsWith("sqlite_", StringComparison.Ordinal) ? string.Empty : name }; _logger.PrimaryKeyFound(name, table.Name); command.CommandText = -""" + """ SELECT "name" FROM pragma_index_info(@index) ORDER BY "seqno" @@ -800,7 +916,7 @@ private static void GetRowidPrimaryKey( { using var command = connection.CreateCommand(); command.CommandText = -""" + """ SELECT "name" FROM pragma_table_info(@table) WHERE "pk" = 1 @@ -836,7 +952,7 @@ private void GetUniqueConstraints(DbConnection connection, DatabaseTable table) { using var command1 = connection.CreateCommand(); command1.CommandText = -""" + """ SELECT "name" FROM pragma_index_list(@table) WHERE "origin" = 'u' @@ -854,8 +970,7 @@ ORDER BY "seq" var constraintName = reader1.GetString(0); var uniqueConstraint = new DatabaseUniqueConstraint { - Table = table, - Name = constraintName.StartsWith("sqlite_", StringComparison.Ordinal) ? string.Empty : constraintName + Table = table, Name = constraintName.StartsWith("sqlite_", StringComparison.Ordinal) ? string.Empty : constraintName }; _logger.UniqueConstraintFound(constraintName, table.Name); @@ -863,7 +978,7 @@ ORDER BY "seq" using (var command2 = connection.CreateCommand()) { command2.CommandText = -""" + """ SELECT "name" FROM pragma_index_info(@index) ORDER BY "seqno" @@ -895,7 +1010,7 @@ private void GetIndexes(DbConnection connection, DatabaseTable table) { using var command1 = connection.CreateCommand(); command1.CommandText = -""" + """ SELECT "name", "unique" FROM pragma_index_list(@table) WHERE "origin" = 'c' AND instr("name", 'sqlite_') <> 1 @@ -922,7 +1037,7 @@ ORDER BY "seq" using (var command2 = connection.CreateCommand()) { command2.CommandText = -""" + """ SELECT "name", "desc" FROM pragma_index_xinfo(@index) WHERE key = 1 @@ -955,7 +1070,7 @@ private void GetForeignKeys(DbConnection connection, DatabaseTable table, IList< { using var command1 = connection.CreateCommand(); command1.CommandText = -""" + """ SELECT DISTINCT "id", "table", "on_delete" FROM pragma_foreign_key_list(@table) ORDER BY "id" @@ -994,7 +1109,7 @@ ORDER BY "id" using var command2 = connection.CreateCommand(); command2.CommandText = -""" + """ SELECT "seq", "from", "to" FROM pragma_foreign_key_list(@table) WHERE "id" = @id diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs index 9864bb0d71f..ce98aff8997 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs @@ -1598,7 +1598,7 @@ C tinyint DEFAULT ((CONVERT ( ""tinyint"", ( (7) ) ))), "DROP TABLE MyTable;"); [ConditionalFact] - public void Non_literal_intl_default_values_are_passed_through() + public void Non_literal_int_default_values_are_passed_through() => Test( @" CREATE TABLE MyTable ( diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs index 39524df0b26..de8744896c1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs @@ -641,25 +641,447 @@ GeneratedColumnStored AS (1 + 2) STORED }, "DROP TABLE ComputedColumnSql;"); - [ConditionalTheory] - [InlineData("DOUBLE NOT NULL DEFAULT 0")] - [InlineData("FLOAT NOT NULL DEFAULT 0")] - [InlineData("INT NOT NULL DEFAULT 0")] - [InlineData("INTEGER NOT NULL DEFAULT 0")] - [InlineData("REAL NOT NULL DEFAULT 0")] - [InlineData("NULL DEFAULT NULL")] - [InlineData("NOT NULL DEFAULT NULL")] - public void Column_default_value_is_ignored_when_clr_default(string columnSql) - => Test( - $"CREATE TABLE DefaultValueClr (IgnoredDefault {columnSql})", - Enumerable.Empty(), - Enumerable.Empty(), - dbModel => - { - var column = Assert.Single(Assert.Single(dbModel.Tables).Columns); - Assert.Null(column.DefaultValueSql); - }, - "DROP TABLE DefaultValueClr"); + [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))); + +INSERT INTO MyTable VALUES (1, 1, 1, 1, 1, 1, 1, 1);", + 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); + }, + "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)); + +INSERT INTO MyTable VALUES (1, 1, 1);", + 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); + }, + "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)); + +INSERT INTO MyTable VALUES (1, {long.MaxValue}, {long.MaxValue});", + 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); + }, + "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)); + +INSERT INTO MyTable VALUES (1, 1, 1);", + 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); + }, + "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)); + +INSERT INTO MyTable VALUES (1, 1.1, 1.2, 1.3);", + 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, (double)column.DefaultValue, 3); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("0.0", column.DefaultValueSql); + Assert.Equal((double)0, (double)column.DefaultValue, 3); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("1.1000000000000001e+000", column.DefaultValueSql); + Assert.Equal(1.1000000000000001e+000, (double)column.DefaultValue, 3); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_float_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A single DEFAULT -1.1111, + B single DEFAULT (0.0), + C single DEFAULT (1.1000000000000001e+000)); + +INSERT INTO MyTable VALUES (1, '1.1', '1.2', '1.3');", + 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, (float)column.DefaultValue, 0.01); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("0.0", column.DefaultValueSql); + Assert.Equal((float)0, (float)column.DefaultValue, 0.01); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("1.1000000000000001e+000", column.DefaultValueSql); + Assert.Equal((float)1.1000000000000001e+000, (float)column.DefaultValue, 0.01); + }, + "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)); + +INSERT INTO MyTable VALUES (1, '1.1', '1.2', '1.3');", + 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); + }, + "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)); + +INSERT INTO MyTable VALUES (1, 1, 1, 1, 1);", + 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); + }, + "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')); + +INSERT INTO MyTable VALUES (1, '2023-01-20 13:37:00', '2023-01-20 13:37:00');", + 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); + }, + "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 CURRENT_TIMESTAMP, + B datetime DEFAULT CURRENT_DATE); + +INSERT INTO MyTable VALUES (1, '2023-01-20 13:37:00', '2023-01-20 13:37:00');", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("CURRENT_TIMESTAMP", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("CURRENT_DATE", 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 (('1973-09-03T01:02:03'))); + +INSERT INTO MyTable VALUES (1, '2023-01-20', '2023-01-20');", + 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("('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 timeonly DEFAULT ('12:00:01.0020000')); + +INSERT INTO MyTable VALUES (1, '13:37:00.0000000');", + 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); + }, + "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')); + +INSERT INTO MyTable VALUES (1, '1973-09-03 12:00:01.0000000+10:00');", + 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); + }, + "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')); + +INSERT INTO MyTable VALUES (1, '993CDD7A-F4DF-4C5E-A810-8F51A11E9B6D');", + 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); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_string_literals_are_parsed_for_HasDefaultValue() + => Test( + @" +CREATE TABLE MyTable ( + Id int, + A nvarchar DEFAULT 'Hot', + B varchar DEFAULT ('Buttered'), + C character(100) DEFAULT (''), + D text DEFAULT (''), + E nvarchar(100) DEFAULT ( ' Toast! ')); + +INSERT INTO MyTable VALUES (1, 'A', 'Tale', 'Of', 'Two', 'Cities');", + 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("''", column.DefaultValueSql); + Assert.Equal("", column.DefaultValue); + + column = columns.Single(c => c.Name == "E"); + Assert.Equal("' Toast! '", column.DefaultValueSql); + Assert.Equal(" Toast! ", column.DefaultValue); + }, + "DROP TABLE MyTable;"); [ConditionalTheory] [InlineData(false)] From fe31d14c87844b2db65a1b9412e9a826f9df0b11 Mon Sep 17 00:00:00 2001 From: Brice Lambson Date: Wed, 19 Jul 2023 12:41:57 -0700 Subject: [PATCH 2/2] Fix parsing of decimal value literals for SQLite --- .../Internal/SqliteDatabaseModelFactory.cs | 32 +- .../SqliteDatabaseModelFactoryTest.cs | 690 +++++++++--------- 2 files changed, 359 insertions(+), 363 deletions(-) diff --git a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs index a1c5dd34387..9fda12ee21e 100644 --- a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs +++ b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs @@ -402,21 +402,13 @@ private void ParseClrDefaults(DatabaseTable table) continue; } - var type = (Type?)column["ClrType"]; + var typeHint = (Type?)column["ClrType"]; + var type = typeHint is null + ? _typeMappingSource.FindMapping(column.StoreType!)?.ClrType + : _typeMappingSource.FindMapping(typeHint, column.StoreType)?.ClrType; if (type == null) { - if (string.Equals(column.StoreType, "REAL", StringComparison.OrdinalIgnoreCase)) - { - type = typeof(double); - } - else - { - type = _typeMappingSource.FindMapping(column.StoreType!)?.ClrType; - if (type == null) - { - continue; - } - } + continue; } Unwrap(); @@ -431,7 +423,9 @@ private void ParseClrDefaults(DatabaseTable table) { column.DefaultValue = intValue != 0; } - else if (type.IsNumeric()) + else if (type.IsInteger() + || type == typeof(float) + || type == typeof(double)) { try { @@ -451,11 +445,6 @@ private void ParseClrDefaults(DatabaseTable table) { column.DefaultValue = defaultValueSql; } - else if (type == typeof(bool) - && bool.TryParse(defaultValueSql, out var boolValue)) - { - column.DefaultValue = boolValue; - } else if (type == typeof(Guid) && Guid.TryParse(defaultValueSql, out var guid)) { @@ -481,6 +470,11 @@ private void ParseClrDefaults(DatabaseTable table) { column.DefaultValue = dateTimeOffset; } + else if (type == typeof(decimal) + && decimal.TryParse(defaultValueSql, out var decimalValue)) + { + column.DefaultValue = decimalValue; + } } void Unwrap() diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs index de8744896c1..b1407afb23f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs @@ -506,6 +506,7 @@ CREATE TABLE ClrType ( [InlineData("TEXT", "'2023-01-20 13:37:00'", typeof(DateTime))] [InlineData("TEXT", "'2023-01-20 13:42:00-08:00'", typeof(DateTimeOffset))] [InlineData("TEXT", "'0.1'", typeof(decimal))] + [InlineData("DECIMAL", "'0.1'", typeof(decimal))] [InlineData("TEXT", "'00000000-0000-0000-0000-000000000000'", typeof(Guid))] [InlineData("TEXT", "'13:44:00'", typeof(TimeSpan))] [InlineData("TIMEONLY", "'14:34:00'", typeof(TimeOnly))] @@ -549,6 +550,7 @@ CREATE TABLE IF NOT EXISTS ClrTypeWithData ( [InlineData("DATETIME", "'A'", typeof(string))] [InlineData("DATETIMEOFFSET", "'A'", typeof(string))] [InlineData("DECIMAL", "'A'", typeof(string))] + [InlineData("DECIMAL", "0.1", typeof(decimal))] [InlineData("GUID", "'A'", typeof(string))] [InlineData("TIME", "'A'", typeof(string))] [InlineData("TIMEONLY", "'A'", typeof(string))] @@ -641,10 +643,10 @@ GeneratedColumnStored AS (1 + 2) STORED }, "DROP TABLE ComputedColumnSql;"); - [ConditionalFact] - public void Simple_int_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + [ConditionalFact] + public void Simple_int_literals_are_parsed_for_HasDefaultValue() + => Test( + @" CREATE TABLE MyTable ( Id int, A int DEFAULT -1, @@ -656,124 +658,124 @@ F int DEFAULT (3 ), G int DEFAULT ((4))); INSERT INTO MyTable VALUES (1, 1, 1, 1, 1, 1, 1, 1);", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_short_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "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)); INSERT INTO MyTable VALUES (1, 1, 1);", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_long_literals_are_parsed_for_HasDefaultValue() - => Test( - @$" + 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); + }, + "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)); INSERT INTO MyTable VALUES (1, {long.MaxValue}, {long.MaxValue});", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_byte_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "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)); INSERT INTO MyTable VALUES (1, 1, 1);", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_double_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_double_literals_are_parsed_for_HasDefaultValue() + => Test( + @" CREATE TABLE MyTable ( Id int, A float DEFAULT -1.1111, @@ -781,30 +783,30 @@ B float DEFAULT (0.0), C float DEFAULT (1.1000000000000001e+000)); INSERT INTO MyTable VALUES (1, 1.1, 1.2, 1.3);", - 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, (double)column.DefaultValue, 3); - - column = columns.Single(c => c.Name == "B"); - Assert.Equal("0.0", column.DefaultValueSql); - Assert.Equal((double)0, (double)column.DefaultValue, 3); - - column = columns.Single(c => c.Name == "C"); - Assert.Equal("1.1000000000000001e+000", column.DefaultValueSql); - Assert.Equal(1.1000000000000001e+000, (double)column.DefaultValue, 3); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_float_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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, (double)column.DefaultValue, 3); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("0.0", column.DefaultValueSql); + Assert.Equal((double)0, (double)column.DefaultValue, 3); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("1.1000000000000001e+000", column.DefaultValueSql); + Assert.Equal(1.1000000000000001e+000, (double)column.DefaultValue, 3); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_float_literals_are_parsed_for_HasDefaultValue() + => Test( + @" CREATE TABLE MyTable ( Id int, A single DEFAULT -1.1111, @@ -812,61 +814,61 @@ B single DEFAULT (0.0), C single DEFAULT (1.1000000000000001e+000)); INSERT INTO MyTable VALUES (1, '1.1', '1.2', '1.3');", - 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, (float)column.DefaultValue, 0.01); - - column = columns.Single(c => c.Name == "B"); - Assert.Equal("0.0", column.DefaultValueSql); - Assert.Equal((float)0, (float)column.DefaultValue, 0.01); - - column = columns.Single(c => c.Name == "C"); - Assert.Equal("1.1000000000000001e+000", column.DefaultValueSql); - Assert.Equal((float)1.1000000000000001e+000, (float)column.DefaultValue, 0.01); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_decimal_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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, (float)column.DefaultValue, 0.01); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("0.0", column.DefaultValueSql); + Assert.Equal((float)0, (float)column.DefaultValue, 0.01); + + column = columns.Single(c => c.Name == "C"); + Assert.Equal("1.1000000000000001e+000", column.DefaultValueSql); + Assert.Equal((float)1.1000000000000001e+000, (float)column.DefaultValue, 0.01); + }, + "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)); + A decimal DEFAULT '-1.1111', + B decimal DEFAULT ('0.0'), + C decimal DEFAULT ('0')); INSERT INTO MyTable VALUES (1, '1.1', '1.2', '1.3');", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_bool_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_bool_literals_are_parsed_for_HasDefaultValue() + => Test( + @" CREATE TABLE MyTable ( Id int, A bit DEFAULT 0, @@ -875,177 +877,177 @@ C bit DEFAULT (0), D bit DEFAULT (1)); INSERT INTO MyTable VALUES (1, 1, 1, 1, 1);", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_DateTime_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "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')); INSERT INTO MyTable VALUES (1, '2023-01-20 13:37:00', '2023-01-20 13:37:00');", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Non_literal_or_non_parsable_DateTime_default_values_are_passed_through() - => Test( - @" + 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); + }, + "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 CURRENT_TIMESTAMP, B datetime DEFAULT CURRENT_DATE); INSERT INTO MyTable VALUES (1, '2023-01-20 13:37:00', '2023-01-20 13:37:00');", - Enumerable.Empty(), - Enumerable.Empty(), - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - var column = columns.Single(c => c.Name == "A"); - Assert.Equal("CURRENT_TIMESTAMP", column.DefaultValueSql); - Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); - - column = columns.Single(c => c.Name == "B"); - Assert.Equal("CURRENT_DATE", column.DefaultValueSql); - Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_DateOnly_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + var column = columns.Single(c => c.Name == "A"); + Assert.Equal("CURRENT_TIMESTAMP", column.DefaultValueSql); + Assert.Null(column.FindAnnotation(RelationalAnnotationNames.DefaultValue)); + + column = columns.Single(c => c.Name == "B"); + Assert.Equal("CURRENT_DATE", 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 (('1973-09-03T01:02:03'))); INSERT INTO MyTable VALUES (1, '2023-01-20', '2023-01-20');", - 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("('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( - @" + 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("('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 timeonly DEFAULT ('12:00:01.0020000')); INSERT INTO MyTable VALUES (1, '13:37:00.0000000');", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_DateTimeOffset_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "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')); INSERT INTO MyTable VALUES (1, '1973-09-03 12:00:01.0000000+10:00');", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_Guid_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "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')); INSERT INTO MyTable VALUES (1, '993CDD7A-F4DF-4C5E-A810-8F51A11E9B6D');", - 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); - }, - "DROP TABLE MyTable;"); - - [ConditionalFact] - public void Simple_string_literals_are_parsed_for_HasDefaultValue() - => Test( - @" + 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); + }, + "DROP TABLE MyTable;"); + + [ConditionalFact] + public void Simple_string_literals_are_parsed_for_HasDefaultValue() + => Test( + @" CREATE TABLE MyTable ( Id int, A nvarchar DEFAULT 'Hot', @@ -1055,33 +1057,33 @@ D text DEFAULT (''), E nvarchar(100) DEFAULT ( ' Toast! ')); INSERT INTO MyTable VALUES (1, 'A', 'Tale', 'Of', 'Two', 'Cities');", - 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("''", column.DefaultValueSql); - Assert.Equal("", column.DefaultValue); - - column = columns.Single(c => c.Name == "E"); - Assert.Equal("' Toast! '", column.DefaultValueSql); - Assert.Equal(" Toast! ", column.DefaultValue); - }, - "DROP TABLE MyTable;"); + 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("''", column.DefaultValueSql); + Assert.Equal("", column.DefaultValue); + + column = columns.Single(c => c.Name == "E"); + Assert.Equal("' Toast! '", column.DefaultValueSql); + Assert.Equal(" Toast! ", column.DefaultValue); + }, + "DROP TABLE MyTable;"); [ConditionalTheory] [InlineData(false)]