Permintaannya datang di ronde revisi kedua dan kedengarannya cuma butuh satu klausa. Klien ingin wawancara berhenti ikut masuk ke daftar berita, karena wawancara sudah punya rail sendiri di beranda dan tab wawancara sendiri. Satu seri wawancara di situs itu disimpan sebagai post biasa, jadi tulisan-tulisannya mendarat di daftar berita semata-mata karena mereka post.
Sinyal untuk membedakannya sudah tersedia. Feed wawancara, yang dipakai bersama oleh rail beranda dan halaman wawancara supaya keduanya tidak pernah melenceng, hanya mengambil post yang meta penanda wawancaranya bernilai '1'. Pengecualian di daftar berita sengaja saya taruh di flag yang sama. Dengan begitu kedua halaman sepakat dengan sendirinya: apa pun yang tampil di halaman wawancara otomatis absen dari daftar berita.
Klausa yang paling jelas untuk pekerjaan itu adalah NOT EXISTS, dan klausa itu salah.
Klausa yang paling jelas
Bentuknya seperti ini, sedikit disederhanakan, dengan nama key yang sudah diganti nama netral. Penggantian yang sama berlaku untuk potongan kode berikutnya:
'meta_query' => [
[ 'key' => '_theme_interview', 'compare' => 'NOT EXISTS' ],
],Dibaca sebagai kalimat, klausa itu terdengar tepat: ambil post yang tidak punya penanda wawancara. Sebelum dikirim, saya mengukurnya terhadap data live, dan versi naif ini memangkas feed dari 356 post menjadi 147. Yang seharusnya keluar cuma 25 wawancara. Bentuk yang akhirnya dikirim menghasilkan 331 berita, dan 331 ditambah 25 sama dengan 356.
Artinya klausa naif membuang 356 dikurangi 147, yaitu 209 post, untuk pekerjaan yang hanya perlu membuang 25. Selisihnya, 209 dikurangi 25, adalah 184 berita biasa yang akan ikut hilang bersama wawancara. WordPress tidak akan mengeluh soal ini, karena klausanya sah dan query-nya berjalan normal. Satu-satunya tanda ada di jumlahnya.
Nilai kosong tetap baris yang ada
Penyebabnya ada di sisi penulisan. Metabox menulis key itu dengan nilai kosong setiap kali kotaknya tidak dicentang, sehingga key tersebut ada di sebagian besar post biasa. Handler simpan generik di tema berjalan untuk setiap field yang didefinisikan, dan cabang checkbox-nya hanya mengenal dua nilai:
// cabang checkbox di handler simpan metabox, dipotong
update_post_meta( $post_id, $meta_key, ( (string) $raw === '1' ) ? '1' : '' );NOT EXISTS menanyakan ada atau tidaknya baris meta, bukan isinya. Baris yang berisi string kosong tetap baris yang ada, jadi post yang pernah tersimpan dengan kotak tidak dicentang ikut gugur dari klausa itu, sama seperti wawancara yang kotaknya dicentang.
Flag itu sendiri lahir untuk keperluan lain. Pada 6 Mei klien meminta selector manual untuk bagian wawancara di beranda, dan checkbox-nya pertama kali ditambahkan ke metabox sebuah custom post type. Klien kemudian melaporkan bahwa kotak itu tidak terlihat olehnya, jadi field yang sama ditambahkan ke metabox Post bawaan, di samping checkbox yang sudah ada di sana. Sejak itu, setiap kali post biasa disimpan lewat metabox tersebut, handler tadi ikut menulis nilai flag-nya, dicentang atau tidak.
Hitungan tadi bisa dipecah menjadi tiga kelompok. Kelompok pertama, 147 post, sama sekali tidak punya baris untuk key itu, karena hanya post seperti itulah yang lolos NOT EXISTS. Kelompok kedua, 25 post, bernilai '1'. Kelompok ketiga adalah sisanya: 356 dikurangi 147 dikurangi 25, yaitu 184 post yang punya key dengan nilai selain '1'. Daftar berita butuh kelompok pertama dan ketiga, dan 147 ditambah 184 sama dengan 331, persis jumlah berita di bentuk yang benar. NOT EXISTS hanya mengambil kelompok pertama.
OR dua klausa, dan kenapa dua-duanya perlu
Bentuk yang benar adalah OR antara NOT EXISTS dan value != '1', dan itulah yang dikirim untuk daftar berita. Pesan commit-nya meninggalkan catatan untuk siapa pun yang menyentuh filter itu berikutnya: query meta NOT EXISTS yang tampak jelas itu salah, metabox menulis key-nya dengan nilai kosong saat kotak tidak dicentang, dan bentuk yang dikirim meng-OR-kan NOT EXISTS dengan value != '1'.
Separuh NOT EXISTS menangkap post yang tidak punya baris, kelompok 147 tadi. Separuh != '1' menangkap post yang punya baris tapi tidak dicentang, kelompok 184. Separuh kedua tidak bisa berdiri sendiri, karena perbandingan nilai di meta_query hanya berlaku untuk post yang memang punya baris meta tersebut. Tanpa pasangan NOT EXISTS, kelompok 147 itulah yang akan hilang. Sisi itu pernah saya tulis di artikel tentang listing yang tidak pernah sampai ke antrean moderasi, ketika meta yang tidak pernah ditulis gugur di perbandingan !=. Kasus ini kebalikannya. Masalahnya bukan baris yang tidak ada, melainkan baris bernilai kosong yang ternyata ada di sebagian besar post.
Ronde itu ditutup pada malam 2 Agustus dengan status dideploy, di-push, cache Cloudflare di-purge, dan diverifikasi publik. Di sini saya perlu membedakan apa yang diverifikasi di mana. Angka 356, 147, dan 331 diukur terhadap data live sebelum kode dikirim. Verifikasi publik sesudahnya mencakup seluruh ronde, yaitu 8 dari 8 penanda ronde kedua lewat DNS sungguhan tanpa cookie, dan bukan hitung ulang khusus untuk feed berita.
Satu helper untuk dua feed
Blok berita di beranda punya masalah yang sama, dan di sana masalahnya sudah kelihatan: dua dari empat baris terbaru di blok itu memang wawancara. Keesokan malamnya perbaikannya saya kirim sebagai satu helper yang dipakai bersama oleh beranda dan daftar berita, supaya dua feed itu tidak bisa melenceng satu sama lain. Docblock-nya membuka dengan peringatan yang sama (dipotong, nama dinetralkan):
/**
* NOT a bare NOT EXISTS. The metabox writes _theme_interview with
* an EMPTY value whenever the box is left unticked, so the key is present on
* most ordinary posts: measured live, NOT EXISTS alone cuts 429 posts to 172.
*/
function theme_exclude_interview_flag() {
return [
'relation' => 'OR',
[ 'key' => '_theme_interview', 'compare' => 'NOT EXISTS' ],
[ 'key' => '_theme_interview', 'value' => '1', 'compare' => '!=' ],
];
}Angka 429 di docblock itu berasal dari pengukuran ulang di permukaan ini sebelum saya mempercayainya. NOT EXISTS polos memangkas 429 post menjadi 172, sedangkan bentuk OR menghasilkan 404, dan 404 ditambah 25 wawancara ber-flag sama dengan 429. Di permukaan ini klausa naif akan membuang 429 dikurangi 172, yaitu 257 post, demi mengeluarkan 25.
Komentar di template beranda menyebut alasan pengecualiannya: blok itu berlabel berita, dan rail wawancara di bawahnya sudah memuat wawancara. Untuk alasan kenapa NOT EXISTS saja salah di sana, komentar itu menunjuk ke helper. Fallback cover di beranda ikut disaring, tetapi pin editorial yang dipasang dengan sengaja tetap dihormati, apa pun isinya. Pesan commit perbaikan ini mengulang aturannya sekali lagi: OR antara NOT EXISTS dan value != '1', tidak pernah NOT EXISTS polos. Re-audit malam itu ditutup dengan status semua live, dan commit tersebut termasuk dalam daftar yang semuanya sudah di-push.
Jadi peringatan yang sama sekarang tertulis atau dirujuk di empat tempat: dua pesan commit, docblock helper, dan komentar di template beranda.
Karena pengecualian dan feed wawancara membaca flag yang sama, kotak itu sekarang menentukan dua hal sekaligus. Dicentang berarti post masuk feed wawancara dan keluar dari feed berita, tidak dicentang berarti sebaliknya. Teks bantuan kotak itu di editor meminta agar ia dicentang di setiap wawancara.
Untuk taksonomi, NOT IN sudah cukup
Pada 10 Agustus pola serupa datang dari arah taksonomi. Orang di pihak klien menerbitkan sebuah tulisan berformat tertentu, melihatnya muncul di daftar berita beranda, mengira dirinya salah memberi tag, lalu membatalkan terbitnya. Tag-nya sebenarnya benar. Daftar berita memang tidak pernah menyaring berdasarkan format.
Pengecualiannya dibuat sebagai helper tax_query yang dipasang di tiga permukaan: fallback cover beranda, daftar terbaru beranda, dan daftar berita. Helper itu sengaja meniru keputusan helper wawancara, hanya bergeser satu taksonomi. Bedanya, kali ini tidak ada pasangan NOT EXISTS. Di taksonomi, NOT IN tidak membutuhkannya, karena WordPress mengompilasinya menjadi subquery ID NOT IN (SELECT object_id ...), sehingga post yang tidak punya term sama sekali tetap lolos.
Bahaya di sisi ini justru terbalik. Komentar di kodenya mencatat bahwa saat itu hanya 1 dari 435 post yang punya term di taksonomi format, sehingga kesalahan berbentuk EXISTS, yang mensyaratkan post punya term, akan mengosongkan feed berita, bukan sekadar memangkasnya. Di produksi, pengecualian ini terbukti mengubah 444 post menjadi 443, tepat satu post yang keluar, dan dengan filter topik aktif, 31 menjadi 30.
Satu keputusan lagi ada di daftar berita. Di sana klausa pengecualian digabung ke tax_query, tidak pernah di-assign, karena filter topik dari pembaca juga membangun tax_query-nya sendiri. Kalau salah satunya di-assign menimpa yang lain, pengecualian itu hilang diam-diam tepat ketika pembaca menyempitkan feed. Versi sederhananya, dengan nama yang dinetralkan:
// Contoh minimal, nama fungsi, taksonomi, dan term dinetralkan.
function theme_news_format_exclusion() {
return [
[
'taxonomy' => 'content_format',
'field' => 'slug',
'terms' => [ 'format-a' ],
'operator' => 'NOT IN',
],
];
}
// Built up rather than assigned: the topic filter below adds a clause of its
// own, and assigning either one over the other would silently drop the exclusion
// exactly when a reader narrows the feed.
$tax_query = theme_news_format_exclusion();
if ( $topic_clause ) {
$tax_query[] = $topic_clause;
}Angka 31 menjadi 30 tadi adalah bukti untuk bagian ini: pengecualiannya tetap bekerja saat filter topik aktif.
Hitungannya harus pas
Aturan kerja yang saya catat dari kasus ini pendek: filter meta harus diverifikasi terhadap hitungan nyata sebelum dipercaya. Yang membuat aturan itu bisa langsung dipakai adalah satu persamaan yang berlaku di setiap angka di atas. Hasil filter ditambah jumlah post yang memang sengaja dikecualikan harus sama dengan total sebelum filter. Di daftar berita, 331 ditambah 25 sama dengan 356. Di beranda, 404 ditambah 25 sama dengan 429. Di taksonomi, 443 ditambah 1 sama dengan 444. Klausa naif gagal di persamaan yang sama, karena selisih 356 dan 147 adalah 209, bukan 25.
Persamaan itu tidak bergantung pada cara WordPress mengompilasi klausanya. Ia hanya butuh tiga angka yang bisa dihitung sebelum kode naik: total, hasil filter, dan jumlah post yang memang ingin dikeluarkan. Kalau selisih dua angka pertama tidak sama dengan angka ketiga, klausanya belum boleh dipercaya, seberapa pun masuk akal bunyinya.