Skema database adalah kontrak pertama proyek ini: sebelum satu baris CSV diproses, kita harus tahu bentuk tabel yang menampungnya. Kontrak tersebut tidak boleh diganti-ganti di tengah jalan, karena modul validasi, pembersihan, dan pemuatan semuanya menunjuk ke bentuk yang sama. Pada bagian ini kita menurunkan sembilan kolom CSV menjadi tabel pesanan, memilih tipe data dan constraint, lalu menyiapkan berkas konfigurasi.

Dari Kolom CSV ke Kolom Tabel #

CSV tidak punya tipe data: semua nilai adalah teks. Tabel database sebaliknya, dan pemilihan tipe adalah keputusan kualitas data:

Kolom CSVTipe PostgreSQLAlasan
order_idVARCHAR(20) UNIQUEkunci dedup lintas berkas; panjang dibatasi agar typo panjang tertolak
tanggalDATEmendukung rentang dan perbandingan tanggal asli
pelangganVARCHAR(100)teks bebas, dibersihkan kapitalisasinya di Bagian 4
emailVARCHAR(120)cukup panjang untuk email valid terpanjang
produkVARCHAR(100)nama produk dari katalog TokoKita
kategoriVARCHAR(50)nilainya terbatas; daftar kanonik dijaga di src/clean.py
jumlahINTEGER CHECK (jumlah > 0)jumlah nol dan negatif ditolak di tingkat tabel
hargaBIGINT CHECK (harga >= 0)rupiah tanpa desimal; BIGINT untuk nilai jutaan
statusVARCHAR(20) CHECK (...)daftar empat nilai tetap

Dua kolom tambahan dibuat khusus untuk tabel: id SERIAL sebagai primary key internal dan dibuat_pada TIMESTAMPTZ sebagai waktu pemuatan. Kita tidak memakai order_id sebagai primary key karena SERIAL memberi kunci internal yang stabil; order_id tetap unik lewat constraint UNIQUE.

DDL PostgreSQL #

Berkas config/schema.sql berisi definisi lengkapnya:

sql
-- Skema tabel pesanan untuk PostgreSQL.
-- Jalankan sekali sebelum memuat data, misalnya:
--   psql -d tokokita -f config/schema.sql

CREATE TABLE IF NOT EXISTS pesanan (
    id          SERIAL       PRIMARY KEY,
    order_id    VARCHAR(20)  NOT NULL UNIQUE,
    tanggal     DATE         NOT NULL,
    pelanggan   VARCHAR(100) NOT NULL,
    email       VARCHAR(120) NOT NULL,
    produk      VARCHAR(100) NOT NULL,
    kategori    VARCHAR(50)  NOT NULL,
    jumlah      INTEGER      NOT NULL CHECK (jumlah > 0),
    harga       BIGINT       NOT NULL CHECK (harga >= 0),
    status      VARCHAR(20)  NOT NULL,
    dibuat_pada TIMESTAMPTZ  NOT NULL DEFAULT now(),
    CONSTRAINT status_valid CHECK (status IN ('Selesai', 'Diproses', 'Dikirim', 'Dibatalkan'))
);

CREATE INDEX IF NOT EXISTS idx_pesanan_tanggal  ON pesanan (tanggal);
CREATE INDEX IF NOT EXISTS idx_pesanan_kategori ON pesanan (kategori);

Mari kita telaah bagian-bagian pentingnya:

  1. CREATE TABLE IF NOT EXISTS: skema dapat dijalankan berulang tanpa galat; sesi kedua melihat tabel sudah ada dan melewatinya. Ini selaras dengan kebutuhan idempotent di Bagian 1.
  2. order_id ... NOT NULL UNIQUE: database menolak dua baris dengan order_id sama. Inilah lapis ketiga setelah dedup Python, dan lapis ini tidak bisa dilewati bug Python.
  3. CHECK (jumlah > 0) dan CHECK (harga >= 0): nilai mustahil dari audit Bagian 1 ditolak meskipun lolos seluruh proses Python.
  4. CONSTRAINT status_valid CHECK (status IN (...)): daftar status diberi nama constraint, sehingga pesan galatnya jelas sumbernya saat proses pemuatan mencoba menyisipkan status asing.
  5. idx_pesanan_tanggal dan idx_pesanan_kategori: dua pola query yang paling sering dipakai tim analitik adalah filter rentang tanggal dan kelompok kategori; keduanya diberi index di awal, bukan setelah query terasa lambat.

Pembahasan lebih dalam tentang pilihan tipe dan index ada pada chapter Primary Key dan Foreign Key serta Indexing.

Kembaran SQLite #

Lingkungan tutorial tidak punya server PostgreSQL, jadi modul src/load.py menyimpan skema kembaran dialek SQLite yang dijalankan nyata:

python
SKEMA_SQLITE = """
CREATE TABLE IF NOT EXISTS pesanan (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id    TEXT    NOT NULL UNIQUE,
    tanggal     DATE    NOT NULL,
    pelanggan   TEXT    NOT NULL,
    email       TEXT    NOT NULL,
    produk      TEXT    NOT NULL,
    kategori    TEXT    NOT NULL,
    jumlah      INTEGER NOT NULL CHECK (jumlah > 0),
    harga       INTEGER NOT NULL CHECK (harga >= 0),
    status      TEXT    NOT NULL CHECK (status IN ('Selesai', 'Diproses', 'Dikirim', 'Dibatalkan'))
);

CREATE INDEX IF NOT EXISTS idx_pesanan_tanggal  ON pesanan (tanggal);
CREATE INDEX IF NOT EXISTS idx_pesanan_kategori ON pesanan (kategori);
"""

Perbedaan dialeknya disengaja sesedikit mungkin:

AspekPostgreSQLSQLite
Kunci internalSERIAL (sequence)INTEGER PRIMARY KEY AUTOINCREMENT
Waktu pemuatanTIMESTAMPTZ DEFAULT now()tidak ada; tidak dipakai pipeline
Panjang teksVARCHAR(n) membatasiTEXT tanpa batas
UPSERTON CONFLICT ... DO UPDATEsama sejak SQLite 3.24

Kolom dibuat_pada sengaja tidak dibuat kembarannya di SQLite karena pipeline tidak membacanya; kalau suatu saat pipeline pindah penuh ke PostgreSQL, kolom tersebut terisi otomatis oleh DEFAULT now().

Berkas Konfigurasi #

Dua berkas YAML hidup di config/. Yang pertama mengatur perilaku pipeline:

yaml
sumber:
  direktori: data
  pola: "pesanan_*.csv"

keluaran:
  bersih: data/processed/pesanan_bersih.csv
  basisdata: data/processed/tokokita.db
  log: data/processed/pipeline.log

log:
  level: INFO

Yang kedua adalah contoh parameter koneksi PostgreSQL. Berkas aslinya (database.yaml) tidak masuk repo karena menyimpan kredensial; pola ini dibahas di Pengelolaan Secrets:

yaml
host: localhost
port: 5432
dbname: tokokita
user: tokokita
password: YOUR_DATABASE_PASSWORD
connect_timeout: 5

Memastikan Skema Tidak Tertinggal #

config/schema.sql tidak dieksekusi di lingkungan tutorial, tetapi strukturnya tetap dijaga oleh pengujian. Fungsi validasi_skema_postgres di src/load.py memeriksa sebelas ketentuan secara statis: tabel, SERIAL, PRIMARY KEY, order_id UNIQUE, tiga CHECK, dua index, dan TIMESTAMPTZ. Pengujian tests/test_load.py::TestSkemaPostgres membaca berkas asli dan menghapus satu ketentuan untuk membuktikan pemeriksa benar-benar bekerja.

Rujukan teknis untuk bagian ini: SQL CREATE TABLE PostgreSQL dan dokumentasi SQLite .

Skema dan konfigurasi sudah siap; tabel menunggu penghuni. Langkah berikutnya adalah membuka berkas CSV dan memeriksa setiap baris terhadap aturan yang kita tetapkan di Bagian 1. Lanjutkan ke Bagian 3: Membaca dan Memvalidasi CSV.