fix: use double-quoted identifiers for NM/NN columns to preserve PostgreSQL case sensitivity

This commit is contained in:
Dwindi Ramadhana
2026-03-22 09:23:51 +07:00
parent 6255e22ec9
commit ec9988b185
2 changed files with 123 additions and 4 deletions

121
SQLALCHEMY_QUOTING_FIX.md Normal file
View File

@@ -0,0 +1,121 @@
# SQLAlchemy Column Name Quoting Fix for PostgreSQL
**Date:** March 22, 2026
**Issue:** PostgreSQL case sensitivity with `mapped_column` parameter
---
## Problem
SQLAlchemy's `name=` parameter generates unquoted column names by default, which PostgreSQL lowercases. When column definitions use uppercase CHECK constraints, they don't match the lowercased column names.
**Example:**
```python
# Model definition
NM: Mapped[Union[int, None]] = mapped_column(
Integer,
name="NM", # Generates: CREATE TABLE ... ( "NM" INTEGER, ...)
nullable=True,
)
# SQL generated
CREATE TABLE sessions (
"NM" INTEGER, -- PostgreSQL lowercases to "nm"
...
CONSTRAINT ck_nm_range CHECK (NM IS NULL OR ...) -- References "NM" (uppercase)
)
```
**Error:** `column "nm" does not exist` (PostgreSQL can't find the uppercase constraint reference)
---
## Solution
Use **double-quoted identifiers** to force PostgreSQL to preserve case:
```python
# CORRECT - Preserves case
NM: Mapped[Union[int, None]] = mapped_column(
Integer,
name='"NM"', # Generates: CREATE TABLE ... ("NM" INTEGER, ...)
nullable=True,
)
```
This generates:
```sql
CREATE TABLE sessions (
"NM" INTEGER, -- Preserves "NM" in SQL
...
CONSTRAINT ck_nm_range CHECK ("NM" IS NULL OR ...) -- Matches quoted name
)
```
---
## Applied Fixes
### File: app/models/session.py
**Lines 108-119**: Fixed NM and NN column definitions to use `name='"NM"'` and `name='"NN"'`
**Before:**
```python
NM: Mapped[Union[int, None]] = mapped_column(
Integer,
name="NM", # ❌ Gets lowercased
nullable=True,
comment="Nilai Mentah (raw score) [0, 1000]",
)
```
**After:**
```python
NM: Mapped[Union[int, None]] = mapped_column(
Integer,
name='"NM"', # ✅ Preserves case in PostgreSQL
nullable=True,
comment="Nilai Mentah (raw score) [0, 1000]",
)
```
---
## Why This Works
1. **Double quotes in Python string** → SQL receives `"NM"` as literal
2. **SQL parser preserves literal inside double quotes** → Column name stays `"NM"` (uppercase)
3. **CHECK constraints match** → Both column and constraint use `"NM"`
---
## Notes
- This fix is **PostgreSQL-specific** - Other databases may handle this differently
- For **MySQL**: Use backticks: ``NM`` instead of quotes
- Best practice: Use lowercase identifiers with underscores: `nm_score`, `nn_score`
- This issue affects **all uppercase column names** in CHECK constraints
---
## Testing
After applying this fix, restart the application and verify:
```bash
cd /www/wwwroot/irt-bank-soal
git pull
source venv/bin/activate
pip install -r requirements.txt
pm2 restart irt-bank-soal
pm2 logs irt-bank-soal --lines 20
```
Expected: Application starts successfully without `"column nm does not exist"` error.
---
## Related Documentation
- [PostgreSQL Identifier Syntax](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-IDENTIFIERS)
- [SQLAlchemy Identifier Quoting](https://docs.sqlalchemy.org/en/20/core/metadata.html#sqlalchemy.schema.sequence.Sequence)