elemes/docs/11-database-migration.md

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.pyusers, 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)

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_dumpbackups/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 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.