Keputusan termahal pada database penjualan tidak diambil saat menulis laporan, melainkan saat merancang tabelnya. Salah menempatkan harga jual hanya di tabel produk, dan seluruh pesanan lama akan ikut berubah nilai setiap kali kasir mengoreksi harga — laporan bulan lalu tidak lagi bisa dipercaya. Pada bagian ini kita menerjemahkan cerita kebutuhan Toko Maju Jaya menjadi lima tabel: katalog produk dengan dua harga, data pelanggan, dan pasangan kepala-detail pesanan. Setiap aturan bisnis ditanam sebagai constraint, lalu kita buktikan satu per satu bahwa PostgreSQL benar-benar menegakkannya.
Dari Kebutuhan ke Lima Tabel #
Pemilik toko menceritakan kebutuhannya dengan sederhana: “saya jual barang, pelanggan memesan, saya catat siapa beli apa dan kapan, dan tiap akhir bulan saya ingin tahu yang laris dan yang untung.” Kalimat itu mengandung tiga entitas data — produk, pelanggan, dan transaksi — plus satu kebutuhan tersembunyi yang menentukan seluruh desain: harga produk berubah seiring waktu, tetapi nilai pesanan yang sudah lewat tidak boleh ikut berubah.
Normalisasi memecah masalah itu menjadi lima tabel. kategori dan produk adalah data master: apa yang dijual dan bagaimana dikelompokkan. pelanggan mencatat siapa yang membeli. Kebutuhan “siapa beli apa dan kapan” tidak muat dalam satu tabel: satu pesanan memuat beberapa produk sekaligus, dan kolom berulang semacam produk1, produk2, produk3 melanggar bentuk normal pertama sekaligus membatasi jumlah item per pesanan. Solusinya adalah pemisahan kepala-detail yang sudah dikenal dari chapter Relasi One-to-Many
: tabel pesanan menyimpan informasi yang tunggal per transaksi (pelanggan, tanggal, status), sedangkan pesanan_item menyimpan baris-baris produk beserta qty-nya.
Dua kolom pada pesanan_item menjadi kunci desain proyek ini. Pertama, harga_saat_transaksi — salinan harga jual pada saat pesanan dibuat. Laporan margin di Bagian 4 memakai kolom ini, bukan harga_jual terkini, sehingga pesanan promo tiga bulan lalu tetap dihitung dengan harga promonya. Kedua, subtotal — kolom generated yang dihitung PostgreSQL sendiri dari qty * harga_saat_transaksi; aplikasi tidak bisa menuliskan nilai yang bertentangan dengan hitungannya.
Membuat Tabel #
-- buat-tabel.sql — struktur database penjualan ritel Toko Maju Jaya.
-- Lima tabel: kategori, produk, pelanggan, pesanan, dan pesanan_item.
CREATE TABLE kategori (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE produk (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
nama text NOT NULL,
kategori_id integer NOT NULL REFERENCES kategori (id),
harga_beli numeric(12, 2) NOT NULL CHECK (harga_beli > 0),
harga_jual numeric(12, 2) NOT NULL CHECK (harga_jual > 0),
stok integer NOT NULL CHECK (stok >= 0)
);
CREATE TABLE pelanggan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
kota text NOT NULL
);
CREATE TABLE pesanan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
tanggal date NOT NULL,
status text NOT NULL DEFAULT 'baru'
CHECK (status IN ('baru', 'diproses', 'selesai', 'batal'))
);
CREATE TABLE pesanan_item (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pesanan_id integer NOT NULL REFERENCES pesanan (id) ON DELETE CASCADE,
produk_id integer NOT NULL REFERENCES produk (id),
qty integer NOT NULL CHECK (qty > 0),
harga_saat_transaksi numeric(12, 2) NOT NULL,
subtotal numeric(12, 2) GENERATED ALWAYS AS (qty * harga_saat_transaksi) STORED
);
CREATE INDEX idx_pesanan_tanggal ON pesanan (tanggal);
CREATE INDEX idx_pesanan_pelanggan ON pesanan (pelanggan_id);
CREATE INDEX idx_pesanan_item_pesanan ON pesanan_item (pesanan_id);
CREATE INDEX idx_pesanan_item_produk ON pesanan_item (produk_id);Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEXMari kita bedah keputusan-keputusan pentingnya:
numeric(12, 2)untuk semua kolom uang: harga beli, harga jual, harga saat transaksi, dan subtotal menyimpan rupiah dengan dua desimal. Tipenumericdihitung secara eksak, tidak sepertifloatyang bisa kehilangan beberapa rupiah akibat pembulatan biner — selisih kecil yang menumpuk di laporan bulanan.CHECK (harga_beli > 0)danCHECK (harga_jual > 0): produk gratis tidak ada dalam bisnis ini. Nilai nol dan negatif ditolak sebelum sempat tersimpan, sehingga laporan margin tidak pernah berhadapan dengan pembagian harga nol.CHECK (stok >= 0)padaproduk: penjaga anti-overselling proyek ini. Stok negatif berarti menjual barang yang tidak pernah ada — dan karena pengurangan stok pesanan berlangsung lewatUPDATE, constraint inilah yang akan menggagalkan seluruh pesanan yang menuntut terlalu banyak, seperti dibuktikan di Bagian 3.status IN ('baru', 'diproses', 'selesai', 'batal')denganDEFAULT 'baru': daftar status tertutup. Aplikasi tidak bisa menyisipkan status “menguap” atau “Selesai” (dengan huruf besar) — transisi status hanya boleh berjalan pada empat nilai itu, dan pesanan baru selalu lahir sebagaibarutanpa perlu dituliskan.harga_saat_transaksitanpa CHECK,subtotalgenerated: snapshot harga boleh bernilai berapa pun yang sah saat transaksi — termasuk harga promo yang lebih rendah dari harga jual biasa — jadi kolom ini sengaja tidak dibatasi. Sebaliknyasubtotaltidak pernah diisi manual:GENERATED ALWAYS AS (qty * harga_saat_transaksi) STOREDmembuat PostgreSQL menghitungnya dan menolak upaya penulisan langsung.ON DELETE CASCADEhanya padapesanan_item → pesanan: menghapus kepala pesanan ikut membawa detailnya, karena item tanpa kepala tidak bermakna. Relasipesanan_item → produksengaja memakai perilaku bawaan yang melarang penghapusan produk yang sudah pernah dijual — riwayat penjualan adalah dokumen keuangan.- Empat index di akhir berkas: laporan menyaring pesanan berdasarkan
tanggaldan mengelompokkannya perpelanggan, sedangkan join laporan hampir selalu lewatpesanan_idatauproduk_idpada tabel detail.
Data Master Toko #
Skema kosong belum bisa diuji, jadi kita isi data master yang akan menemani seluruh proyek: sepuluh produk dalam empat kategori (Sembako, Minuman, Makanan, Kebersihan) dengan rentang harga dan stok yang sengaja beragam, serta enam pelanggan dari tiga kota. Harga beli dan harga jual berpasangan di setiap baris — selisih keduanya adalah bibit margin yang dihitung di Bagian 4.
-- data-awal.sql — data master toko: kategori, produk, dan pelanggan.
INSERT INTO kategori (nama) VALUES
('Sembako'),
('Minuman'),
('Makanan'),
('Kebersihan');
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok) VALUES
('PRD-001', 'Beras Premium 5kg', 1, 61000.00, 68000.00, 40),
('PRD-002', 'Gula Pasir 1kg', 1, 15000.00, 17000.00, 60),
('PRD-003', 'Minyak Goreng 2L', 1, 32000.00, 36500.00, 35),
('PRD-004', 'Telur Ayam 1kg', 1, 27000.00, 31000.00, 25),
('PRD-005', 'Kopi Sachet Instan', 2, 9000.00, 12000.00, 80),
('PRD-006', 'Teh Celup 50 kantong', 2, 11000.00, 13500.00, 45),
('PRD-007', 'Air Mineral 600ml', 2, 2500.00, 4000.00, 150),
('PRD-008', 'Mie Instan Goreng', 3, 2800.00, 3500.00, 100),
('PRD-009', 'Biskuit Cokelat', 3, 7500.00, 9500.00, 30),
('PRD-010', 'Sabun Cuci Piring 780ml', 4, 16000.00, 19500.00, 22);
INSERT INTO pelanggan (nama, email, kota) VALUES
('Budi Santoso', '[email protected]', 'Bandung'),
('Siti Rahayu', '[email protected]', 'Bandung'),
('Andi Wijaya', '[email protected]', 'Jakarta'),
('Dewi Lestari', '[email protected]', 'Surabaya'),
('Rudi Hartono', '[email protected]', 'Bandung'),
('Maya Putri', '[email protected]', 'Jakarta');
SELECT p.id, p.kode, p.nama, k.nama AS kategori,
p.harga_beli, p.harga_jual, p.stok
FROM produk p
JOIN kategori k ON k.id = p.kategori_id
ORDER BY p.id;
SELECT id, nama, email, kota
FROM pelanggan
ORDER BY id;Output yang Dihasilkan:
INSERT 0 4
INSERT 0 10
INSERT 0 6
id | kode | nama | kategori | harga_beli | harga_jual | stok
----+---------+-------------------------+------------+------------+------------+------
1 | PRD-001 | Beras Premium 5kg | Sembako | 61000.00 | 68000.00 | 40
2 | PRD-002 | Gula Pasir 1kg | Sembako | 15000.00 | 17000.00 | 60
3 | PRD-003 | Minyak Goreng 2L | Sembako | 32000.00 | 36500.00 | 35
4 | PRD-004 | Telur Ayam 1kg | Sembako | 27000.00 | 31000.00 | 25
5 | PRD-005 | Kopi Sachet Instan | Minuman | 9000.00 | 12000.00 | 80
6 | PRD-006 | Teh Celup 50 kantong | Minuman | 11000.00 | 13500.00 | 45
7 | PRD-007 | Air Mineral 600ml | Minuman | 2500.00 | 4000.00 | 150
8 | PRD-008 | Mie Instan Goreng | Makanan | 2800.00 | 3500.00 | 100
9 | PRD-009 | Biskuit Cokelat | Makanan | 7500.00 | 9500.00 | 30
10 | PRD-010 | Sabun Cuci Piring 780ml | Kebersihan | 16000.00 | 19500.00 | 22
(10 rows)
id | nama | email | kota
----+--------------+------------------+----------
1 | Budi Santoso | [email protected] | Bandung
2 | Siti Rahayu | [email protected] | Bandung
3 | Andi Wijaya | [email protected] | Jakarta
4 | Dewi Lestari | [email protected] | Surabaya
5 | Rudi Hartono | [email protected] | Bandung
6 | Maya Putri | [email protected] | Jakarta
(6 rows)Dua hal patut dicermati dari data ini. Pertama, kode produk seperti PRD-005 adalah identitas yang dibaca manusia pada label rak dan struk — berbeda dari id yang dikelola PostgreSQL, kode ini stabil dan mudah diketik kasir. Kedua, rentang margin sengaja lebar: Air Mineral dijual 4000 dari modal 2500 (margin 37,5 persen), sedangkan Beras Premium hanya 7000 dari modal 61000 (sekitar 10 persen). Keragaman ini akan menghasilkan laporan margin yang benar-benar membedakan produk di Bagian 4, bukan angka yang rata semua.
Menguji Constraint Sebelum Lanjut #
Skema yang belum diuji adalah skema yang belum selesai. Berkas berikut mencoba enam pelanggaran secara sengaja di dalam blok EXCEPTION, menangkap SQLSTATE yang dikembalikan PostgreSQL, lalu membandingkannya dengan yang diharapkan — pola yang sama dengan chapter Pengujian Integritas Data
.
-- uji-constraint.sql — bukti bahwa constraint bisnis benar-benar ditegakkan.
-- Setiap pelanggaran ditangkap dalam blok EXCEPTION, lalu dibandingkan
-- dengan SQLSTATE yang diharapkan.
CREATE TEMP TABLE hasil_uji (
nomor integer,
kasus text,
diharapkan text,
aktual text,
status text
);
DO $$
DECLARE
v_state text;
BEGIN
-- Kasus 1: email pelanggan duplikat (UNIQUE)
BEGIN
INSERT INTO pelanggan (nama, email, kota)
VALUES ('Budi Kedua', '[email protected]', 'Bandung');
INSERT INTO hasil_uji VALUES (1, 'email pelanggan duplikat', '23505', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (1, 'email pelanggan duplikat', '23505', v_state,
CASE WHEN v_state = '23505' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 2: kode produk duplikat (UNIQUE)
BEGIN
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok)
VALUES ('PRD-001', 'Beras tiruan', 1, 61000.00, 68000.00, 5);
INSERT INTO hasil_uji VALUES (2, 'kode produk duplikat', '23505', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (2, 'kode produk duplikat', '23505', v_state,
CASE WHEN v_state = '23505' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 3: harga jual nol (CHECK)
BEGIN
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok)
VALUES ('PRD-100', 'Produk gratis', 3, 5000.00, 0, 5);
INSERT INTO hasil_uji VALUES (3, 'harga jual nol', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (3, 'harga jual nol', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 4: stok produk negatif (CHECK — penjaga anti overselling)
BEGIN
UPDATE produk SET stok = -10 WHERE kode = 'PRD-008';
INSERT INTO hasil_uji VALUES (4, 'stok produk negatif', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (4, 'stok produk negatif', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 5: status pesanan tidak dikenal (CHECK)
BEGIN
INSERT INTO pesanan (pelanggan_id, tanggal, status)
VALUES (1, DATE '2026-04-01', 'menguap');
INSERT INTO hasil_uji VALUES (5, 'status pesanan tidak dikenal', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (5, 'status pesanan tidak dikenal', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 6: pesanan merujuk pelanggan yang tidak ada (FOREIGN KEY)
BEGIN
INSERT INTO pesanan (pelanggan_id, tanggal)
VALUES (999, DATE '2026-04-01');
INSERT INTO hasil_uji VALUES (6, 'pesanan merujuk pelanggan 999', '23503', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (6, 'pesanan merujuk pelanggan 999', '23503', v_state,
CASE WHEN v_state = '23503' THEN 'LULUS' ELSE 'GAGAL' END);
END;
END $$;
SELECT nomor, kasus, diharapkan, aktual, status
FROM hasil_uji
ORDER BY nomor;Output yang Dihasilkan:
CREATE TABLE
DO
nomor | kasus | diharapkan | aktual | status
-------+-------------------------------+------------+--------+--------
1 | email pelanggan duplikat | 23505 | 23505 | LULUS
2 | kode produk duplikat | 23505 | 23505 | LULUS
3 | harga jual nol | 23514 | 23514 | LULUS
4 | stok produk negatif | 23514 | 23514 | LULUS
5 | status pesanan tidak dikenal | 23514 | 23514 | LULUS
6 | pesanan merujuk pelanggan 999 | 23503 | 23503 | LULUS
(6 rows)Enam kasus, enam vonis LULUS: email dan kode duplikat ditolak UNIQUE (SQLSTATE 23505), harga nol, stok negatif, dan status asing ditolak CHECK (23514), sedangkan pesanan untuk pelanggan yang tidak ada ditolak foreign key (23503). Kasus 4 layak digarisbawahi — penolakan terjadi pada UPDATE, bukan INSERT, yang berarti penjaga stok >= 0 aktif pada setiap jalur yang mencoba mengubah stok, termasuk jalur pengurangan stok saat pesanan diproses di Bagian 3.
Struktur database kini berdiri dan teruji. Katalog dan pelanggan siap diisi kegiatan nyata: produk baru masuk, harga dikoreksi, pelanggan mendaftar, dan sistem pusat mengirim pembaruan katalog. Bagian Pengelolaan Produk dan Pelanggan menghidupkan data master itu sehari penuh di kehidupan toko.
Referensi #
- CREATE TABLE — sintaks lengkap pembuatan tabel, termasuk kolom generated.
- Constraints pada Tabel
— spesifikasi CHECK, UNIQUE, FOREIGN KEY, dan pilihan
ON DELETE. - Generated Columns — cara kerja kolom yang dihitung PostgreSQL secara otomatis.
- Tipe Numeric
— alasan
numeric(12, 2)dipilih untuk kolom uang.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- buat-tabel.sql — struktur database penjualan ritel Toko Maju Jaya.
-- Lima tabel: kategori, produk, pelanggan, pesanan, dan pesanan_item.
CREATE TABLE kategori (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE produk (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
nama text NOT NULL,
kategori_id integer NOT NULL REFERENCES kategori (id),
harga_beli numeric(12, 2) NOT NULL CHECK (harga_beli > 0),
harga_jual numeric(12, 2) NOT NULL CHECK (harga_jual > 0),
stok integer NOT NULL CHECK (stok >= 0)
);
CREATE TABLE pelanggan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
kota text NOT NULL
);
CREATE TABLE pesanan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
tanggal date NOT NULL,
status text NOT NULL DEFAULT 'baru'
CHECK (status IN ('baru', 'diproses', 'selesai', 'batal'))
);
CREATE TABLE pesanan_item (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pesanan_id integer NOT NULL REFERENCES pesanan (id) ON DELETE CASCADE,
produk_id integer NOT NULL REFERENCES produk (id),
qty integer NOT NULL CHECK (qty > 0),
harga_saat_transaksi numeric(12, 2) NOT NULL,
subtotal numeric(12, 2) GENERATED ALWAYS AS (qty * harga_saat_transaksi) STORED
);
CREATE INDEX idx_pesanan_tanggal ON pesanan (tanggal);
CREATE INDEX idx_pesanan_pelanggan ON pesanan (pelanggan_id);
CREATE INDEX idx_pesanan_item_pesanan ON pesanan_item (pesanan_id);
CREATE INDEX idx_pesanan_item_produk ON pesanan_item (produk_id);
data-awal.sql
Unduh berkas-- data-awal.sql — data master toko: kategori, produk, dan pelanggan.
INSERT INTO kategori (nama) VALUES
('Sembako'),
('Minuman'),
('Makanan'),
('Kebersihan');
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok) VALUES
('PRD-001', 'Beras Premium 5kg', 1, 61000.00, 68000.00, 40),
('PRD-002', 'Gula Pasir 1kg', 1, 15000.00, 17000.00, 60),
('PRD-003', 'Minyak Goreng 2L', 1, 32000.00, 36500.00, 35),
('PRD-004', 'Telur Ayam 1kg', 1, 27000.00, 31000.00, 25),
('PRD-005', 'Kopi Sachet Instan', 2, 9000.00, 12000.00, 80),
('PRD-006', 'Teh Celup 50 kantong', 2, 11000.00, 13500.00, 45),
('PRD-007', 'Air Mineral 600ml', 2, 2500.00, 4000.00, 150),
('PRD-008', 'Mie Instan Goreng', 3, 2800.00, 3500.00, 100),
('PRD-009', 'Biskuit Cokelat', 3, 7500.00, 9500.00, 30),
('PRD-010', 'Sabun Cuci Piring 780ml', 4, 16000.00, 19500.00, 22);
INSERT INTO pelanggan (nama, email, kota) VALUES
('Budi Santoso', '[email protected]', 'Bandung'),
('Siti Rahayu', '[email protected]', 'Bandung'),
('Andi Wijaya', '[email protected]', 'Jakarta'),
('Dewi Lestari', '[email protected]', 'Surabaya'),
('Rudi Hartono', '[email protected]', 'Bandung'),
('Maya Putri', '[email protected]', 'Jakarta');
SELECT p.id, p.kode, p.nama, k.nama AS kategori,
p.harga_beli, p.harga_jual, p.stok
FROM produk p
JOIN kategori k ON k.id = p.kategori_id
ORDER BY p.id;
SELECT id, nama, email, kota
FROM pelanggan
ORDER BY id;
uji-constraint.sql
Unduh berkas-- uji-constraint.sql — bukti bahwa constraint bisnis benar-benar ditegakkan.
-- Setiap pelanggaran ditangkap dalam blok EXCEPTION, lalu dibandingkan
-- dengan SQLSTATE yang diharapkan.
CREATE TEMP TABLE hasil_uji (
nomor integer,
kasus text,
diharapkan text,
aktual text,
status text
);
DO $$
DECLARE
v_state text;
BEGIN
-- Kasus 1: email pelanggan duplikat (UNIQUE)
BEGIN
INSERT INTO pelanggan (nama, email, kota)
VALUES ('Budi Kedua', '[email protected]', 'Bandung');
INSERT INTO hasil_uji VALUES (1, 'email pelanggan duplikat', '23505', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (1, 'email pelanggan duplikat', '23505', v_state,
CASE WHEN v_state = '23505' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 2: kode produk duplikat (UNIQUE)
BEGIN
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok)
VALUES ('PRD-001', 'Beras tiruan', 1, 61000.00, 68000.00, 5);
INSERT INTO hasil_uji VALUES (2, 'kode produk duplikat', '23505', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (2, 'kode produk duplikat', '23505', v_state,
CASE WHEN v_state = '23505' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 3: harga jual nol (CHECK)
BEGIN
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok)
VALUES ('PRD-100', 'Produk gratis', 3, 5000.00, 0, 5);
INSERT INTO hasil_uji VALUES (3, 'harga jual nol', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (3, 'harga jual nol', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 4: stok produk negatif (CHECK — penjaga anti overselling)
BEGIN
UPDATE produk SET stok = -10 WHERE kode = 'PRD-008';
INSERT INTO hasil_uji VALUES (4, 'stok produk negatif', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (4, 'stok produk negatif', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 5: status pesanan tidak dikenal (CHECK)
BEGIN
INSERT INTO pesanan (pelanggan_id, tanggal, status)
VALUES (1, DATE '2026-04-01', 'menguap');
INSERT INTO hasil_uji VALUES (5, 'status pesanan tidak dikenal', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (5, 'status pesanan tidak dikenal', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 6: pesanan merujuk pelanggan yang tidak ada (FOREIGN KEY)
BEGIN
INSERT INTO pesanan (pelanggan_id, tanggal)
VALUES (999, DATE '2026-04-01');
INSERT INTO hasil_uji VALUES (6, 'pesanan merujuk pelanggan 999', '23503', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (6, 'pesanan merujuk pelanggan 999', '23503', v_state,
CASE WHEN v_state = '23503' THEN 'LULUS' ELSE 'GAGAL' END);
END;
END $$;
SELECT nomor, kasus, diharapkan, aktual, status
FROM hasil_uji
ORDER BY nomor;