Semua trigger di section ini menempel pada satu tabel dan hanya bangkit ketika baris data ditulis. Padahal operasi paling berbahaya di database produksi sering bukan INSERT atau DELETE baris, melainkan perubahan skema: satu DROP TABLE yang salah alamat menghapus seluruh data seketika, dan tidak ada satu pun trigger baris yang sempat bereaksi. PostgreSQL menyediakan penjaga khusus untuk wilayah ini: event trigger. Event trigger tidak menempel pada tabel, melainkan pada database; event trigger tidak mengawasi DML, melainkan perintah DDL seperti CREATE TABLE, ALTER TABLE, dan DROP TABLE. Dengan event trigger, kita bisa mencatat setiap perubahan skema ke tabel audit, memblokir DDL yang berbahaya sebelum dieksekusi, atau keduanya sekaligus.

Trigger Level Database untuk Perintah DDL #

Bentuk dasarnya: CREATE EVENT TRIGGER nama ON event WHEN tag IN (...) EXECUTE FUNCTION fungsi(). Klausa WHEN tag IN (...) memfilter command tag — label perintah yang juga terlihat di kolom tag output psql, misalnya CREATE TABLE atau DROP INDEX. Fungsi yang dipanggil harus dideklarasikan RETURNS event_trigger, dan di dalam fungsi tersebut tersedia variabel TG_TAG yang berisi tag perintah yang sedang berjalan. Empat event bisa dipilih:

EventMenyala kapanFungsi bantu khas
ddl_command_startsesaat sebelum DDL dieksekusi— (cocok untuk memblokir)
ddl_command_endsetelah DDL selesai dieksekusipg_event_trigger_ddl_commands()
sql_dropsesaat sebelum ddl_command_end untuk perintah DROPpg_event_trigger_dropped_objects()
table_rewritesaat ALTER TABLE menulis ulang seluruh tabel, misalnya mengubah tipe kolom—

Kita mulai dengan kasus paling umum: jejak audit DDL. Setiap perintah CREATE dan ALTER yang selesai dicatat ke tabel ddl_log lengkap dengan jenis dan identitas objeknya:

-- Jejak audit DDL: catat perintah CREATE/ALTER yang selesai dieksekusi
CREATE TABLE ddl_log (
    id         integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    tag        text NOT NULL,
    tipe_objek text,
    identitas  text
);

CREATE FUNCTION catat_ddl() RETURNS event_trigger AS $$
DECLARE
    c record;
BEGIN
    FOR c IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
        INSERT INTO ddl_log (tag, tipe_objek, identitas)
        VALUES (c.command_tag, c.object_type, c.object_identity);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER catat_perintah_ddl
ON ddl_command_end
WHEN tag IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE INDEX', 'DROP INDEX')
EXECUTE FUNCTION catat_ddl();

CREATE TABLE produk (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    harga integer NOT NULL
);

-- INSERT adalah DML, bukan DDL: event trigger tidak bereaksi
INSERT INTO produk VALUES (1, 'Kopi Gayo', 85000);

SELECT id, tag, tipe_objek, identitas FROM ddl_log ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE FUNCTION
CREATE EVENT TRIGGER
CREATE TABLE
INSERT 0 1
 id |     tag      | tipe_objek |     identitas      
----+--------------+------------+--------------------
  1 | CREATE TABLE | table      | public.produk
  2 | CREATE INDEX | index      | public.produk_pkey
(2 rows)

Ada tiga hal penting pada hasil tersebut:

  1. Satu CREATE TABLE menghasilkan dua baris log. Fungsi pg_event_trigger_ddl_commands() mengembalikan satu baris untuk setiap perintah dasar yang dijalankan; deklarasi PRIMARY KEY diam-diam menjalankan CREATE INDEX untuk index produk_pkey, sehingga keduanya tercatat. Satu kalimat SQL bisa berarti beberapa perintah catalog.
  2. INSERT INTO produk tidak meninggalkan jejak. INSERT adalah DML — event trigger sama sekali tidak menyala untuk DML, apa pun filter tag-nya. Kolom identitas juga menunjukkan format skema.objek, sehingga jejak ini tetap benar ketika skema selain public mulai dipakai.
  3. FOR c IN SELECT * FROM pg_event_trigger_ddl_commands(): fungsi bantu ini hanya boleh dipanggil dari event trigger ddl_command_end — dipanggil di konteks lain, server menolaknya dengan error. Setiap barisnya memuat command_tag, object_type, schema_name, dan object_identity yang kita pilih untuk disimpan.

Perintah ALTER TABLE dan CREATE INDEX eksplisit juga masuk jejak, sementara INSERT kedua tetap tidak terlihat:

ALTER TABLE produk ADD COLUMN stok integer NOT NULL DEFAULT 0;

CREATE INDEX idx_produk_nama ON produk (nama);

-- DML tetap tidak memicu event trigger apa pun
INSERT INTO produk (id, nama, harga) VALUES (2, 'Teh Melati', 40000);

SELECT id, tag, tipe_objek, identitas FROM ddl_log ORDER BY id;

Output yang Dihasilkan:

text
ALTER TABLE
CREATE INDEX
INSERT 0 1
 id |     tag      | tipe_objek |       identitas        
----+--------------+------------+------------------------
  1 | CREATE TABLE | table      | public.produk
  2 | CREATE INDEX | index      | public.produk_pkey
  3 | ALTER TABLE  | table      | public.produk
  4 | CREATE INDEX | index      | public.idx_produk_nama
(4 rows)

Memblokir DDL yang Berbahaya #

Event ddl_command_start menyala sebelum perintah DDL dieksekusi. Posisi ini menjadikannya titik pemeriksaan: jika fungsi trigger mengangkat exception, perintah DDL batal total sebelum menyentuh data. Kita pasang larangan menghapus tabel — semua tabel, karena event trigger berlaku untuk seluruh database:

sql blokir-drop.sql
CREATE FUNCTION blokir_drop() RETURNS event_trigger AS $$
BEGIN
    RAISE EXCEPTION 'perintah % ditolak: penghapusan tabel wajib lewat prosedur migrasi',
        TG_TAG;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER larang_drop_tabel
ON ddl_command_start
WHEN tag IN ('DROP TABLE')
EXECUTE FUNCTION blokir_drop();

DROP TABLE produk;

Output yang Dihasilkan:

text
psql:blokir-drop.sql:13: ERROR:  perintah DROP TABLE ditolak: penghapusan tabel wajib lewat prosedur migrasi
CONTEXT:  PL/pgSQL function blokir_drop() line 3 at RAISE

Perintah DROP TABLE produk batal sebelum sempat menghapus apa pun — pesan errornya pun menyertakan nilai TG_TAG, yaitu DROP TABLE, sehingga log aplikasi langsung menunjukkan perintah mana yang ditolak. Pola ini sering dipakai di database produksi untuk memaksa perubahan skema selalu lewat jalur migrasi terkontrol, bukan konsol ad-hoc.

Blokirnya bisa dibuka dengan DROP EVENT TRIGGER — dan setelah itu penghapusan berjalan normal:

sql buka-blokir.sql
-- Cabut larangan, lalu hapus tabelnya
DROP EVENT TRIGGER larang_drop_tabel;

DROP TABLE produk;

-- Tag DROP TABLE ada di daftar tag catat_perintah_ddl,
-- tetapi tidak ada baris baru yang tercatat
SELECT id, tag, tipe_objek, identitas FROM ddl_log ORDER BY id;

Output yang Dihasilkan:

text
DROP EVENT TRIGGER
DROP TABLE
 id |     tag      | tipe_objek |       identitas        
----+--------------+------------+------------------------
  1 | CREATE TABLE | table      | public.produk
  2 | CREATE INDEX | index      | public.produk_pkey
  3 | ALTER TABLE  | table      | public.produk
  4 | CREATE INDEX | index      | public.idx_produk_nama
(4 rows)

Keanehan DROP pada Event ddl_command_end #

Perhatikan hasil terakhir: DROP TABLE produk sukses dieksekusi, tag DROP TABLE tercantum dalam daftar tag catat_perintah_ddl, tetapi ddl_log tidak bertambah satu baris pun. Ini bukan bug. Event ddl_command_end memang menyala untuk perintah DROP, tetapi fungsi pg_event_trigger_ddl_commands() tidak menampilkan objek yang sudah terhapus — daftar perintahnya hanya memuat objek yang masih ada ketika fungsi tersebut dipanggil.

Detail objek yang dihapus hidup di event sql_drop melalui fungsi pg_event_trigger_dropped_objects(). Fungsi tersebut mengembalikan semua objek yang ikut lenyap: tabelnya, tipe komposit yang dibuat implisit oleh CREATE TABLE, index, constraint, sampai tabel TOAST internal. Daftar lengkap itu berisik — nama tabel TOAST mengandung OID yang berbeda-beda setiap pembuatan — sehingga praktik umumnya memfilter ke objek yang benar-benar ingin diaudit:

-- Jejak khusus objek yang dihapus: event sql_drop
CREATE TABLE drop_log (
    id         integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    tag        text NOT NULL,
    tipe_objek text NOT NULL,
    objek      text NOT NULL
);

CREATE FUNCTION catat_drop() RETURNS event_trigger AS $$
DECLARE
    c record;
BEGIN
    FOR c IN
        SELECT * FROM pg_event_trigger_dropped_objects()
        WHERE schema_name = 'public'
          AND object_type IN ('table', 'index')
    LOOP
        INSERT INTO drop_log (tag, tipe_objek, objek)
        VALUES (TG_TAG, c.object_type, c.object_name);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER catat_penghapusan
ON sql_drop
WHEN tag IN ('DROP TABLE', 'DROP INDEX')
EXECUTE FUNCTION catat_drop();

CREATE TABLE uji_drop (
    id   integer PRIMARY KEY,
    nama text NOT NULL
);

CREATE INDEX uji_drop_nama ON uji_drop (nama);

DROP TABLE uji_drop;

SELECT id, tag, tipe_objek, objek FROM drop_log ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE FUNCTION
CREATE EVENT TRIGGER
CREATE TABLE
CREATE INDEX
DROP TABLE
 id |    tag     | tipe_objek |     objek     
----+------------+------------+---------------
  1 | DROP TABLE | table      | uji_drop
  2 | DROP TABLE | index      | uji_drop_pkey
  3 | DROP TABLE | index      | uji_drop_nama
(3 rows)

Satu DROP TABLE uji_drop mencatat tiga objek: tabelnya sendiri plus dua index yang melekat (uji_drop_pkey dari primary key dan uji_drop_nama yang kita buat eksplisit). Filter schema_name = 'public' AND object_type IN ('table', 'index') membuang tipe komposit, tabel TOAST, dan constraint agar jejaknya ringkas; longgarkan filter tersebut bila kebutuhan auditnya lebih luas. Kombinasi dua event trigger — ddl_command_end untuk perintah yang mencipta dan sql_drop untuk yang menghapus — membentuk audit DDL yang utuh.

Catalog dan Hak Akses #

Seluruh event trigger yang terpasang di sebuah database bisa dilihat dari catalog pg_event_trigger:

sql katalog.sql
-- Catalog seluruh event trigger yang terpasang di database ini
SELECT evtname, evtevent, evtenabled, evttags
FROM pg_event_trigger
ORDER BY evtname;

Output yang Dihasilkan:

text
      evtname       |    evtevent     | evtenabled |                                 evttags                                 
--------------------+-----------------+------------+-------------------------------------------------------------------------
 catat_penghapusan  | sql_drop        | O          | {"DROP TABLE","DROP INDEX"}
 catat_perintah_ddl | ddl_command_end | O          | {"CREATE TABLE","ALTER TABLE","DROP TABLE","CREATE INDEX","DROP INDEX"}
(2 rows)

Kolom evttags menampilkan kembali daftar tag pada klausa WHEN — cara cepat memastikan filter mana yang sedang aktif. Kolom evtenabled bernilai O (origin) saat event trigger aktif; nilainya berubah menjadi D bila event trigger dimatikan lewat ALTER EVENT TRIGGER ... DISABLE, dan diaktifkan kembali dengan ALTER EVENT TRIGGER ... ENABLE.

Ketika dua kebutuhan ini digabung — jejak DDL di atas jejak DML — database Anda memiliki catatan lengkap atas semua perubahan, dari skema sampai isi baris. Jejak untuk sisi DML masih kita bangun secara manual di file audit sederhana; chapter Auditing dengan Trigger berikutnya menyusun pola produksinya: satu fungsi audit generik yang menangkap siapa pelakunya, kapan perubahannya, dan apa isi lama serta baru tiap baris.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

blokir-drop.sql

Unduh berkas
sql
CREATE FUNCTION blokir_drop() RETURNS event_trigger AS $$
BEGIN
    RAISE EXCEPTION 'perintah % ditolak: penghapusan tabel wajib lewat prosedur migrasi',
        TG_TAG;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER larang_drop_tabel
ON ddl_command_start
WHEN tag IN ('DROP TABLE')
EXECUTE FUNCTION blokir_drop();

DROP TABLE produk;

buat-catatan-ddl.sql

Unduh berkas
sql
-- Jejak audit DDL: catat perintah CREATE/ALTER yang selesai dieksekusi
CREATE TABLE ddl_log (
    id         integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    tag        text NOT NULL,
    tipe_objek text,
    identitas  text
);

CREATE FUNCTION catat_ddl() RETURNS event_trigger AS $$
DECLARE
    c record;
BEGIN
    FOR c IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
        INSERT INTO ddl_log (tag, tipe_objek, identitas)
        VALUES (c.command_tag, c.object_type, c.object_identity);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER catat_perintah_ddl
ON ddl_command_end
WHEN tag IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE INDEX', 'DROP INDEX')
EXECUTE FUNCTION catat_ddl();

CREATE TABLE produk (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    harga integer NOT NULL
);

-- INSERT adalah DML, bukan DDL: event trigger tidak bereaksi
INSERT INTO produk VALUES (1, 'Kopi Gayo', 85000);

SELECT id, tag, tipe_objek, identitas FROM ddl_log ORDER BY id;

buka-blokir.sql

Unduh berkas
sql
-- Cabut larangan, lalu hapus tabelnya
DROP EVENT TRIGGER larang_drop_tabel;

DROP TABLE produk;

-- Tag DROP TABLE ada di daftar tag catat_perintah_ddl,
-- tetapi tidak ada baris baru yang tercatat
SELECT id, tag, tipe_objek, identitas FROM ddl_log ORDER BY id;

catat-penghapusan.sql

Unduh berkas
sql
-- Jejak khusus objek yang dihapus: event sql_drop
CREATE TABLE drop_log (
    id         integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    tag        text NOT NULL,
    tipe_objek text NOT NULL,
    objek      text NOT NULL
);

CREATE FUNCTION catat_drop() RETURNS event_trigger AS $$
DECLARE
    c record;
BEGIN
    FOR c IN
        SELECT * FROM pg_event_trigger_dropped_objects()
        WHERE schema_name = 'public'
          AND object_type IN ('table', 'index')
    LOOP
        INSERT INTO drop_log (tag, tipe_objek, objek)
        VALUES (TG_TAG, c.object_type, c.object_name);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER catat_penghapusan
ON sql_drop
WHEN tag IN ('DROP TABLE', 'DROP INDEX')
EXECUTE FUNCTION catat_drop();

CREATE TABLE uji_drop (
    id   integer PRIMARY KEY,
    nama text NOT NULL
);

CREATE INDEX uji_drop_nama ON uji_drop (nama);

DROP TABLE uji_drop;

SELECT id, tag, tipe_objek, objek FROM drop_log ORDER BY id;

katalog.sql

Unduh berkas
sql
-- Catalog seluruh event trigger yang terpasang di database ini
SELECT evtname, evtevent, evtenabled, evttags
FROM pg_event_trigger
ORDER BY evtname;

perintah-lain.sql

Unduh berkas
sql
ALTER TABLE produk ADD COLUMN stok integer NOT NULL DEFAULT 0;

CREATE INDEX idx_produk_nama ON produk (nama);

-- DML tetap tidak memicu event trigger apa pun
INSERT INTO produk (id, nama, harga) VALUES (2, 'Teh Melati', 40000);

SELECT id, tag, tipe_objek, identitas FROM ddl_log ORDER BY id;