ข้าม​ไป​ยัง​เนื้อหา

UPDATE ที่​ย้าย​แถว​ข้าม​ผู้​เช่า​ได้ 42501 แล้ว​เติม WITH CHECK ที่​เปิด​ให้​สุด ก็​ยัง​ได้ 42501

บท​ที่ 2 จบ​ลง​ที่​การ​อ่าน ผู้​เช่า​ที่​ถูก​ตั้ง​ไว้​เห็น​สาม​แถว​ของ​ตัว​เอง และ​ไม่​เห็น​ของ​คน​อื่น

บท​นี้​เปลี่ยน​คำ​สั่ง​จาก​การ​อ่าน​เป็นการ​เขียน แล้ว​ถาม​คำถาม​เดียว อะไร​กัน​ไม่​ให้​ผู้​เช่า​คน​หนึ่ง ย้าย​แถว​ไป​เป็น​ของ​อีก​คน

คำ​ตอบ​ที่​คน​ส่วน​ใหญ่​คาด​คือ WITH CHECK เพราะ​เป็น​ส่วน​ของ policy ที่​มีชื่อ​ว่า​ตรวจ คำ​ตอบ​ที่​วัด​ได้​ไม่ใช่​ตัว​นั้น และ​การ​เขียน​ตัว​นั้น​แบบ​หลวม เป็นการเปิดทาง​ให้​แถว​ลง ไม่ใช่​การ​ปิด

📦 clinic — ซี​รีส์​เดิม แต่​หกบท​แรก​เป็น SQL ล้วน

คอร์ส​นี้​อยู่​ใน​ซี​รีส์​คลินิก​เดียว​กับ คอร์ส​คัด​กรอง​ผู้​เรียก และ คอร์ส CORS กับ CSRF แต่​หกบท​แรก​ไม่มี​แอป​มา​เกี่ยวข้อง มี​แต่​ตาราง policy และ role

ตัวเลข​ทุก​ตัว​ใน​บท​นี้​คุณ​รัน​เอง​ได้ สั่ง scripts/clinic-rls/up.sh ซึ่ง​ยก container ชื่อ kaen-rls-pg ขึ้น​ที่ port 55438 แล้ว​สั่ง python3 scripts/clinic-rls/spike.py เก็บ​กวาด​ด้วย docker rm -f kaen-rls-pg

ทรง​ตั้ง​ต้น​เหมือน​บท​ที่ 1 ทุก​บรรทัด ต่าง​กัน​ที่​ชื่อ​ตาราง และ​บท​นี้​ไม่​สั่ง FORCE เลย​สัก​ครั้ง

ทรง​ตั้ง​ต้น · สั่ง​โดย role ที่​เป็น​เจ้าของ​ตาราง
create table t_update (
id bigserial primary key,
tenant_id text not null,
patient text not null,
archived boolean not null default false);
insert into t_update (tenant_id, patient, archived) values
('clinic-a','Ann',false),('clinic-a','Ben',false),('clinic-a','Cid',true),
('clinic-b','Dan',false),('clinic-b','Eve',false),('clinic-b','Fay',true);
grant select, insert, update, delete on t_update to kaen_rls_app, kaen_rls_bypass;
grant usage, select on sequence t_update_id_seq to kaen_rls_app, kaen_rls_bypass;
alter table t_update enable row level security;
create policy p on t_update for all using (tenant_id = current_setting('app.tenant', true));

บรรทัด​สุดท้าย​คือ CREATE POLICYCREATE POLICYคำ​สั่ง​ที่​ผูก​นิพจน์​หนึ่ง​เข้า​กับ​ตาราง​หนึ่ง · ตาราง t_app ของ probe มี policy ผูก​อยู่ 1 ใบ (appPolicyCount) และ​เขียน​ถูก​ทุก​ตัว​อักษร แต่​จำนวน policy ที่​ผูก​อยู่​ไม่ใช่​หลัก​ฐาน​ว่า​มัน​เคย​ถูก​เรียก​ใช้ ที่​มี​แต่ USINGUSING (…)นิพจน์​ที่ policy ใช้​คัด​ว่า​แถว​ใด​มอง​เห็น​หรือ​แตะ​ได้ · เมื่อ policy ไม่มี WITH CHECK นิพจน์​นี้​ทำ​หน้าที่​ตรวจ​แถว​หลัง​การ​เขียน​ให้​เอง​ด้วย · UPDATE ที่​ย้าย​แถว​ข้าม​ผู้​เช่า​บน​ตาราง t_update ใต้ USING ล้วน ได้ 42501 และ​ยอด​ของ​สอง​ผู้​เช่า​ค้าง​ที่ 3 กับ 3 ใบ​เดียว ไม่มี​ส่วน​อื่น​ต่อ​ท้าย for all ทำให้​ใบ​นี้​ครอบ​ทั้ง SELECT INSERT UPDATE และ DELETE

ผู้​สั่ง​คำ​สั่ง​เขียน​ทุก​ก้อน​ใน​บท​นี้​คือ kaen_rls_app ซึ่ง​ไม่ใช่​เจ้าของ​ตาราง​และ​ไม่ใช่ superuser นิพจน์​จึง​ถูก​เรียก​ใช้​จริง โดย​ไม่​ต้อง​สั่ง FORCE ตาม​ที่ บท​ที่ 1 วัด​ไว้

ส่วน​ยอด​ของ​แต่ละ​ผู้​เช่า เจ้าของ​ตาราง​เป็น​คน​นับ เจ้าของ​อยู่​นอก policy ของ​ตัว​เอง เพราะ​ตาราง​ใบ​นี้​มี​แค่ ENABLE ยอด​ทุก​ตัว​ที่​ชื่อ​คีย์​ลงท้าย​ด้วย RowsA หรือ RowsB จึง​เป็น​ยอด​จริง​ทั้ง​ตาราง ส่วน​คีย์​ที่​ไม่​ได้​ลงท้าย​แบบ​นั้น ผู้​สั่ง​เป็น​คน​อื่น

คำ​สั่ง​ที่​คาด​ว่า​จะ​พัง ถูก​เรียก​ผ่าน​ฟังก์ชัน public.kaen_try ซึ่ง​คืน​ค่า​เป็น​สตริง​สอง​ส่วน

รูป​เดียว​ที่​ทุก​คีย์​ใน​บท​นี้​ใช้
select 'KAEN|updMoveUsingOnly|'||public.kaen_try(
'update t_update set tenant_id=''clinic-b'' where patient=''Ann''');
  • สำเร็จ ได้ 00000 เว้น​วรรค แล้ว​ตาม​ด้วย​จำนวน​แถว​ที่​คำ​สั่ง​กระทำ​จริง
  • ล้ม ได้ SQLSTATESQLSTATEรหัส​ห้า​หลัก​ที่ PostgreSQL ให้​กับ​ผล​ของ​คำ​สั่ง​หนึ่ง · คอร์ส​นี้​ยก​เฉพาะ​รหัส ไม่​ยก​ข้อความ​ผิด​พลาด เพราะ​ข้อความ​เป็น​ร้อย​แก้ว​ที่​เปลี่ยน​ข้าม major ส่วน​รหัส​ไม่​เปลี่ยน · 00000 แปล​ว่า​คำ​สั่ง​สำเร็จ ซึ่ง​เกิด​คู่​กับ​จำนวน​แถว​ที่​ถูก​กระทำ​เท่าใด​ก็ได้ ของ​ข้อ​ผิด​พลาด เว้น​วรรค แล้ว​ตาม​ด้วย -1

-1 ไม่ใช่​จำนวน​แถว มัน​คือ​ค่าที่​ฟังก์ชัน​ใส่​ไว้​เพื่อ​บอก​ว่า​คำ​สั่ง​ไม่​เคย​นับ​แถว​เลย คอร์ส​นี้​ยก​เฉพาะ​รหัส ไม่​ยก​ข้อความ​ผิด​พลาด เพราะ​ข้อความ​เปลี่ยน​ข้าม major ส่วน​รหัส​ไม่​เปลี่ยน

คำ​สั่ง​สอง​ก้อน​ใน session เดียว ต่าง​กัน​ที่​ก้อน​หนึ่ง​แตะ tenant_id อีก​ก้อน​ไม่​แตะ

ผู้​สั่ง​คือ kaen_rls_app · app.tenant เป็น clinic-a
set app.tenant = 'clinic-a';
select 'KAEN|updMoveUsingOnly|'||public.kaen_try(
'update t_update set tenant_id=''clinic-b'' where patient=''Ann''');
select 'KAEN|updSameTenant|'||public.kaen_try(
'update t_update set patient=''Ann2'' where patient=''Ann''');
เจ้าของ​ตาราง​นับ​ยอด​หลัง​จาก​นั้น
select 'KAEN|updUsingOnlyRowsA|'||count(*) from t_update where tenant_id='clinic-a';
select 'KAEN|updUsingOnlyRowsB|'||count(*) from t_update where tenant_id='clinic-b';
คีย์สิ่ง​ที่​คำ​สั่ง​ทำค่าที่​วัด​ได้
updMoveUsingOnlyย้าย​แถว Ann ไป​เป็น​ของ clinic-b42501 -1
updSameTenantเปลี่ยน​ชื่อ Ann เป็น Ann2 ผู้​เช่า​เดิม00000 1
updUsingOnlyRowsAยอด​ของ clinic-a หลัง​จบ​ทั้ง​สอง​คำ​สั่ง3
updUsingOnlyRowsBยอด​ของ clinic-b หลัง​จบ​ทั้ง​สอง​คำ​สั่ง3

42501 insufficient_privilegeSQLSTATE 42501 (insufficient_privilege)รหัส​ที่ RLS ใช้​ปฏิเสธ​การ​เขียน​แบบ​ส่ง​เสียง​ออก​มา · วัด​ได้​ใน​ห้า​ทรง คือ UPDATE บน​ตาราง t_update ที่​ย้าย​แถว​ข้าม​ผู้​เช่า ทั้ง​ใต้ USING ล้วน​และ​ใต้ WITH CHECK (true) · INSERT ข้าม​ผู้​เช่า​บน​ตาราง t_insert ใต้ USING ล้วน · ทรง​เดียวกัน​ที่​ผ่าน​แล้ว​แต่​เติม RETURNING เข้าไป · และ INSERT บน​ตาราง t_selonly ใต้ policy ที่​ระบุ FOR SELECT อย่าง​เดียว · ตรง​ข้าม​กับ UPDATE และ DELETE บน​ตาราง​ใบ​หลัง​นั้น ซึ่ง​คืน 00000 และ 0 แถว​โดย​ไม่​ส่ง​เสียง คือ​รหัส​ที่ RLS ใช้​ปฏิเสธ​การ​เขียน​แบบ​ส่ง​เสียง​ออก​มา สอง​แถว​ล่าง​ยืนยัน​ว่า​เสียง​นั้น​ตรง​กับ​ของ​จริง ไม่มี​แถว​ไหน​ย้าย​ฝั่ง

แถว​ที่​สอง​สำคัญ​พอกัน คำ​สั่ง​ที่​แตะ​แถว​เดีย​วกันเป๊ะ แต่​ไม่​แตะ tenant_id ผ่าน​และ​กระทบ​หนึ่ง​แถว สิ่ง​ที่​ถูก​ปฏิเสธ​จึง​ไม่ใช่ “การ​เขียน​ลง​แถว​นี้” แต่​เป็น​ค่า​ใหม่​ที่​แถว​นี้​จะ​กลาย​เป็น

p ไม่มี​ส่วน​ไหน​ที่​เขียน​ไว้​ให้​ตรวจ​แถว​หลัง​การ​เขียน​เลย แต่​การ​ปฏิเสธ​เกิด​ขึ้น​จริง หัวข้อ​ถัด​ไป​ทดสอบ​ข้อ​นี้​ด้วย​การ​เติม​ส่วน​นั้น​เข้าไป

นิพจน์​ที่​เติม​เข้าไป​คือ true ซึ่ง​อนุญาต​ทุก​แถว​ที่​เป็น​ไป​ได้ และ​ตัวเลข​ทั้ง​สาม​ตัว​เท่า​เดิม

แก้ policy ใบ​เดิม · สั่ง​โดย​เจ้าของ​ตาราง
alter policy p on t_update
using (tenant_id = current_setting('app.tenant', true)) with check (true);
แถว​เดียวกัน · ชื่อ​ของ​มัน​เปลี่ยน​เป็น Ann2 ไป​แล้ว​จาก​คำ​สั่ง​ก่อน​หน้า
set app.tenant = 'clinic-a';
select 'KAEN|updMoveWithCheckTrue|'||public.kaen_try(
'update t_update set tenant_id=''clinic-b'' where patient=''Ann2''');
คีย์ค่าที่​วัด​ได้
updMoveWithCheckTrue42501 -1
updWithCheckRowsA3
updWithCheckRowsB3

WITH CHECKWITH CHECK (…)นิพจน์​ที่​ตรวจ​แถว​หลัง​การ​เขียน · เติม WITH CHECK (true) ให้ policy ของ INSERT บน​ตาราง t_insert แล้ว​แถว​ข้าม​ผู้​เช่า​ลง​จริง ได้ 00000 หนึ่ง​แถว และ​แถว​ของ​ผู้​เช่า​ปลาย​ทาง​ขึ้น​เป็น 4 · การ​เขียน​มัน​แบบ​หลวม​จึง​เป็นการ​เปิด​ทาง ไม่ใช่​การ​ปิด คือ​นิพจน์​ที่​ตรวจ​แถว​หลัง​การ​เขียน และ true คือ​รูป​ที่​หลวม​ที่สุด​ที่​เขียน​ได้ มัน​ผ่าน​ให้​ทุก​แถว​โดย​ไม่​ดู​ค่า​อะไร​เลย ผล​ที่​ได้​คือ​รหัส​เดิม​และ​ยอด​เดิม

ตัว​ที่​ปฏิเสธ จึง​ไม่ใช่ WITH CHECK เพราะ​ถ้า​ใช่ การ​เปิด​มัน​จน​สุด​ต้อง​ทำให้​คำ​สั่ง​ผ่าน สิ่ง​ที่​บท​นี้​เพิ่ง​วัด​คือ​มัน​ไม่​ผ่าน ทั้ง​ที่​นิพจน์​ตัว​นั้น​เป็น true

เปลี่ยน​นิพจน์​ฝั่ง SELECT เป็น true แล้ว​คำ​สั่ง​เดิม​ผ่าน โดย USING และ WITH CHECK ฝั่ง​เขียน​ไม่​เปลี่ยน

ทรง​ที่​สาม · แยก policy ของ SELECT ออก​มา​หนึ่ง​ใบ
drop policy p on t_update;
create policy p_sel on t_update for select using (true);
create policy p_upd on t_update for update
using (tenant_id = current_setting('app.tenant', true)) with check (true);
คำ​สั่ง​เดิม​เป๊ะ · app.tenant เป็น clinic-a
set app.tenant = 'clinic-a';
select 'KAEN|updMoveSelectOpen|'||public.kaen_try(
'update t_update set tenant_id=''clinic-b'' where patient=''Ann2''');
คีย์ค่าที่​วัด​ได้
updMoveSelectOpen00000 1
updSelectOpenRowsA2
updSelectOpenRowsB4

ยอด​ขยับ​จริง clinic-a เหลือ 2 และ clinic-b เป็น 4 แถว​หนึ่ง​แถว​ย้าย​ฝั่ง​ไป​แล้ว

เทียบ​สอง​ทรง​ที​ละ​ส่วน นิพจน์​ที่​ครอบ UPDATE เหมือน​กัน​ทั้ง​คู่ คือ USING เป็นการ​เทียบ​ผู้​เช่า และ WITH CHECK เป็น true ส่วน​ที่​เปลี่ยน​มี​ส่วน​เดียว คือ​นิพจน์​ที่​ครอบ SELECT ซึ่ง​เปลี่ยน​จาก​การ​เทียบ​ผู้​เช่า เป็น true

เอกสาร​ของ PostgreSQL 18 มี​ตาราง​ชื่อ Policies Applied by Command Type แถว​ของ UPDATE ระบุ​ว่า​นิพจน์ USING ของ policy ฝั่ง SELECT ถูก​ใช้​สอง​หน้าที่ คือ​คัด​แถว​เดิม และ​ตรวจ​แถว​ใหม่​ด้วย

เอกสาร​หน้า​เดียวกัน​อธิบาย​เงื่อนไข​ของ​หน้าที่​ที่​สอง คำ​สั่ง UPDATE ที่​ต้อง​อ่าน​ค่า​จาก​ตาราง เช่น​มี where ต้อง​มี​สิทธิ์ SELECT และ policy ฝั่ง​นั้น​จึง​ถูก​นำ​มา​ใช้​เพิ่ม​จาก policy ฝั่ง UPDATE

แถว​ใหม่​ถูก​ตรวจ​สอง​รอบ และ​รอบ​ที่​คน​ลืม​คือ​รอบ​ของ SELECT

update … where patient='Ann2' มี where จึง​ต้อง​อ่าน​ค่า​จาก​ตาราง ใน​ทรง​ที่​หนึ่ง​และ​ที่​สอง policy ใบ​เดียว​เป็น for all มัน​จึง​เป็น policy ฝั่ง SELECT ไป​ด้วย และ​นิพจน์​ของ​มัน​คือ​การ​เทียบ​ผู้​เช่า

แถว​ใหม่​ที่​จะ​กลาย​เป็น​ของ clinic-b ไม่​ผ่าน​นิพจน์​นั้น ทั้ง​ที่ WITH CHECK เป็น true เพราะ​การ​ตรวจ​สอง​รอบ​นี้​เป็นการ​รวม​ด้วย AND ไม่ใช่​การ​เลือก​อัน​ใด​อัน​หนึ่ง

ข้อ​นี้​เป็น​ข้อ​อ้าง​เรื่อง​กลไก จึง​อ้าง​เอกสาร ส่วน​ตัวเลข​สาม​ตัว​ข้าง​บน​อ้าง probe

คำ​สั่ง​เดียว​กับ​ข้าง​บน​คนละ​ชนิด และ WITH CHECK (true) ที่​ไม่​ช่วย​อะไร​เลย​กับ UPDATE เปิด​ทาง​ให้ INSERT

ทรง​นี้​ใช้​ตาราง t_insert ซึ่ง​สร้าง​ด้วย DDL ก้อน​เดียว​กับ t_update ทุก​บรรทัด และ​ผูก policy ใบ​เดียว​รูป​เดียวกัน คือ for all ที่​มี​แต่ USING

ผู้​สั่ง​คือ kaen_rls_app · app.tenant เป็น clinic-a
set app.tenant = 'clinic-a';
select 'KAEN|insUsingOnlyOther|'||public.kaen_try(
'insert into t_insert (tenant_id, patient) values (''clinic-b'',''Mallory'')');
select 'KAEN|insUsingOnlyOwn|'||public.kaen_try(
'insert into t_insert (tenant_id, patient) values (''clinic-a'',''Gil'')');
คีย์แถว​ที่​พยายาม​เขียนค่าที่​วัด​ได้
insUsingOnlyOtherของ clinic-b42501 -1
insUsingOnlyOwnของ clinic-a00000 1
insUsingOnlyRowsAยอด​ของ clinic-a หลัง​จบ4
insUsingOnlyRowsBยอด​ของ clinic-b หลัง​จบ3

INSERT ไม่มี​แถว​เดิม​ให้​คัด​สัก​แถว มัน​มี​แต่​แถว​ใหม่ การ​ปฏิเสธ​ของ​แถว​แรก​จึง​มา​จาก นิพจน์​ที่​ถูก​นำ​ไป​ใช้​ตรวจ​แถว​ใหม่ ทั้ง​ที่ policy ใบ​นี้​ไม่มี WITH CHECK เขียน​ไว้​เลย

เอกสาร​ของ PostgreSQL 18 อธิบาย​ข้อ​นี้​ไว้​กับ policy ที่​ระบุ ALL ว่า​นิพจน์ USING ถูก​นำ​ไป​ใช้​ทั้ง​ฝั่ง​ที่​คัด​แถว​และ​ฝั่ง​ที่​เขียน เมื่อ policy ใบ​นั้น​มี​แต่ USING อย่าง​เดียว

เติม with check (true) ให้​ใบ​เดิม แล้ว​สั่ง​ใหม่
alter policy p on t_insert
using (tenant_id = current_setting('app.tenant', true)) with check (true);
คีย์ค่าที่​วัด​ได้
insWithCheckOther00000 1
insWithCheckRowsA4
insWithCheckRowsB4

insWithCheckOther คือ​คำ​สั่ง​รูป​เดียว​กับ​ที่​เพิ่ง​ได้ 42501 ต่าง​กัน​ที่​ชื่อ​ผู้​ป่วย​ที่​ใส่​ลง​ไป ซึ่ง​ไม่มี​นิพจน์​ไหน​ใน policy อ่าน​ค่า​นั้น​เลย ตอน​นี้​มัน​ผ่าน และ​กระทบ​หนึ่ง​แถว ยอด​ของ clinic-b ขึ้น​จาก 3 เป็น 4 แถว​ข้าม​ผู้​เช่า​ลง​ใน​ตาราง​จริง

นี่​คือ​ข้อ​ที่​บท​นี้​มี​ไว้​พิสูจน์ นิพจน์​ที่​หลวม​ใน WITH CHECK ไม่​ได้​เพิ่ม​การ​ตรวจ มัน​ถอน​การ​ตรวจ​ที่ USING ทำให้​อยู่​แล้ว​ออก​ไป แล้ว​เอา true ไป​วาง​แทน

คำ​สั่ง​รูป​เดียวกัน​สอง​ก้อน​ใน session เดียว ก้อน​ที่​มี​สอง​คำ​ต่อ​ท้าย ถูก​ปฏิเสธ

สอง INSERT ใน session เดียว ต่าง​กัน​แค่​ท้าย​คำ​สั่ง
set app.tenant = 'clinic-a';
select 'KAEN|insWithCheckOther|'||public.kaen_try(
'insert into t_insert (tenant_id, patient) values (''clinic-b'',''Mal2'')');
select 'KAEN|insWithCheckOtherReturning|'||public.kaen_try(
'insert into t_insert (tenant_id, patient) values (''clinic-b'',''Mal3'') returning id');
select 'KAEN|insWithCheckVisibleToApp|'||count(*) from t_insert where true;
คีย์ค่าที่​วัด​ได้
insWithCheckOther00000 1
insWithCheckOtherReturning42501 -1
insWithCheckVisibleToApp4

สอง​คำ​สั่ง​นี้​ต่าง​กัน​สอง​อย่าง คือ​ชื่อ​ผู้​ป่วย​ที่​ใส่​ลง​ไป ซึ่ง​ไม่มี​นิพจน์​ไหน​ใน policy อ่าน และ returning id ที่​ต่อ​ท้าย​ก้อน​ที่​สอง ค่าที่​ต่าง​กัน​คือ​รหัส​ที่​ได้​กลับ​มา

RETURNINGRETURNINGส่วน​ท้าย​ของ​คำ​สั่ง​เขียน ที่​สั่ง​ให้​คืน​แถว​ซึ่ง​เพิ่ง​ถูก​เขียน · การ​คืน​นั้น​ถูก​คัด​ด้วย policy ของ SELECT · INSERT บน​ตาราง t_insert ทรง​เดิม​ที่​ผ่าน​ด้วย 00000 หนึ่ง​แถว กลับ​เป็น 42501 เมื่อ​เติม​เพียง RETURNING เข้าไป สั่ง​ให้​คำ​สั่ง​คืน​แถว​ที่​เพิ่ง​เขียน​กลับ​มา และ​การ​คืน​นั้น​คือ​การ​อ่าน เอกสาร​ของ PostgreSQL 18 ระบุ​ว่า​แถว​ที่​จะ​ถูก​คืน​ต้อง​ผ่าน policy ฝั่ง SELECT ก่อน และ​แถว​ที่​ไม่​ผ่าน​จะ​ไม่​ถูก​ข้าม​ไป​โดย​ไม่​ส่ง​เสียง

insWithCheckVisibleToApp ต้อง​อ่าน​คู่​กับ​ยอด​ของ​เจ้าของ​ตาราง​ถึง​จะ​เห็น​ของ​จริง ตอน​นั้น​ตาราง​มี 8 แถว คือ clinic-a 4 แถว และ clinic-b 4 แถว ตาม​ที่ insWithCheckRowsA กับ insWithCheckRowsB วัด​ไว้ แต่​ผู้​สั่ง​นับ​ได้ 4

สี่​แถว​นั้น​เป็น​ของ clinic-a ทั้งหมด แถว​ที่​ผู้​สั่ง​เพิ่ง​เขียน​ลง​ไป​เอง ไม่​ได้​อยู่​ใน​นั้น ผู้​เขียน​มอง​ไม่​เห็น​แถว​ที่​ตัว​เอง​เพิ่ง​เขียน และ​รู้​ว่า​เขียน​สำเร็จ​จาก 00000 1 เท่านั้น

สี่​ทาง​ที่​จบ​ลง​ที่ 42501 และ​สาม​ทาง​ที่​ไม่​จบ​ตรง​นั้น

หัวข้อ​ที่​มีชื่อ​ว่า “สี่​ทาง​ที่​จบ​ลง​ที่ 42501 และ​สาม​ทาง​ที่​ไม่​จบ​ตรง​นั้น”

แผนภาพ​เดียว​รวบ​ทุก​ทรง​ใน​บท พร้อม​คีย์​ที่ probe ใช้​เก็บ​ค่า​ของ​แต่ละ​ทาง

flowchart TB
  U["UPDATE · ย้ายแถวของ clinic-a ไปเป็นของ clinic-b"] --> U1{"policy ใบเดียว for all"}
  U1 -->|"มีแต่ USING · updMoveUsingOnly"| UX["42501 · ยอดค้างที่ 3 กับ 3"]
  U1 -->|"เติม WITH CHECK true · updMoveWithCheckTrue"| UY["42501 · ยอดค้างที่ 3 กับ 3"]
  U --> U2{"แยก policy ของ SELECT ออกมา แล้วเปิดเป็น true"}
  U2 -->|"updMoveSelectOpen"| UZ["00000 หนึ่งแถว · ยอดเปลี่ยนเป็น 2 กับ 4"]
  V["UPDATE · ไม่แตะ tenant_id · updSameTenant"] --> VZ["00000 หนึ่งแถว"]
  I["INSERT · แถวของ clinic-b"] --> I1{"policy ใบเดียว for all"}
  I1 -->|"มีแต่ USING · insUsingOnlyOther"| IX["42501"]
  I1 -->|"เติม WITH CHECK true · insWithCheckOther"| IY["00000 หนึ่งแถว · ยอดของ clinic-b เป็น 4"]
  IY --> I2{"เติม returning id ต่อท้ายคำสั่งเดิม"}
  I2 -->|"insWithCheckOtherReturning"| IR["42501"]

คำ​บรรยาย​ภาพ: สี่​ทางใน​ภาพ​จบ​ที่ 42501 และ​สาม​ทาง​จบ​ที่ 00000 พร้อม​หนึ่ง​แถว ทาง​ของ UPDATE ที่​เปิด WITH CHECK จน​สุด​แล้ว​ยัง​ถูก​ปฏิเสธ กับ​ทาง​ของ INSERT ที่​เติม returning id แล้ว​ถูก​ปฏิเสธ เป็น​สอง​ทาง​ที่​นิพจน์​ฝั่ง​เขียน​อนุญาต​ไว้​แล้ว​ทั้ง​คู่

ตัวเลข​ทั้ง​บท​มา​จาก SQL ที่​รัน​จริง ส่วน​คำ​อธิบาย​ว่า​นิพจน์​ตัว​ไหน​ถูก​ใช้​ตอน​ไหน มา​จาก​เอกสาร

spike.py รัน​ชุด​ข้อ​อ้าง​ทั้ง​ชุด​สาม​รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง แล้ว​เทียบ​ผล​ที​ละ​คีย์ รอบ​ที่​รัน​เมื่อ 2026-09-06 ได้ 52 ข้อ​อ้าง​ตรง​ทั้งหมด 98 ค่าที่​วัด​ได้ และ 0 ค่าที่​ขยับ​ระหว่าง​รอบ

สิ่ง​ที่​บท​นี้ ไม่​ได้ พิสูจน์​คือ​ลำดับ​ข้าง​ใน บท​นี้​ไม่​ได้​แสดง​ว่า​ตัว​ตัดสิน​ตรวจ​นิพจน์​ใบ​ไหน​ก่อน ข้อ​อ้าง​แบบ​นั้น​อ้าง​เอกสาร​ของ PostgreSQL 18 และ​เอกสาร​เป็น​คนละ​หลัก​ฐาน​กับ probe

ตัวเลข​ทุก​ตัว​ผูก​กับ​รุ่น​ที่​ระบุ​ไว้

ผล​ทั้งหมด​ใน​บท​นี้​วัด​บน PostgreSQL 18.6 บน aarch64-unknown-linux-musl ใน container ของ​คอร์ส​นี้​เอง major อื่น​เป็น​คนละ​การ​วัด และ​บท​นี้​ไม่​ได้​อ้าง​ถึง​มัน

หน่วย​ของ​หลัก​ฐาน​ใน​บท​นี้​มี​สาม​หน่วย คือ​รหัส SQLSTATE จำนวน​แถว​ที่​คำ​สั่ง​กระทำ​จริง และ​จำนวน​แถว​ที่​นับ​ได้​ใน​ตาราง ทั้ง​คอร์ส​ไม่มี​หน่วย​เวลา​สัก​ตัว

บท​นี้​เห็น​การ​ปฏิเสธ​ที่​ส่ง​เสียง​ทุก​ครั้ง และ​เสียง​นั้น​คือ​สิ่ง​ที่​ทำให้​มัน​ตรวจ​สอบ​ได้

ทั้ง​บท​มี​คำ​สั่ง​ที่​ถูก​ปฏิเสธ​สี่​ครั้ง และ​ทั้ง​สี่​ครั้ง​ได้ 42501 กลับ​มา คน​ที่​เขียน​แอป​จึง​จับ​ได้​ทันที​ว่า​คำ​สั่ง​นั้น​ไม่​ผ่าน แล้ว​ตัดสิน​ใจ​ต่อ​ได้

แต่ 42501 ไม่ใช่​รูป​เดียว​ของ​การ​ปฏิเสธ policy ใบ​หนึ่ง​ปฏิเสธ​คำ​สั่ง​ได้​โดย​ไม่​ยก​รหัส​อะไร​เลย คำ​สั่ง​จบ​ด้วย 00000 เหมือน​คำ​สั่ง​ที่​สำเร็จ ต่าง​กัน​แค่​จำนวน​แถว​ที่​มัน​กระทำ

บท​ที่ 4 วัด​เสียง​นั้น แล้ว​ยืนยัน​ด้วย​จำนวน​แถว​หลัง​จบ ว่า​คำ​สั่ง​ที่​เงียบ​สนิท ไม่​ได้​แปล​ว่า​คำ​สั่ง​ที่​ไม่มี​อะไร​ให้​ทำ


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • PostgreSQL 18 — CREATE POLICY (เข้า​ถึง 2026-09-06) — ตาราง Policies Applied by Command Type ที่​ระบุ​ว่า​นิพจน์​ของ policy ฝั่ง SELECT ถูก​ใช้​ทั้ง​คัด​แถว​เดิม​และ​ตรวจ​แถว​ใหม่​ของ UPDATE · กฎ​ที่ policy แบบ ALL ซึ่ง​มี​แต่ USING เอา​นิพจน์​นั้น​ไป​ใช้​ฝั่ง​เขียน​ด้วย · และ​กฎ​ที่​แถว​ซึ่ง RETURNING จะ​คืน ต้อง​ผ่าน policy ฝั่ง SELECT ก่อน

  • PostgreSQL 18 — Row Security Policies (เข้า​ถึง 2026-09-06) — ที่มา​ของ​กฎ​ว่า​เจ้าของ​ตาราง​อยู่​นอก policy ของ​ตัว​เอง เมื่อ​ตาราง​มี​แค่ ENABLE ซึ่ง​เป็น​เหตุผล​ที่​ยอด​ใน​บท​นี้​นับ​โดย​เจ้าของ​ตาราง

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/clinic-rls/spike.py ซึ่ง​สร้าง​ฐาน​ข้อมูล​กับ role ขึ้น​ใหม่​ทุกร​อบ แล้ว​รื้อ​ทิ้ง​เมื่อ​จบ · ต้อง​สั่ง scripts/clinic-rls/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55438

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

ตารางมี policy ใบเดียว `for all using (tenant_id = current_setting('app.tenant', true))` · app role ตั้ง `app.tenant` เป็น `clinic-a` แล้วสั่ง UPDATE ย้ายแถวของตัวเองไปเป็นของ `clinic-b` ได้อะไร