diff --git a/src/GreenHome.DataCopy/Program.cs b/src/GreenHome.DataCopy/Program.cs index 16ba518..4d65e34 100644 --- a/src/GreenHome.DataCopy/Program.cs +++ b/src/GreenHome.DataCopy/Program.cs @@ -47,12 +47,6 @@ internal static class Program ?? throw new InvalidOperationException("Missing ConnectionStrings:Postgres"); var reset = args.Contains("--reset"); - // ponytail: this tool passes DateTime values through verbatim (SqlServer datetime2 reads - // back as Kind=Unspecified, the API's timestamptz columns reject that). Legacy behaviour - // makes Npgsql accept either Kind for this pass-through copy only — the API process does - // not enable it. Retire this if the copy ever has to reinterpret time zones. - AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); - using var source = new GreenHomeDbContext( new DbContextOptionsBuilder().UseSqlServer(sqlServerCs).Options); // the target must be the Postgres-specific context so Migrate() picks the Postgres @@ -170,6 +164,7 @@ internal static class Program .Where(where).OrderBy(orderBy).Take(batchSize).ToListAsync(); if (batch.Count == 0) break; + foreach (var item in batch) NormalizeTimestampsToUtc(item); target.Set().AddRange(batch); await target.SaveChangesAsync(); copied += batch.Count; @@ -204,6 +199,7 @@ internal static class Program foreach (var row in await source.Set().AsNoTracking().ToListAsync()) { if (existing.Contains(KeyOf(row, pkNames))) continue; + NormalizeTimestampsToUtc(row); target.Set().Add(row); existing.Add(KeyOf(row, pkNames)); copied++; @@ -217,6 +213,32 @@ internal static class Program // Plumbing // ============================================================ + /// + /// SqlServer's datetime2 carries no Kind, so values are read back as Unspecified. The app has + /// always written UTC (DateTime.UtcNow) and the target columns are timestamptz, which rejects an + /// Unspecified value. Tag every timestamp as Utc before it is written — this names the value, + /// it never shifts it, so the stored instant stays exactly what SqlServer held. + /// + static void NormalizeTimestampsToUtc(object entity) + { + foreach (var property in entity.GetType().GetProperties()) + { + if (!property.CanRead || !property.CanWrite) continue; + + if (property.PropertyType == typeof(DateTime)) + { + var value = (DateTime)property.GetValue(entity)!; + if (value.Kind != DateTimeKind.Utc) + property.SetValue(entity, DateTime.SpecifyKind(value, DateTimeKind.Utc)); + } + else if (property.PropertyType == typeof(DateTime?)) + { + if (property.GetValue(entity) is DateTime value && value.Kind != DateTimeKind.Utc) + property.SetValue(entity, DateTime.SpecifyKind(value, DateTimeKind.Utc)); + } + } + } + static async Task ScalarAsync(DbConnection conn, string sql) { await using var cmd = conn.CreateCommand();