Gudang yang mengandalkan catatan manual hidup dengan dua ketakutan: barang yang “hilang” entah ke mana, dan stok yang menipis tanpa ada yang tahu lebih dulu. Perputaran barangnya pun dua arah sekaligus — masuk dari supplier, keluar ke toko, dan pindah antarlokasi karena penataan ulang — sehingga satu angka “stok gudang” saja tidak pernah cukup menjelaskan keadaan. Pada proyek ini kita membangun database inventaris untuk CV Maju Jaya, distributor barang kebutuhan harian dengan dua gedung penyimpanan, dari skema kosong sampai laporan nilai persediaan dan audit rekonsiliasi. Seperti proyek lain di section ini, batas keberhasilannya bukan “query-nya jalan”, melainkan seluruh aturan bisnis yang ditegakkan oleh PostgreSQL sendiri dan terbukti lewat eksekusi nyata.
Yang Akan Kita Bangun #
Sistem yang dirakit dari lima tabel menghasilkan fitur-fitur berikut:
- Stok per lokasi, bukan stok global: satu barang dapat tersebar di lima rak pada dua gudang, dan sistem selalu tahu jumlah di masing-masing rak.
- Riwayat mutasi lengkap — setiap kenaikan dan penurunan stok lahir dari dokumen mutasi bertipe
masuk,keluar, ataupindah, lengkap dengan tanggal dan petugas. - Pergerakan atomik: penerimaan, pengiriman, dan perpindahan antarlokasi dibungkus transaksi
BEGIN…COMMIT— termasuk bukti nyata bahwa transaksi yang gagal tidak meninggalkan setengah jejak. - Jaring pengaman constraint: stok negatif ditolak
CHECK, barang tanpa lokasi ditolaknum_nonnulls(), kode duplikat ditolakUNIQUE. - Laporan manajerial: posisi stok dibanding ambang minimum, nilai persediaan dalam rupiah per kategori, perputaran lambat beserta modal yang terkunci, dan rekonsiliasi yang menghitung ulang stok dari riwayat mutasi.
Seluruh contoh proyek ini dijalankan terhadap PostgreSQL 17 dengan data yang tetap (tanggal dan angka dipatok) sehingga setiap output dapat Anda bandingkan langsung.
Rancangan Data #
Lima tabel proyek ini dan hubungannya dapat diringkas dalam diagram berikut:
Dua keputusan rancangan menentukan watak seluruh proyek. Pertama, relasi barang–lokasi dimodelkan sebagai junction table stok_lokasi dengan primary key gabungan — inilah pola baku relasi many-to-many yang dibahas chapter Junction Table
. Kedua, pergerakan dicatat sebagai pasangan kepala–detail (mutasi dan mutasi_item) meniru bentuk dokumen fisik gudang: satu surat jalan memuat banyak barang. Stok saat ini diperlakukan sebagai hasil yang selalu bisa dihitung ulang dari riwayat — dan Bagian 4 memang menghitungnya ulang sebagai audit.
Prasyarat #
Proyek ini memadatkan materi dari beberapa section sebelumnya. Pastikan Anda sudah nyaman dengan topik-topik berikut:
- Table Design: Primary Key , Foreign Key , Check Constraint , dan Identity Columns — seluruh tulang punggung skema proyek.
- Relational Database Design: Junction Table dan Relasi Many-to-Many untuk pola stok per lokasi.
- Manipulasi Data: Insert Multiple Rows , Update , ON CONFLICT , dan RETURNING Clause .
- Transactions: BEGIN, COMMIT, dan ROLLBACK — setiap pergerakan stok adalah satu transaksi.
- Agregasi Data: GROUP BY dan FILTER Clause untuk laporan pada Bagian 4.
Tidak ada extension atau alat tambahan yang dibutuhkan — cukup psql dan PostgreSQL 17.
Alur Kerja dan Peta Bagian #
Proyek dikerjakan dalam empat bagian berurutan. Setiap bagian berdiri sendiri: berkas siapkan.sql di awal bagian memasang ulang skema dan data yang dibutuhkan, sehingga Anda dapat melompat ke bagian mana pun dengan database kosong dan tetap memperoleh hasil yang sama dengan halaman ini.
| Bagian | Fokus |
|---|---|
| Bagian 1: Perancangan Database | Lima tabel, constraint bisnis, index, data master, dan uji pelanggaran |
| Bagian 2: Pencatatan Stok | Mutasi masuk dan keluar dalam transaksi; bukti rollback saat stok kurang |
| Bagian 3: Transaksi Barang | Perpindahan antarlokasi dua-sisi yang atomik, termasuk skenario gagalnya |
| Bagian 4: Laporan Persediaan | Stok kini, nilai persediaan, perputaran lambat, dan rekonsiliasi audit |
Berkas Sumber #
Seluruh SQL proyek ini juga tersedia sebagai berkas siap pakai pada bagian Berkas Sumber di bawah halaman ini: folder database/ berisi skema, data master, dan riwayat aktivitas satu kuartal; folder queries/ berisi keempat laporan final; folder tests/ berisi uji constraint dan uji atomisitas yang dapat dijalankan ulang kapan saja. Jalankan berkasnya berurutan dari database/ lalu queries/ atau tests/ untuk memeriksa kondisi database Anda sendiri.
Setelah fondasi dan kebiasaan kerjanya jelas, tidak ada alasan menunda: mulailah dari Bagian 1: Perancangan Database — tempat lima tabel ini pertama kali ditulis dan diuji.
Rancang tabel barang, lokasi, dan mutasi stok dengan constraint bisnis.
Buka → # Bagian 2: Pencatatan StokCatat pergerakan barang masuk-keluar dengan transaksi yang konsisten.
Buka → # Bagian 3: Transaksi BarangProses mutasi barang antarlokasi dengan validasi stok mencukupi.
Buka → # Bagian 4: Laporan PersediaanSusun laporan stok kini, nilai persediaan, dan barang perputaran lambat.
Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
database/aktivitas.sql
Unduh berkas-- aktivitas.sql — riwayat mutasi satu kuartal (Jan-Mar 2026):
-- 3 masuk, 3 keluar, 1 pindah, diterapkan set-based dalam satu transaksi.
-- Prasyarat: skema.sql dan data-awal.sql.
-- Riwayat aktivitas gudang Januari-Maret 2026: seluruh stok berasal
-- dari mutasi, diterapkan secara set-based dalam SATU transaksi.
BEGIN;
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan) VALUES
('MUT-0001', 'masuk', DATE '2026-01-05', 'Budi', 'Penerimaan PO-1001 dari supplier'),
('MUT-0002', 'masuk', DATE '2026-01-12', 'Budi', 'Penerimaan PO-1007 dari supplier'),
('MUT-0003', 'keluar', DATE '2026-01-20', 'Siti', 'Pengiriman ke Toko Pusat'),
('MUT-0004', 'pindah', DATE '2026-02-02', 'Budi', 'Penataan ulang: menyiapkan gudang cadangan'),
('MUT-0005', 'masuk', DATE '2026-02-14', 'Budi', 'Penerimaan PO-1012 dari supplier'),
('MUT-0006', 'keluar', DATE '2026-03-05', 'Siti', 'Pengiriman ke Toko Pusat'),
('MUT-0007', 'keluar', DATE '2026-03-18', 'Siti', 'Pengiriman ke Toko Cabang');
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_asal_id, lokasi_tujuan_id)
SELECT m.id, b.id, v.qty, la.id, lt.id
FROM (VALUES
('MUT-0001', 'BRG-001', 20, NULL, 'G1-A-01'),
('MUT-0001', 'BRG-005', 15, NULL, 'G1-B-02'),
('MUT-0001', 'BRG-007', 40, NULL, 'G1-A-01'),
('MUT-0001', 'BRG-008', 12, NULL, 'G1-C-03'),
('MUT-0002', 'BRG-001', 10, NULL, 'G1-A-01'),
('MUT-0002', 'BRG-002', 30, NULL, 'G1-A-01'),
('MUT-0002', 'BRG-003', 40, NULL, 'G1-B-02'),
('MUT-0002', 'BRG-006', 18, NULL, 'G1-B-02'),
('MUT-0002', 'BRG-009', 45, NULL, 'G2-A-01'),
('MUT-0003', 'BRG-007', 12, 'G1-A-01', NULL),
('MUT-0003', 'BRG-008', 5, 'G1-C-03', NULL),
('MUT-0004', 'BRG-007', 10, 'G1-A-01', 'G2-A-01'),
('MUT-0004', 'BRG-009', 15, 'G2-A-01', 'G2-B-02'),
('MUT-0005', 'BRG-004', 10, NULL, 'G1-A-01'),
('MUT-0005', 'BRG-005', 10, NULL, 'G1-B-02'),
('MUT-0006', 'BRG-001', 15, 'G1-A-01', NULL),
('MUT-0006', 'BRG-003', 20, 'G1-B-02', NULL),
('MUT-0006', 'BRG-005', 18, 'G1-B-02', NULL),
('MUT-0007', 'BRG-002', 22, 'G1-A-01', NULL),
('MUT-0007', 'BRG-009', 20, 'G2-A-01', NULL)
) AS v(kode_mutasi, kode_barang, qty, asal, tujuan)
JOIN mutasi m ON m.kode = v.kode_mutasi
JOIN barang b ON b.kode = v.kode_barang
LEFT JOIN lokasi la ON la.kode = v.asal
LEFT JOIN lokasi lt ON lt.kode = v.tujuan;
-- efek 1: seluruh mutasi masuk menambah stok lokasi tujuan
INSERT INTO stok_lokasi (barang_id, lokasi_id, qty)
SELECT mi.barang_id, mi.lokasi_tujuan_id, SUM(mi.qty)
FROM mutasi_item mi
JOIN mutasi m ON m.id = mi.mutasi_id
WHERE m.tipe = 'masuk'
GROUP BY mi.barang_id, mi.lokasi_tujuan_id
ON CONFLICT (barang_id, lokasi_id) DO UPDATE
SET qty = stok_lokasi.qty + EXCLUDED.qty;
-- efek 2: seluruh pindahan mengurangi stok lokasi asal
UPDATE stok_lokasi sl
SET qty = sl.qty - d.qty_pindah
FROM (
SELECT mi.barang_id, mi.lokasi_asal_id, SUM(mi.qty) AS qty_pindah
FROM mutasi_item mi
JOIN mutasi m ON m.id = mi.mutasi_id
WHERE m.tipe = 'pindah'
GROUP BY mi.barang_id, mi.lokasi_asal_id
) d
WHERE d.barang_id = sl.barang_id
AND d.lokasi_asal_id = sl.lokasi_id;
-- efek 3: seluruh pindahan menambah stok lokasi tujuan
INSERT INTO stok_lokasi (barang_id, lokasi_id, qty)
SELECT mi.barang_id, mi.lokasi_tujuan_id, SUM(mi.qty)
FROM mutasi_item mi
JOIN mutasi m ON m.id = mi.mutasi_id
WHERE m.tipe = 'pindah'
GROUP BY mi.barang_id, mi.lokasi_tujuan_id
ON CONFLICT (barang_id, lokasi_id) DO UPDATE
SET qty = stok_lokasi.qty + EXCLUDED.qty;
-- efek 4: seluruh mutasi keluar mengurangi stok lokasi asal
UPDATE stok_lokasi sl
SET qty = sl.qty - d.qty_keluar
FROM (
SELECT mi.barang_id, mi.lokasi_asal_id, SUM(mi.qty) AS qty_keluar
FROM mutasi_item mi
JOIN mutasi m ON m.id = mi.mutasi_id
WHERE m.tipe = 'keluar'
GROUP BY mi.barang_id, mi.lokasi_asal_id
) d
WHERE d.barang_id = sl.barang_id
AND d.lokasi_asal_id = sl.lokasi_id;
COMMIT;
database/data-awal.sql
Unduh berkas-- data-awal.sql — data master: 9 barang dan 5 lokasi (2 gudang).
-- Prasyarat: skema.sql.
-- data-awal.sql — data master: daftar barang dan lokasi penyimpanan.
INSERT INTO barang (kode, nama, kategori, satuan, stok_minimum, harga_beli) VALUES
('BRG-001', 'Kertas HVS A4 80gsm', 'Alat Tulis', 'box', 10, 185000.00),
('BRG-002', 'Pulpen Gel Hitam', 'Alat Tulis', 'box', 20, 42000.00),
('BRG-003', 'Buku Tulis 58 halaman', 'Alat Tulis', 'lusin', 30, 38500.00),
('BRG-004', 'Tinta Printer Hitam', 'Alat Tulis', 'pcs', 8, 95000.00),
('BRG-005', 'Sabun Cuci Cair 1L', 'Rumah Tangga','dus', 12, 87000.00),
('BRG-006', 'Deterjen Bubuk 800g', 'Rumah Tangga','dus', 15, 72000.00),
('BRG-007', 'Minyak Goreng 2L', 'Pangan', 'dus', 25, 115000.00),
('BRG-008', 'Beras Premium 5kg', 'Pangan', 'karung', 10, 320000.00),
('BRG-009', 'Kopi Sachet Instan', 'Pangan', 'dus', 40, 96000.00);
INSERT INTO lokasi (kode, gudang, rak) VALUES
('G1-A-01', 'Gudang Utama', 'Rak A-01'),
('G1-B-02', 'Gudang Utama', 'Rak B-02'),
('G1-C-03', 'Gudang Utama', 'Rak C-03'),
('G2-A-01', 'Gudang Cadangan', 'Rak A-01'),
('G2-B-02', 'Gudang Cadangan', 'Rak B-02');
database/skema.sql
Unduh berkas-- skema.sql — struktur lengkap database inventaris gudang.
-- Jalankan lebih dulu sebelum data-awal.sql, aktivitas.sql, queries/, atau tests/.
-- buat-tabel.sql — struktur database inventaris gudang.
-- Lima tabel: barang, lokasi, stok_lokasi, mutasi, dan mutasi_item.
CREATE TABLE barang (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
nama text NOT NULL,
kategori text NOT NULL,
satuan text NOT NULL,
stok_minimum integer NOT NULL CHECK (stok_minimum >= 0),
harga_beli numeric(12, 2) NOT NULL CHECK (harga_beli > 0)
);
CREATE TABLE lokasi (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
gudang text NOT NULL,
rak text NOT NULL,
UNIQUE (gudang, rak)
);
CREATE TABLE stok_lokasi (
barang_id integer NOT NULL REFERENCES barang (id) ON DELETE CASCADE,
lokasi_id integer NOT NULL REFERENCES lokasi (id),
qty integer NOT NULL CONSTRAINT stok_qty_nonneg CHECK (qty >= 0),
PRIMARY KEY (barang_id, lokasi_id)
);
CREATE TABLE mutasi (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
tipe text NOT NULL CHECK (tipe IN ('masuk', 'keluar', 'pindah')),
tanggal date NOT NULL,
petugas text NOT NULL,
keterangan text
);
CREATE TABLE mutasi_item (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
mutasi_id integer NOT NULL REFERENCES mutasi (id) ON DELETE CASCADE,
barang_id integer NOT NULL REFERENCES barang (id),
qty integer NOT NULL CHECK (qty > 0),
lokasi_asal_id integer REFERENCES lokasi (id),
lokasi_tujuan_id integer REFERENCES lokasi (id),
CONSTRAINT arah_lokasi_terisi
CHECK (num_nonnulls(lokasi_asal_id, lokasi_tujuan_id) >= 1)
);
CREATE INDEX idx_mutasi_tanggal ON mutasi (tanggal);
CREATE INDEX idx_mutasi_item_barang ON mutasi_item (barang_id);
CREATE INDEX idx_mutasi_item_mutasi ON mutasi_item (mutasi_id);
queries/laporan-stok.sql
Unduh berkas-- Prasyarat: jalankan database/skema.sql, database/data-awal.sql,
-- dan database/aktivitas.sql lebih dulu.
-- laporan-stok.sql — dua laporan pokok persediaan:
-- 1) stok total per barang dibandingkan dengan stok_minimum,
-- 2) sebaran stok per barang per lokasi.
-- Laporan 1: posisi stok dan status peringatan
SELECT b.kode,
b.nama,
b.kategori,
b.satuan,
b.stok_minimum,
COALESCE(SUM(sl.qty), 0) AS stok_total,
CASE WHEN COALESCE(SUM(sl.qty), 0) < b.stok_minimum
THEN 'DI BAWAH MINIMUM' ELSE 'aman' END AS status
FROM barang b
LEFT JOIN stok_lokasi sl ON sl.barang_id = b.id
GROUP BY b.id, b.kode, b.nama, b.kategori, b.satuan, b.stok_minimum
ORDER BY b.kode;
-- Laporan 2: barang tersebar di lokasi mana saja
SELECT b.kode,
b.nama,
l.gudang,
l.rak,
sl.qty
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
ORDER BY b.kode, l.kode;
queries/nilai-persediaan.sql
Unduh berkas-- Prasyarat: jalankan database/skema.sql, database/data-awal.sql,
-- dan database/aktivitas.sql lebih dulu.
-- nilai-persediaan.sql — berapa rupiah modal yang sedang tertanam
-- di gudang: harga_beli dikali stok, per barang lalu per kategori.
-- Nilai persediaan per barang
SELECT b.kode,
b.nama,
b.kategori,
COALESCE(SUM(sl.qty), 0) AS stok,
b.harga_beli,
COALESCE(SUM(sl.qty), 0) * b.harga_beli AS nilai_persediaan
FROM barang b
LEFT JOIN stok_lokasi sl ON sl.barang_id = b.id
GROUP BY b.id, b.kode, b.nama, b.kategori, b.harga_beli
ORDER BY nilai_persediaan DESC, b.kode;
-- Ringkasan per kategori dengan total keseluruhan (ROLLUP)
WITH nilai AS (
SELECT b.kategori,
COALESCE(SUM(sl.qty), 0) * b.harga_beli AS nilai
FROM barang b
LEFT JOIN stok_lokasi sl ON sl.barang_id = b.id
GROUP BY b.id, b.kategori, b.harga_beli
)
SELECT COALESCE(kategori, 'TOTAL SEMUA') AS kategori,
SUM(nilai) AS nilai_persediaan
FROM nilai
GROUP BY ROLLUP (kategori)
ORDER BY kategori NULLS LAST;
queries/perputaran-lambat.sql
Unduh berkas-- Prasyarat: jalankan database/skema.sql, database/data-awal.sql,
-- dan database/aktivitas.sql lebih dulu.
-- perputaran-lambat.sql — barang yang tidak tersentuh mutasi dalam
-- waktu lama. Tanggal laporan dipatok ke 2026-04-01 agar hasil dapat
-- direproduksi; di produksi Anda menggantinya dengan CURRENT_DATE.
WITH aktivitas_terakhir AS (
SELECT mi.barang_id, MAX(m.tanggal) AS tanggal_terakhir
FROM mutasi_item mi
JOIN mutasi m ON m.id = mi.mutasi_id
GROUP BY mi.barang_id
),
posisi AS (
SELECT b.id, b.kode, b.nama, b.kategori, b.harga_beli,
COALESCE(SUM(sl.qty), 0) AS stok
FROM barang b
LEFT JOIN stok_lokasi sl ON sl.barang_id = b.id
GROUP BY b.id, b.kode, b.nama, b.kategori, b.harga_beli
)
SELECT p.kode,
p.nama,
p.kategori,
p.stok,
at.tanggal_terakhir,
(DATE '2026-04-01' - at.tanggal_terakhir) AS hari_sejak_mutasi,
CASE
WHEN (DATE '2026-04-01' - at.tanggal_terakhir) >= 60
THEN 'perputaran lambat'
WHEN (DATE '2026-04-01' - at.tanggal_terakhir) >= 30
THEN 'waspada'
ELSE 'aktif'
END AS status_perputaran,
p.stok * p.harga_beli AS modal_terkunci
FROM posisi p
LEFT JOIN aktivitas_terakhir at ON at.barang_id = p.id
ORDER BY hari_sejak_mutasi DESC, p.kode;
queries/rekonsiliasi.sql
Unduh berkas-- Prasyarat: jalankan database/skema.sql, database/data-awal.sql,
-- dan database/aktivitas.sql lebih dulu.
-- rekonsiliasi.sql — audit konsistensi: hitung ulang stok dari riwayat
-- mutasi, lalu bandingkan dengan isi tabel stok_lokasi.
-- Setiap mutasi_item berkontribusi +qty di lokasi tujuan dan -qty di
-- lokasi asal; pindah menyentuh dua lokasi sekaligus.
WITH arus AS (
SELECT barang_id, lokasi_tujuan_id AS lokasi_id, qty
FROM mutasi_item
WHERE lokasi_tujuan_id IS NOT NULL
UNION ALL
SELECT barang_id, lokasi_asal_id, -qty
FROM mutasi_item
WHERE lokasi_asal_id IS NOT NULL
),
arus_per_lokasi AS (
SELECT barang_id, lokasi_id, SUM(qty) AS perubahan_bersih
FROM arus
GROUP BY barang_id, lokasi_id
),
detail AS (
SELECT b.kode,
l.kode AS lokasi,
COALESCE(a.perubahan_bersih, 0) AS dari_mutasi,
COALESCE(sl.qty, 0) AS di_stok_lokasi,
COALESCE(sl.qty, 0) - COALESCE(a.perubahan_bersih, 0) AS selisih
FROM arus_per_lokasi a
FULL JOIN stok_lokasi sl
ON sl.barang_id = a.barang_id AND sl.lokasi_id = a.lokasi_id
JOIN barang b ON b.id = COALESCE(a.barang_id, sl.barang_id)
JOIN lokasi l ON l.id = COALESCE(a.lokasi_id, sl.lokasi_id)
)
SELECT kode, lokasi, dari_mutasi, di_stok_lokasi, selisih
FROM detail
ORDER BY kode, lokasi;
-- Vonis rekonsiliasi: seluruh selisih harus nol
WITH arus AS (
SELECT barang_id, lokasi_tujuan_id AS lokasi_id, qty
FROM mutasi_item
WHERE lokasi_tujuan_id IS NOT NULL
UNION ALL
SELECT barang_id, lokasi_asal_id, -qty
FROM mutasi_item
WHERE lokasi_asal_id IS NOT NULL
),
arus_per_lokasi AS (
SELECT barang_id, lokasi_id, SUM(qty) AS perubahan_bersih
FROM arus
GROUP BY barang_id, lokasi_id
),
detail AS (
SELECT COALESCE(sl.qty, 0) - COALESCE(a.perubahan_bersih, 0) AS selisih
FROM arus_per_lokasi a
FULL JOIN stok_lokasi sl
ON sl.barang_id = a.barang_id AND sl.lokasi_id = a.lokasi_id
)
SELECT count(*) AS baris_diperiksa,
count(*) FILTER (WHERE selisih <> 0) AS baris_selisih,
CASE WHEN count(*) FILTER (WHERE selisih <> 0) = 0
THEN 'REKONSILIASI LULUS'
ELSE 'ADA SELISIH: PERIKSA MUTASI' END AS vonis
FROM detail;
tests/uji-atomik.sql
Unduh berkas-- uji-atomik.sql — uji perilaku transaksional stok: pengeluaran yang
-- melebihi stok harus ditolak CHECK dan tidak boleh mengubah apa pun.
-- Prasyarat: jalankan database/skema.sql, database/data-awal.sql,
-- dan database/aktivitas.sql lebih dulu.
CREATE TEMP TABLE hasil_uji (
nomor integer,
kasus text,
status text
);
DO $$
DECLARE
v_sebelum integer;
v_sesudah integer;
v_ditolak boolean := false;
BEGIN
SELECT sl.qty INTO v_sebelum
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
WHERE b.kode = 'BRG-008' AND l.kode = 'G1-C-03';
-- percobaan pengeluaran 30 karung dari stok 7: pasti melanggar CHECK.
-- Blok EXCEPTION membuat subtransaksi: efek UPDATE yang gagal
-- dibatalkan otomatis tanpa membatalkan uji ini.
BEGIN
UPDATE stok_lokasi sl
SET qty = sl.qty - 30
FROM barang b, lokasi l
WHERE b.id = sl.barang_id
AND l.id = sl.lokasi_id
AND b.kode = 'BRG-008'
AND l.kode = 'G1-C-03';
EXCEPTION WHEN check_violation THEN
v_ditolak := true;
END;
SELECT sl.qty INTO v_sesudah
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
WHERE b.kode = 'BRG-008' AND l.kode = 'G1-C-03';
INSERT INTO hasil_uji VALUES (1, 'stok awal skenario adalah 7',
CASE WHEN v_sebelum = 7 THEN 'LULUS' ELSE 'GAGAL' END);
INSERT INTO hasil_uji VALUES (2, 'pengeluaran 30 dari stok 7 ditolak CHECK',
CASE WHEN v_ditolak THEN 'LULUS' ELSE 'GAGAL: lolos' END);
INSERT INTO hasil_uji VALUES (3, 'stok tidak berubah setelah penolakan',
CASE WHEN v_sebelum = v_sesudah THEN 'LULUS' ELSE 'GAGAL: berubah' END);
END $$;
SELECT nomor, kasus, status
FROM hasil_uji
ORDER BY nomor;
tests/uji-constraint.sql
Unduh berkas-- Prasyarat: jalankan database/skema.sql, database/data-awal.sql,
-- dan database/aktivitas.sql lebih dulu.
-- 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
-- satu mutasi sah sebagai induk untuk kasus yang mengujinya
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan)
VALUES ('MUT-UJI-1', 'masuk', DATE '2026-02-01', 'Budi', 'mutasi untuk pengujian');
-- Kasus 1: kode barang duplikat (UNIQUE)
BEGIN
INSERT INTO barang (kode, nama, kategori, satuan, stok_minimum, harga_beli)
VALUES ('BRG-001', 'Kertas duplikat', 'Alat Tulis', 'box', 5, 1000.00);
INSERT INTO hasil_uji VALUES (1, 'kode barang duplikat', '23505', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (1, 'kode barang duplikat', '23505', v_state,
CASE WHEN v_state = '23505' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 2: stok_minimum negatif (CHECK)
BEGIN
INSERT INTO barang (kode, nama, kategori, satuan, stok_minimum, harga_beli)
VALUES ('BRG-100', 'Barang rusak', 'Pangan', 'pcs', -5, 1000.00);
INSERT INTO hasil_uji VALUES (2, 'stok_minimum negatif', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (2, 'stok_minimum negatif', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 3: tipe mutasi tidak dikenal (CHECK)
BEGIN
INSERT INTO mutasi (kode, tipe, tanggal, petugas)
VALUES ('MUT-UJI-2', 'hilang', DATE '2026-02-01', 'Budi');
INSERT INTO hasil_uji VALUES (3, 'tipe mutasi tidak dikenal', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (3, 'tipe mutasi tidak dikenal', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 4: item mutasi tanpa lokasi asal maupun tujuan (CHECK arah_lokasi_terisi)
BEGIN
INSERT INTO mutasi_item (mutasi_id, barang_id, qty)
VALUES ((SELECT id FROM mutasi WHERE kode = 'MUT-UJI-1'),
(SELECT id FROM barang WHERE kode = 'BRG-001'),
10);
INSERT INTO hasil_uji VALUES (4, 'item tanpa lokasi asal/tujuan', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (4, 'item tanpa lokasi asal/tujuan', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 5: item mutasi merujuk barang yang tidak ada (FOREIGN KEY)
BEGIN
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_tujuan_id)
VALUES ((SELECT id FROM mutasi WHERE kode = 'MUT-UJI-1'),
999,
10,
(SELECT id FROM lokasi WHERE kode = 'G1-A-01'));
INSERT INTO hasil_uji VALUES (5, 'item merujuk barang 999', '23503', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (5, 'item merujuk barang 999', '23503', v_state,
CASE WHEN v_state = '23503' THEN 'LULUS' ELSE 'GAGAL' END);
END;
-- Kasus 6: qty mutasi bernilai nol (CHECK qty > 0)
BEGIN
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_tujuan_id)
VALUES ((SELECT id FROM mutasi WHERE kode = 'MUT-UJI-1'),
(SELECT id FROM barang WHERE kode = 'BRG-001'),
0,
(SELECT id FROM lokasi WHERE kode = 'G1-A-01'));
INSERT INTO hasil_uji VALUES (6, 'qty mutasi bernilai nol', '23514', NULL, 'GAGAL: diterima');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE;
INSERT INTO hasil_uji VALUES (6, 'qty mutasi bernilai nol', '23514', v_state,
CASE WHEN v_state = '23514' THEN 'LULUS' ELSE 'GAGAL' END);
END;
END $$;
SELECT nomor, kasus, diharapkan, aktual, status
FROM hasil_uji
ORDER BY nomor;