122 lines
3.0 KiB
Markdown
122 lines
3.0 KiB
Markdown
# 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)
|