From ec9988b185688356dad7315dd3c9172721a89013 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Sun, 22 Mar 2026 09:23:51 +0700 Subject: [PATCH] fix: use double-quoted identifiers for NM/NN columns to preserve PostgreSQL case sensitivity --- SQLALCHEMY_QUOTING_FIX.md | 121 ++++++++++++++++++++++++++++++++++++++ app/models/session.py | 6 +- 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 SQLALCHEMY_QUOTING_FIX.md diff --git a/SQLALCHEMY_QUOTING_FIX.md b/SQLALCHEMY_QUOTING_FIX.md new file mode 100644 index 0000000..76be7c3 --- /dev/null +++ b/SQLALCHEMY_QUOTING_FIX.md @@ -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) diff --git a/app/models/session.py b/app/models/session.py index a3217d3..b72c6a6 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -106,14 +106,12 @@ class Session(Base): Float, nullable=False, default=0.0, comment="Total weight earned" ) NM: Mapped[Union[int, None]] = mapped_column( - Integer, - name="NM", + name='"NM"', nullable=True, comment="Nilai Mentah (raw score) [0, 1000]", ) NN: Mapped[Union[int, None]] = mapped_column( - Integer, - name="NN", + name='"NN"', nullable=True, comment="Nilai Nasional (normalized score) [0, 1000]", )