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 CSV | Tipe PostgreSQL | Alasan |
|---|---|---|
order_id | VARCHAR(20) UNIQUE | kunci dedup lintas berkas; panjang dibatasi agar typo panjang tertolak |
tanggal | DATE | mendukung rentang dan perbandingan tanggal asli |
pelanggan | VARCHAR(100) | teks bebas, dibersihkan kapitalisasinya di Bagian 4 |
email | VARCHAR(120) | cukup panjang untuk email valid terpanjang |
produk | VARCHAR(100) | nama produk dari katalog TokoKita |
kategori | VARCHAR(50) | nilainya terbatas; daftar kanonik dijaga di src/clean.py |
jumlah | INTEGER CHECK (jumlah > 0) | jumlah nol dan negatif ditolak di tingkat tabel |
harga | BIGINT CHECK (harga >= 0) | rupiah tanpa desimal; BIGINT untuk nilai jutaan |
status | VARCHAR(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:
-- 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:
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.order_id ... NOT NULL UNIQUE: database menolak dua baris denganorder_idsama. Inilah lapis ketiga setelah dedup Python, dan lapis ini tidak bisa dilewati bug Python.CHECK (jumlah > 0)danCHECK (harga >= 0): nilai mustahil dari audit Bagian 1 ditolak meskipun lolos seluruh proses Python.CONSTRAINT status_valid CHECK (status IN (...)): daftar status diberi nama constraint, sehingga pesan galatnya jelas sumbernya saat proses pemuatan mencoba menyisipkan status asing.idx_pesanan_tanggaldanidx_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:
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:
| Aspek | PostgreSQL | SQLite |
|---|---|---|
| Kunci internal | SERIAL (sequence) | INTEGER PRIMARY KEY AUTOINCREMENT |
| Waktu pemuatan | TIMESTAMPTZ DEFAULT now() | tidak ada; tidak dipakai pipeline |
| Panjang teks | VARCHAR(n) membatasi | TEXT tanpa batas |
| UPSERT | ON CONFLICT ... DO UPDATE | sama 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:
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: INFOYang kedua adalah contoh parameter koneksi PostgreSQL. Berkas aslinya (database.yaml) tidak masuk repo karena menyimpan kredensial; pola ini dibahas di Pengelolaan Secrets:
host: localhost
port: 5432
dbname: tokokita
user: tokokita
password: YOUR_DATABASE_PASSWORD
connect_timeout: 5Memastikan 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.