All skills
github avatar

/reviewing-oracle-to-postgres-migration

@0b950f9 official
by githubgithub/awesome-copilot40k stars
5,040

Identifies Oracle-to-PostgreSQL migration risks by cross-referencing code against known behavioral differences (empty strings, refcursors, type coercion, sorting/collations, UNION ALL planner risks, materialized-view refresh requirements, timestamps, concurrent transactions, etc.). Use when planning a database migration, reviewing migration artifacts, or validating that integration tests cover Oracle/PostgreSQL differences.

Use this Skill: https://skilld.dev/gh/github/awesome-copilot/reviewing-oracle-to-postgres-migration

This session only. Nothing lands on disk.

referencesoracle-to-postgres-timestamp-timezone.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Oracle to PostgreSQL: CURRENT_TIMESTAMP and NOW() Timezone Handling

Contents

  • Problem
  • Behavior Comparison
  • PostgreSQL Timezone Precedence
  • Common Error Symptoms
  • Migration Actions — Npgsql config, DateTime normalization, stored procedures, session timezone, application code
  • Integration Test Patterns
  • Checklist

Problem

Oracle's CURRENT_TIMESTAMP returns a value in the session timezone and stores it in the column's declared precision. When .NET reads this value back via ODP.NET, it is surfaced as a DateTime with Kind=Local, reflecting the OS timezone of the client.

PostgreSQL's CURRENT_TIMESTAMP and NOW() both return a timestamptz (timestamp with time zone) anchored to UTC, regardless of the session timezone setting. How Npgsql surfaces this value depends on the driver version and configuration:

  • Npgsql < 6 / legacy mode (EnableLegacyTimestampBehavior = true): timestamptz columns are returned as DateTime with Kind=Unspecified. This is the source of silent timezone bugs when migrating from Oracle.
  • Npgsql 6+ with legacy mode disabled (the new default): timestamptz columns are returned as DateTime with Kind=Utc, and writing a Kind=Unspecified value throws an exception at insertion time.

Projects that have not yet upgraded to Npgsql 6+, or that explicitly opt back into legacy mode, remain vulnerable to the Kind=Unspecified issue. This mismatch — and the ease of accidentally re-enabling legacy mode — causes silent data corruption, incorrect comparisons, and off-by-N-hours bugs that are extremely difficult to trace.


Behavior Comparison

Aspect Oracle PostgreSQL
CURRENT_TIMESTAMP type TIMESTAMP WITH LOCAL TIME ZONE timestamptz (UTC-normalised)
Client DateTime.Kind via driver Local Unspecified (Npgsql < 6 / legacy mode); Utc (Npgsql 6+ default)
Session timezone influence Yes — affects stored/returned value Affects display only; UTC stored internally
NOW() equivalent SYSDATE / CURRENT_TIMESTAMP NOW() = CURRENT_TIMESTAMP (both return timestamptz)
Implicit conversion on comparison Oracle applies session TZ offset PostgreSQL compares UTC; session TZ is display-only

PostgreSQL Timezone Precedence

PostgreSQL resolves the effective session timezone using the following hierarchy (highest priority wins):

Level How it is set
Session SET TimeZone = 'UTC' sent at connection open
Role ALTER ROLE app_user SET TimeZone = 'UTC'
Database ALTER DATABASE mydb SET TimeZone = 'UTC'
Server postgresql.conf → TimeZone = 'America/New_York'

The session timezone does not affect the stored UTC value of a timestamptz column — it only controls how SHOW timezone and ::text casts format a value for display. Application code that relies on DateTime.Kind or compares timestamps without an explicit timezone can produce incorrect results if the server's default timezone is not UTC.


Common Error Symptoms

  • Timestamps read from PostgreSQL have Kind=Unspecified; comparisons with DateTime.UtcNow or DateTime.Now produce incorrect results.
  • Date-range queries return too few or too many rows because the WHERE clause comparison is evaluated in a timezone that differs from the stored UTC value.
  • Integration tests pass on a developer machine (UTC OS timezone) but fail in CI or production (non-UTC timezone).
  • Stored procedure output parameters carrying timestamps arrive with a session-offset applied by the server but are then compared to UTC values in the application.

Migration Actions

1. Configure Npgsql for UTC via Connection String or AppContext

Npgsql 6+ ships with EnableLegacyTimestampBehavior set to false by default, which causes timestamptz values to be returned as DateTime with Kind=Utc. Explicitly setting the switch at startup is still recommended to guard against accidental opt-in to legacy mode (e.g., via a config file or a transitive dependency) and to make the intent visible to future maintainers:

// Program.cs / Startup.cs — apply once at application start
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", false);

With this switch disabled, Npgsql throws if you try to write a DateTime with Kind=Unspecified to a timestamptz column, making timezone bugs loud and detectable at insertion time rather than silently at query time.

2. Normalise DateTime Values Before Persistence

Replace any DateTime.Now with DateTime.UtcNow throughout the migrated codebase. For values that originate from external input (e.g., user-provided dates deserialized from JSON), ensure they are converted to UTC before being saved:

// Before (Oracle-era code — relied on session/OS timezone)
var timestamp = DateTime.Now;

// After (PostgreSQL-compatible)
var timestamp = DateTime.UtcNow;

// For externally-supplied values
var utcTimestamp = dateTimeInput.Kind == DateTimeKind.Utc
    ? dateTimeInput
    : dateTimeInput.ToUniversalTime();

3. Fix Stored Procedures Using CURRENT_TIMESTAMP / NOW()

Stored procedures that assign CURRENT_TIMESTAMP or NOW() to a timestamp without time zone (timestamp) column must be reviewed. Prefer timestamptz columns or cast explicitly:

-- Ambiguous: server timezone influences interpretation
INSERT INTO audit_log (created_at) VALUES (NOW()::timestamp);

-- Safe: always UTC
INSERT INTO audit_log (created_at) VALUES (NOW() AT TIME ZONE 'UTC');

-- Or: use timestamptz column type and let PostgreSQL store UTC natively
INSERT INTO audit_log (created_at) VALUES (CURRENT_TIMESTAMP);

4. Force Session Timezone on Connection Open (Defence-in-Depth)

Regardless of role or database defaults, set the session timezone explicitly when opening a connection. This guarantees consistent behavior independent of server configuration:

// Npgsql connection string approach
var connString = "Host=localhost;Database=mydb;Username=app;Password=...;Timezone=UTC";

// Or: apply via NpgsqlDataSourceBuilder
var dataSource = new NpgsqlDataSourceBuilder(connString)
    .Build();

// Or: execute on every new connection
await using var conn = new NpgsqlConnection(connString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand("SET TimeZone = 'UTC'", conn);
await cmd.ExecuteNonQueryAsync();

5. Application Code — Avoid DateTime.Kind=Unspecified

Audit all repository and data-access code that reads timestamp columns. Where Npgsql returns Unspecified, either configure the data source globally (option 1 above) or wrap the read:

// Safe reader helper — convert Unspecified to Utc at the boundary
DateTime ReadUtcDateTime(NpgsqlDataReader reader, int ordinal)
{
    var dt = reader.GetDateTime(ordinal);
    return dt.Kind == DateTimeKind.Unspecified
        ? DateTime.SpecifyKind(dt, DateTimeKind.Utc)
        : dt.ToUniversalTime();
}

Integration Test Patterns

Test: Verify timestamps persist and return as UTC

[Fact]
public async Task InsertedTimestamp_ShouldRoundTripAsUtc()
{
    var before = DateTime.UtcNow;

    await repository.InsertAuditEntryAsync(/* ... */);

    var retrieved = await repository.GetLatestAuditEntryAsync();

    Assert.Equal(DateTimeKind.Utc, retrieved.CreatedAt.Kind);
    Assert.True(retrieved.CreatedAt >= before,
        "Persisted CreatedAt should not be earlier than the pre-insert UTC timestamp.");
}

Test: Verify timestamp comparisons across Oracle and PostgreSQL baselines

[Fact]
public async Task TimestampComparison_ShouldReturnSameRowsAsOracle()
{
    var cutoff = DateTime.UtcNow.AddDays(-1);

    var oracleResults = await oracleRepository.GetEntriesAfter(cutoff);
    var postgresResults = await postgresRepository.GetEntriesAfter(cutoff);

    Assert.Equal(oracleResults.Count, postgresResults.Count);
}

Checklist

  • AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", false) applied at application startup.
  • All DateTime.Now usages in data-access code replaced with DateTime.UtcNow.
  • Connection string or connection-open hook sets Timezone=UTC / SET TimeZone = 'UTC'.
  • Stored procedures that use CURRENT_TIMESTAMP or NOW() reviewed; timestamp without time zone columns explicitly cast or replaced with timestamptz.
  • Integration tests assert DateTime.Kind == Utc on retrieved timestamp values.
  • Tests cover date-range queries to confirm row counts match Oracle baseline.

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    No security issues detected. The skill provides reference documentation and workflows for migrating databases from Oracle to PostgreSQL.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    2/11 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 0b950f9. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 months ago

README badge

README badge for github/awesome-copilot/reviewing-oracle-to-postgres-migration

Identifies Oracle-to-PostgreSQL migration risks by cross-referencing code against known behavioral differences in empty strings, refcursors, type coercion, sorting, timestamps, and concurrent transactions. Use this skill when planning a migration, reviewing completed migration work, or validating that integration tests cover the semantic differences between the two databases.

Generated from the current SKILL.md.

What Oracle-to-PostgreSQL differences does this skill cover?
The skill references known behavioral differences including empty strings, refcursors, type coercion, sorting, timestamps, and concurrent transactions. Specific insights are documented in the references/ folder and screened for applicability to your migration scope.
Should I use this for planning or validation?
Both. Use the risk assessment workflow before migration to identify which differences apply to your scope. Use the validation workflow after migration to confirm each applicable difference was addressed and is covered by integration tests.
Does this automatically fix migration code?
No. The skill identifies risks and surfaces reference documentation. You review the references, decide which insights apply, and determine the fix pattern for each applicable difference.
What if a reference insight requires a design decision?
Flag it during the risk assessment step. For example, the empty-string-as-NULL difference may require choosing whether to preserve Oracle semantics or adopt PostgreSQL behavior.

Generated from the current SKILL.md. These answers refresh after source changes.