# Migrasi Database: CSV → PostgreSQL > Status: **SELESAI — live di PostgreSQL** (`STORAGE_BACKEND=postgresql`, cutover Agustus 2026). > CSV tetap dipelihara sebagai backup read-only (`/app/tokens.csv`, mount `:ro`). > Dokumen ini menjelaskan arsitektur, alur migrasi, operasional, dan rollback. ## 1. Kenapa migrasi? Penyimpanan lama `tokens_siswa.csv` (satu baris = satu siswa, satu kolom per lesson): | Masalah | Dampak | |---|---| | `read-modify-write` dengan file lock | Race condition saat banyak siswa submit bersamaan | | Token siswa **plaintext** di file + bocor ke log & payload report | Kebocoran credential | | Tidak ada relasi / constraint | Progress rusak tanpa error | | Satu file membesar seiring lesson & siswa | Semakin lambat dibaca setiap request | | Role guru = "baris pertama" | Fragile & ambigu | PostgreSQL + SQLAlchemy menyelesaikan semuanya: transaksi ACID, relasi, constraint, index, token **hanya disimpan sebagai HMAC-SHA256 digest**. ## 2. Stack | Komponen | Teknologi | Versi (verified) | |---|---|---| | Database | PostgreSQL | 18 (image `postgres:18-alpine`) | | ORM | SQLAlchemy | 2.0.51 | | Migrasi schema | Alembic | 1.19.0 | | Driver | psycopg | 3.3.4 (`psycopg[binary]`) | | Hashing token | HMAC-SHA256 + pepper (`TOKEN_PEPPER`) | — | ## 3. Arsitektur ``` Flask routes (auth, progress, lessons, compile) │ (hanya kenal facade) ▼ services/token_service.py ← facade, kontrak publik │ ▼ services/storage/ (dipilih via STORAGE_BACKEND) ├── csv_backend.py → tokens_siswa.csv (transisi/rollback) └── postgres_backend.py → repositories → SQLAlchemy → PostgreSQL ``` - `services/models.py` — `users`, `access_tokens` (token_hash unik), `lessons` (registry metadata), `student_progress` (unique user+lesson). - `migrations/` — Alembic; `0001_initial_schema.py` (hand-written, deterministik). - `services/csv_importer.py` — import idempotent CSV → PG (`3/4` legacy → `state=scored, score_earned=3, score_total=4`). - `services/lesson_registry.py` — sync daftar lesson dari `home.md` (lesson hilang → `is_active=false`, **bukan dihapus**). ### Keamanan token - Token mentah **tidak pernah** disimpan: `access_tokens.token_hash = HMAC-SHA256(token, TOKEN_PEPPER)` — deterministik untuk lookup, tak reversibel. - Pepper (`TOKEN_PEPPER`) di `.env`, **di luar database**. Hilang = semua token invalid (regenerate & import ulang). - Report & export **tidak menyertakan token mentah**; reset guru memakai `student_id` anonim (`user.id` di PG, sha256(token) di CSV). ## 4. Alur migrasi (production) ```bash cd elemes # 1. Set variabel di ../.env (lihat .env.example): # POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD (strong!) # DATABASE_URL=postgresql+psycopg://:@127.0.0.1:5432/ # TOKEN_PEPPER= # JANGAN hilangkan — hash bergantung padanya # STORAGE_BACKEND=csv # tetap csv selama transisi # 2. Build ulang image (deps baru: SQLAlchemy, alembic, psycopg) + start postgres ./elemes.sh runclearbuild # 3. Buat schema + import data (idempotent — aman dijalankan ulang) ./elemes.sh dbupgrade ./elemes.sh synclessons ./elemes.sh dbimport # lihat laporan ./elemes.sh dbimport --dry-run # simulasi tanpa menulis # 4. PARITY CHECK — wajib lulus sebelum cutover ./elemes.sh dbverify # 5. Cek versi schema & backup pertama ./elemes.sh dbstatus ./elemes.sh dbbackup # 6. Cutover: ubah STORAGE_BACKEND=postgresql di ../.env, lalu ./elemes.sh run # restart service dengan backend baru ``` Setelah cutover, CSV tetap ada di `/app/tokens.csv` sebagai **backup read-only** — tapi tidak lagi dibaca oleh aplikasi. ## 5. Operasional harian | Perintah | Fungsi | |---|---| | `./elemes.sh dbupgrade` | Jalankan migrasi schema (alembic upgrade head) | | `./elemes.sh dbstatus` | Versi schema aktif & head | | `./elemes.sh dbimport` | Impor/refresh data dari CSV (idempotent) | | `./elemes.sh synclessons` | Sinkronisasi daftar lesson dari `home.md` | | `./elemes.sh dbverify` | Parity check CSV ↔ PG | | `./elemes.sh dbbackup` | `pg_dump` → `backups/elemes_.sql` | | `./elemes.sh dbrestore` | Restore backup terbaru | | `./elemes.sh dbexport` | Snapshot PG → `pg_snapshot.csv` (tanpa token) | ## 6. Rollback (kembali ke CSV) Selama transisi, `STORAGE_BACKEND=csv` → aplikasi memakai CSV lagi. CSV tidak berubah oleh operasi PG (write PG tidak menyentuh CSV). Catatan: progress yang masuk lewat PG selama periode postgresql **tidak** otomatis balik ke CSV — jalankan `dbexport` dulu bila perlu. ## 7. Load test endpoint database ```bash cd elemes/load-test python content_parser.py --content-dir ../../content --tokens-file ../../tokens_siswa.csv locust -f locustfile_db.py # set host ke URL Elemes ``` Skenario: login/validate-token, baca lesson, track-progress (siswa), progress-report + export-csv (guru). ## 8. Test ```bash # Unit & kontrak (host): backend aktif default = csv tanpa DATABASE_URL PYTHONPATH=services python -m pytest services/tests -q # Integrasi (butuh DATABASE_URL + postgres hidup): jalankan di container podman exec -w /app -e PYTHONPATH=services lms-dev_elemes_1 \ python -m pytest services/tests -q ``` - Kontrak suite (`test_token_service_contract.py`, `test_auth_routes.py`, `test_progress_routes.py`) dijalankan terhadap **kedua** backend. - Test integrasi (`test_repositories.py`, `test_csv_importer.py`, `test_lesson_registry.py`) otomatis skip bila `DATABASE_URL` tidak diset. - Isolasi antar test (DB bersama): conftest punya fixture `autouse` yang TRUNCATE semua tabel sebelum tiap test; `TOKENS_FILE`/`CONTENT_DIR` di-**PAKSA** ke fixture (jangan `setdefault` — env container membawa path produksi). Integration test dijalankan di DB terpisah (mis. `elemes_test`): `CREATE DATABASE` + `alembic upgrade`. - Script di `/app/scripts` (dbverify/dbexport) butuh `PYTHONPATH=/app` (bukan `services`) agar `from services...` resolve — sudah di-apply di `elemes.sh`.