# Oracle to PostgreSQL: TO_CHAR() Numeric Conversions

## Contents

- Problem
- Root Cause
- Solution Patterns — CAST, format string, concatenation
- Migration Checklist
- Application Code Review
- Testing Recommendations
- Common Locations
- Error Messages to Watch For

## Problem

Oracle allows `TO_CHAR()` to convert numeric types to strings without a format specifier:

```sql
-- Oracle: Works fine
SELECT TO_CHAR(vessel_id) FROM vessels;
SELECT TO_CHAR(fiscal_year) FROM certificates;
```

PostgreSQL requires a format string when using `TO_CHAR()` with numeric types, otherwise it raises:

```
42883: function to_char(numeric) does not exist
```

## Root Cause

- **Oracle**: `TO_CHAR(number)` without a format mask implicitly converts the number to a string using default formatting
- **PostgreSQL**: `TO_CHAR()` always requires an explicit format string for numeric types (e.g., `'999999'`, `'FM999999'`)

## Solution Patterns

### Pattern 1: Use CAST (Recommended)

The cleanest migration approach is to replace `TO_CHAR(numeric_column)` with `CAST(numeric_column AS TEXT)`:

```sql
-- Oracle
SELECT TO_CHAR(vessel_id) AS vessel_item FROM vessels;

-- PostgreSQL (preferred)
SELECT CAST(vessel_id AS TEXT) AS vessel_item FROM vessels;
```

**Advantages:**

- More idiomatic in PostgreSQL
- Clearer intent
- No format string needed

### Pattern 2: Provide Format String

If you need specific numeric formatting, use an explicit format mask:

```sql
-- PostgreSQL with format
SELECT TO_CHAR(vessel_id, 'FM999999') AS vessel_item FROM vessels;
SELECT TO_CHAR(amount, 'FM999999.00') AS amount_text FROM payments;
```

**Format masks:**

- `'FM999999'`: Fixed-width integer (FM = Fill Mode, removes leading spaces)
- `'FM999999.00'`: Decimal with 2 places
- `'999,999.00'`: With thousand separators

### Pattern 3: String Concatenation

For simple concatenation where numeric conversion is implicit:

```sql
-- Oracle
WHERE TO_CHAR(fiscal_year) = '2024'

-- PostgreSQL (using concatenation)
WHERE fiscal_year::TEXT = '2024'
-- or
WHERE CAST(fiscal_year AS TEXT) = '2024'
```

## Migration Checklist

When migrating SQL containing `TO_CHAR()`:

1. **Identify all TO_CHAR() calls**: Search for `TO_CHAR\(` in SQL strings, stored procedures, and application queries
2. **Check the argument type**:
   - **DATE/TIMESTAMP**: Keep `TO_CHAR()` with format string (e.g., `TO_CHAR(date_col, 'YYYY-MM-DD')`)
   - **NUMERIC/INTEGER**: Replace with `CAST(... AS TEXT)` or add format string
3. **Test the output**: Verify that the string representation matches expectations (no unexpected spaces, decimals, etc.)
4. **Update comparison logic**: If comparing numeric-to-string, ensure consistent types on both sides

## Application Code Review

### C# Example

```csharp
// Before (Oracle)
var sql = "SELECT TO_CHAR(id) AS id_text FROM entities WHERE TO_CHAR(status) = @status";

// After (PostgreSQL)
var sql = "SELECT CAST(id AS TEXT) AS id_text FROM entities WHERE CAST(status AS TEXT) = @status";
```

## Testing Recommendations

1. **Unit Tests**: Verify numeric-to-string conversions return expected values

   ```csharp
   [Fact]
   public void GetVesselNumbers_ReturnsVesselIdsAsStrings()
   {
       var results = dal.GetVesselNumbers(certificateType);
       Assert.All(results, item => Assert.True(int.TryParse(item.DISPLAY_MEMBER, out _)));
   }
   ```

2. **Integration Tests**: Ensure queries with `CAST()` execute without errors
3. **Comparison Tests**: Verify WHERE clauses with numeric-to-string comparisons filter correctly

## Common Locations

Search for `TO_CHAR` in:

- ✅ Stored procedures and functions (DDL scripts)
- ✅ Application data access layers (DAL classes)
- ✅ Dynamic SQL builders
- ✅ Reporting queries
- ✅ ORM/Entity Framework raw SQL

## Error Messages to Watch For

```
Npgsql.PostgresException: 42883: function to_char(numeric) does not exist
Npgsql.PostgresException: 42883: function to_char(integer) does not exist
Npgsql.PostgresException: 42883: function to_char(bigint) does not exist
```

## See Also

- [oracle-to-postgres-type-coercion.md](oracle-to-postgres-type-coercion.md) - Related type conversion issues
- PostgreSQL Documentation: [Data Type Formatting Functions](https://www.postgresql.org/docs/current/functions-formatting.html)
