5.9 KiB
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/4legacy →state=scored, score_earned=3, score_total=4).services/lesson_registry.py— sync daftar lesson darihome.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_idanonim (user.iddi PG, sha256(token) di CSV).
4. Alur migrasi (production)
cd elemes
# 1. Set variabel di ../.env (lihat .env.example):
# POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD (strong!)
# DATABASE_URL=postgresql+psycopg://<user>:<pass>@127.0.0.1:5432/<db>
# TOKEN_PEPPER=<random 48 hex> # 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_<ts>.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
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
# 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 bilaDATABASE_URLtidak diset. - Isolasi antar test (DB bersama): conftest punya fixture
autouseyang TRUNCATE semua tabel sebelum tiap test;TOKENS_FILE/CONTENT_DIRdi-PAKSA ke fixture (jangansetdefault— env container membawa path produksi). Integration test dijalankan di DB terpisah (mis.elemes_test):CREATE DATABASE+alembic upgrade. - Script di
/app/scripts(dbverify/dbexport) butuhPYTHONPATH=/app(bukanservices) agarfrom services...resolve — sudah di-apply dielemes.sh.