From 2ef7e2387d7515feec1c43d325464456fbcd7fda Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Tue, 25 Nov 2025 08:46:50 +0100 Subject: [PATCH] Parse and set DateTimeOffset Default values in Postgre --- ...ransformationProvider_ChangeColumnTests.cs | 27 +++++++++ ...onProvider_GetColumns_DefaultValueTests.cs | 5 ++ .../Impl/PostgreSQL/PostgreSQLDialect.cs | 5 ++ .../PostgreSQLTransformationProvider.cs | 59 +++++++++++++++++++ 4 files changed, 96 insertions(+) diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs index cb91de8e..7aaa3227 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs @@ -1,3 +1,4 @@ +using System; using System.Data; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; @@ -15,4 +16,30 @@ public async Task SetUpAsync() { await BeginPostgreSQLTransactionAsync(); } + + [Test] + public void ChangeColumn_DateTimeOffsetToDateTime_Success() + { + // Arrange + var tableName = "TableName"; + var column1Name = "Column1"; + var column2Name = "Column2"; + var dateTimeDefaultValue = new DateTime(2025, 5, 4, 3, 2, 1, DateTimeKind.Utc); + var dateTimeInsert = new DateTime(2001, 2, 3, 4, 5, 6, 7, DateTimeKind.Utc); + + // Act + Provider.AddTable(tableName, + new Column(column1Name, DbType.Int32, ColumnProperty.Null), + new Column(column2Name, DbType.DateTimeOffset, ColumnProperty.Null, defaultValue: dateTimeDefaultValue) + ); + + Provider.Insert(table: tableName, columns: [column2Name], values: [dateTimeInsert]); + + // Assert + Provider.ChangeColumn(tableName, new Column(column2Name, DbType.DateTime2, ColumnProperty.NotNull)); + var column2 = Provider.GetColumnByName(tableName, column2Name); + + Assert.That(column2.MigratorDbType, Is.EqualTo(MigratorDbType.DateTime2)); + Assert.That(column2.DefaultValue, Is.Null); + } } diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_DefaultValueTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_DefaultValueTests.cs index d4b825ec..1d11bf0b 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_DefaultValueTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_DefaultValueTests.cs @@ -55,6 +55,7 @@ public void GetColumns_DefaultValues_Succeeds() { // Arrange var dateTimeDefaultValue = new DateTime(2000, 1, 2, 3, 4, 5, DateTimeKind.Utc); + var dateTimeOffsetDefaultValue = new DateTimeOffset(2022, 2, 2, 3, 3, 4, 4, TimeSpan.FromHours(1)); var guidDefaultValue = Guid.NewGuid(); var decimalDefaultValue = 14.56565m; @@ -62,6 +63,7 @@ public void GetColumns_DefaultValues_Succeeds() const string dateTimeColumnName1 = "datetimecolumn1"; const string dateTimeColumnName2 = "datetimecolumn2"; + const string dateTimeOffsetColumnName1 = "datetimeoffset1"; const string decimalColumnName1 = "decimalcolumn"; const string guidColumnName1 = "guidcolumn1"; const string booleanColumnName1 = "booleancolumn1"; @@ -76,6 +78,7 @@ public void GetColumns_DefaultValues_Succeeds() Provider.AddTable(testTableName, new Column(dateTimeColumnName1, DbType.DateTime, dateTimeDefaultValue), new Column(dateTimeColumnName2, DbType.DateTime2, dateTimeDefaultValue), + new Column(dateTimeOffsetColumnName1, DbType.DateTimeOffset, dateTimeOffsetDefaultValue), new Column(decimalColumnName1, DbType.Decimal, decimalDefaultValue), new Column(guidColumnName1, DbType.Guid, guidDefaultValue), @@ -96,6 +99,7 @@ public void GetColumns_DefaultValues_Succeeds() // Assert var dateTimeColumn1 = columns.Single(x => x.Name.Equals(dateTimeColumnName1, StringComparison.OrdinalIgnoreCase)); var dateTimeColumn2 = columns.Single(x => x.Name.Equals(dateTimeColumnName2, StringComparison.OrdinalIgnoreCase)); + var dateTimeOffsetColumn1 = columns.Single(x => x.Name.Equals(dateTimeOffsetColumnName1, StringComparison.OrdinalIgnoreCase)); var decimalColumn1 = columns.Single(x => x.Name.Equals(decimalColumnName1, StringComparison.OrdinalIgnoreCase)); var guidColumn1 = columns.Single(x => x.Name.Equals(guidColumnName1, StringComparison.OrdinalIgnoreCase)); var booleanColumn1 = columns.Single(x => x.Name.Equals(booleanColumnName1, StringComparison.OrdinalIgnoreCase)); @@ -108,6 +112,7 @@ public void GetColumns_DefaultValues_Succeeds() Assert.That(dateTimeColumn1.DefaultValue, Is.EqualTo(dateTimeDefaultValue)); Assert.That(dateTimeColumn2.DefaultValue, Is.EqualTo(dateTimeDefaultValue)); + Assert.That(dateTimeOffsetColumn1.DefaultValue, Is.EqualTo(dateTimeOffsetDefaultValue)); Assert.That(decimalColumn1.DefaultValue, Is.EqualTo(decimalDefaultValue)); Assert.That(guidColumn1.DefaultValue, Is.EqualTo(guidDefaultValue)); Assert.That(booleanColumn1.DefaultValue, Is.True); diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs index 1200b8f9..2987d1d8 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs @@ -129,6 +129,11 @@ public override string Default(object defaultValue) var convertedString = BitConverter.ToString(byteArray).Replace("-", "").ToLower(); return @$"DEFAULT E'\\x{convertedString}'"; } + else if (defaultValue is DateTimeOffset offset) + { + var convertedString = offset.ToString("yyyy-MM-dd HH:mm:ss.fffzzz"); + return @$"DEFAULT '{convertedString}'"; + } return base.Default(defaultValue); } diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index f7e750a0..a669f833 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -762,6 +762,65 @@ public override Column[] GetColumns(string table) throw new NotImplementedException($"Cannot parse {columnInfo.ColumnDefault} in column '{column.Name}'"); } } + else if (column.MigratorDbType == MigratorDbType.DateTimeOffset) + { + if (columnInfo.ColumnDefault.StartsWith("'")) + { + var match = stripSingleQuoteRegEx.Match(columnInfo.ColumnDefault); + + if (!match.Success) + { + throw new NotImplementedException($"Cannot parse {columnInfo.ColumnDefault} in column '{column.Name}'"); + } + + var singleQuoteString = match.Value; + + // 1) Normalize "Z" at the end → "+00:00" + singleQuoteString = Regex.Replace(singleQuoteString, @"Z$", "+00:00"); + + // 2) Normalize offset at the end of the string + // Cases handled: + // +HH → +HH:00 + // +HHMM → +HH:MM + // +HH:MM → stays unchanged + // -HH / -HHMM → same logic + singleQuoteString = Regex.Replace( + singleQuoteString, + @"([+-])(\d{2})(?::?(\d{2}))?$", + m => + { + var sign = m.Groups[1].Value; // "+" or "-" + var hh = m.Groups[2].Value; // hours + var hasMm = m.Groups[3].Success; // minutes present? + var mm = hasMm ? m.Groups[3].Value : "00"; + return $"{sign}{hh}:{mm}"; + } + ); + + // 3) Parse using multiple possible formats + // Supports both space and "T" separator, with/without milliseconds + var formats = new[] + { + "yyyy-MM-dd HH:mm:ss.fffzzz", // space separator, with ms + "yyyy-MM-dd HH:mm:sszzz", // space separator, no ms + "yyyy-MM-ddTHH:mm:ss.fffzzz", // ISO8601, with ms + "yyyy-MM-ddTHH:mm:sszzz" // ISO8601, no ms + }; + + var dateTimeOffset = DateTimeOffset.ParseExact( + singleQuoteString, + formats, + CultureInfo.InvariantCulture, + DateTimeStyles.None + ); + + column.DefaultValue = dateTimeOffset; + } + else + { + throw new NotImplementedException($"Cannot parse {columnInfo.ColumnDefault} in column '{column.Name}'"); + } + } else { throw new NotImplementedException($"{nameof(DbType)} {column.MigratorDbType} not implemented.");