chore: publish from main

This commit is contained in:
github-actions[bot]
2026-08-11 02:48:20 +00:00
parent dab2d38097
commit be857f7808
28 changed files with 1057 additions and 136 deletions
@@ -4,7 +4,10 @@
| --- | --- |
| [empty-strings-handling.md](empty-strings-handling.md) | Oracle treats '' as NULL; PostgreSQL keeps empty strings distinct—patterns to align behavior in code, tests, and migrations. |
| [no-data-found-exceptions.md](no-data-found-exceptions.md) | Oracle SELECT INTO raises "no data found"; PostgreSQL doesnt—add explicit NOT FOUND handling to mirror Oracle behavior. |
| [oracle-nvl-decode-functions.md](oracle-nvl-decode-functions.md) | Oracle NVL/NVL2/DECODE have no direct equivalents—replace with COALESCE, CASE WHEN, and NULL-safe comparisons. |
| [oracle-parentheses-from-clause.md](oracle-parentheses-from-clause.md) | Oracle allows `FROM(TABLE_NAME)` syntax; PostgreSQL requires `FROM TABLE_NAME`—remove unnecessary parentheses around table names. |
| [oracle-rownum-pagination.md](oracle-rownum-pagination.md) | Oracle ROWNUM is assigned before ORDER BY; replace top-N and pagination patterns with LIMIT/OFFSET. |
| [oracle-sysdate-sequences-dual.md](oracle-sysdate-sequences-dual.md) | Replace SYSDATE/SYSTIMESTAMP with NOW(), sequence .NEXTVAL with nextval('seq'), and remove FROM DUAL. |
| [oracle-to-postgres-sorting.md](oracle-to-postgres-sorting.md) | How to preserve Oracle-like ordering in PostgreSQL using COLLATE "C" and DISTINCT wrapper patterns. |
| [oracle-to-postgres-to-char-numeric.md](oracle-to-postgres-to-char-numeric.md) | Oracle allows TO_CHAR(numeric) without format; PostgreSQL requires format string—use CAST(numeric AS TEXT) instead. |
| [oracle-to-postgres-type-coercion.md](oracle-to-postgres-type-coercion.md) | PostgreSQL strict type checks vs. Oracle implicit coercion—fix comparison errors by quoting or casting literals. |
@@ -0,0 +1,79 @@
# Oracle to PostgreSQL: NVL, DECODE, and Null-Handling Functions
## Problem
Oracle provides several functions — `NVL`, `NVL2`, `DECODE` — that have no direct equivalents in standard SQL but are commonly used in Oracle stored procedures and inline SQL. PostgreSQL uses standard SQL alternatives: `COALESCE`, `NULLIF`, and `CASE` expressions.
## Behavior Comparison
### NVL
**Oracle:** `NVL(expr, replacement)` — returns `replacement` if `expr` is `NULL`, otherwise `expr`.
**PostgreSQL:** Use `COALESCE(expr, replacement)` — semantically identical for two arguments.
```sql
-- Oracle
NVL(column_name, 'default')
-- PostgreSQL
COALESCE(column_name, 'default')
```
### NVL2
**Oracle:** `NVL2(expr, not_null_val, null_val)` — returns `not_null_val` if `expr` IS NOT NULL, `null_val` if NULL.
**PostgreSQL:** No direct equivalent — use `CASE`:
```sql
-- Oracle
NVL2(column_name, 'has value', 'no value')
-- PostgreSQL
CASE WHEN column_name IS NOT NULL THEN 'has value' ELSE 'no value' END
```
### DECODE
**Oracle:** `DECODE(expr, search1, result1, search2, result2, ..., default)` — equality-based switch.
**PostgreSQL:** No `DECODE` function — use `CASE WHEN`:
```sql
-- Oracle
DECODE(status, 1, 'Active', 2, 'Inactive', 'Unknown')
-- PostgreSQL
CASE status
WHEN 1 THEN 'Active'
WHEN 2 THEN 'Inactive'
ELSE 'Unknown'
END
```
Note: `DECODE` in Oracle treats two `NULL` values as equal (unlike `=`). If any search value is `NULL`, use `IS NULL` in the `CASE` equivalent:
```sql
-- Oracle: DECODE treats NULL = NULL
DECODE(col, NULL, 'empty', col)
-- PostgreSQL
CASE WHEN col IS NULL THEN 'empty' ELSE col END
```
## Migration Actions
### 1. Stored Procedures
Apply the direct substitutions above. Pay special attention to:
- `NVL` on numeric expressions — `COALESCE` is type-sensitive in PostgreSQL; ensure both arguments are the same type or cast explicitly.
- `DECODE` with `NULL` search values — replace with `IS NULL` guard in the `CASE` expression.
### 2. Application Code (inline SQL strings)
Search C# string literals and query builders for `NVL(`, `NVL2(`, and `DECODE(`. Apply the same substitutions.
### 3. Tests
Write test cases that exercise `NULL` inputs specifically — the Oracle → PostgreSQL translation of `NVL`/`COALESCE` is straightforward, but edge cases around NULL equality in `DECODE``CASE` are a common source of silent behavioral differences.
@@ -0,0 +1,75 @@
# Oracle to PostgreSQL: ROWNUM Pagination vs LIMIT/OFFSET
## Problem
Oracle uses `ROWNUM` pseudo-column for pagination and row-limiting. PostgreSQL uses standard `LIMIT` / `OFFSET` syntax. `ROWNUM` is also fundamentally different in *when* it is assigned, which affects filtering behavior.
## Behavior Comparison
**Oracle:**
- `ROWNUM` is assigned before `ORDER BY` — filtering with `ROWNUM` on an unordered result set is non-deterministic
- Common pattern to get "top N ordered rows" requires a subquery:
```sql
SELECT * FROM (SELECT * FROM t ORDER BY col) WHERE ROWNUM <= 10
```
- `ROWNUM BETWEEN n AND m` requires a double-wrapped subquery
**PostgreSQL:**
- `LIMIT n` restricts result rows after `ORDER BY` is applied — straightforward and deterministic
- `OFFSET n` skips rows; combine with `LIMIT` for pagination
- No `ROWNUM` pseudo-column exists
## Code Example
```sql
-- Oracle: top 10 rows by date
SELECT * FROM (
SELECT * FROM orders ORDER BY created_at DESC
) WHERE ROWNUM <= 10;
-- PostgreSQL equivalent
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- Oracle: rows 1120 (keyset pagination via ROWNUM)
SELECT * FROM (
SELECT t.*, ROWNUM rn FROM (
SELECT * FROM orders ORDER BY created_at DESC
) t WHERE ROWNUM <= 20
) WHERE rn > 10;
-- PostgreSQL equivalent
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 10;
```
## Migration Actions
### 1. Stored Procedures
Replace all `ROWNUM`-based limiting patterns with `LIMIT`/`OFFSET`:
```sql
-- Oracle
WHERE ROWNUM = 1
WHERE ROWNUM <= :n
-- PostgreSQL
LIMIT 1
LIMIT :n -- note: use $n parameter style in PL/pgSQL
```
For subquery wrapping patterns:
```sql
-- Oracle
SELECT * FROM (SELECT ... ORDER BY col) WHERE ROWNUM <= :n
-- PostgreSQL
SELECT ... ORDER BY col LIMIT :n
```
### 2. Application Code (inline SQL strings)
Search for `ROWNUM` in C# string literals, `StringBuilder`, and query-builder methods. Apply the same replacement patterns above.
### 3. Tests
Ensure integration tests validate that result set sizes are correct and that ordering is preserved (i.e., the correct *n* rows are returned, not just any *n* rows).
@@ -0,0 +1,86 @@
# Oracle to PostgreSQL: Date Functions, Sequences, and DUAL
## Problem
Oracle relies on several built-in constructs — `SYSDATE`, `SYSTIMESTAMP`, sequence `NEXTVAL` syntax, and the `DUAL` dummy table — that do not exist in PostgreSQL. Each requires a direct substitution.
## SYSDATE and SYSTIMESTAMP
**Oracle:**
- `SYSDATE` — returns the current date and time (no time zone) as an Oracle `DATE` type
- `SYSTIMESTAMP` — returns the current timestamp with time zone
**PostgreSQL:**
- Use `NOW()` or `CURRENT_TIMESTAMP` for timestamp with time zone
- Use `CURRENT_DATE` for date only
- Use `LOCALTIMESTAMP` for timestamp without time zone (closer to Oracle's `SYSDATE` semantics)
```sql
-- Oracle
SELECT SYSDATE FROM DUAL;
INSERT INTO t (created_at) VALUES (SYSDATE);
-- PostgreSQL
SELECT NOW();
INSERT INTO t (created_at) VALUES (NOW());
-- or, if the column is DATE-only:
INSERT INTO t (created_at) VALUES (CURRENT_DATE);
```
> **Warning:** Oracle `DATE` stores date *and* time; PostgreSQL `DATE` stores date only. If Oracle columns typed as `DATE` carry a time component, the PostgreSQL target column should be `TIMESTAMP`, not `DATE`.
## Sequence NEXTVAL Syntax
**Oracle:**
```sql
SELECT my_sequence.NEXTVAL FROM DUAL;
INSERT INTO t (id) VALUES (my_sequence.NEXTVAL);
```
**PostgreSQL:**
```sql
SELECT nextval('my_sequence');
INSERT INTO t (id) VALUES (nextval('my_sequence'));
```
Key differences:
- PostgreSQL `nextval()` is a function call with the sequence name as a quoted string argument
- Oracle uses dot notation: `sequence_name.NEXTVAL`
- Oracle also has `CURRVAL` → PostgreSQL `currval('sequence_name')`
- If the column uses a `DEFAULT nextval(...)` constraint (set during Phase 4 DDL migration), application code can omit the sequence call entirely and omit the column from the `INSERT`
## DUAL Table
Oracle requires a `FROM DUAL` clause in `SELECT` statements that evaluate expressions without a real table. PostgreSQL does not have `DUAL` — expressions can be selected without a `FROM` clause.
```sql
-- Oracle
SELECT 1 + 1 FROM DUAL;
SELECT SYSDATE FROM DUAL;
SELECT my_sequence.NEXTVAL FROM DUAL;
-- PostgreSQL
SELECT 1 + 1;
SELECT NOW();
SELECT nextval('my_sequence');
```
> **orafce extension:** If `orafce` is installed, it provides a `DUAL` view that makes Oracle-style `FROM DUAL` queries work without changes. This is a useful transitional aid but should not be relied on permanently.
## Migration Actions
### 1. Stored Procedures
- Replace all `SYSDATE` / `SYSTIMESTAMP` references with `NOW()` or `CURRENT_TIMESTAMP` (verify column type — use `LOCALTIMESTAMP` if the target is `TIMESTAMP WITHOUT TIME ZONE`)
- Replace `sequence_name.NEXTVAL` with `nextval('sequence_name')`
- Replace `sequence_name.CURRVAL` with `currval('sequence_name')`
- Remove `FROM DUAL` from all expression-only `SELECT` statements
### 2. Application Code (inline SQL strings)
Search C# string literals for `SYSDATE`, `SYSTIMESTAMP`, `.NEXTVAL`, `.CURRVAL`, and `FROM DUAL`. Apply the same substitutions.
### 3. Tests
- Verify datetime assertions use timezone-safe comparisons (see `oracle-to-postgres-timestamp-timezone.md` for Npgsql-specific behavior)
- Verify sequence-dependent IDs are correctly populated in assertions