Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Data;
using System.Threading.Tasks;
using DotNetProjects.Migrator.Framework;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ 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;

const string testTableName = "MyDefaultTestTable";

const string dateTimeColumnName1 = "datetimecolumn1";
const string dateTimeColumnName2 = "datetimecolumn2";
const string dateTimeOffsetColumnName1 = "datetimeoffset1";
const string decimalColumnName1 = "decimalcolumn";
const string guidColumnName1 = "guidcolumn1";
const string booleanColumnName1 = "booleancolumn1";
Expand All @@ -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),

Expand All @@ -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));
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
Loading