@@ -1311,3 +1311,262 @@ update t
13111311 │ └── c_new:15 + 1 [as=v_comp:16]
13121312 └── projections
13131313 └── (v_comp:16 > 0) AND (v_comp:16 > 5) [as=rls:17]
1314+
1315+ # Tests that verify FK maintenance operations are exempt from RLS checks on the
1316+ # referencing table.
1317+
1318+ exec-ddl
1319+ CREATE TABLE customers (
1320+ id INT PRIMARY KEY,
1321+ name TEXT
1322+ );
1323+ ----
1324+
1325+ exec-ddl
1326+ CREATE TABLE orders (
1327+ id INT PRIMARY KEY,
1328+ customer_id INT REFERENCES customers(id) ON UPDATE CASCADE ON DELETE SET NULL
1329+ );
1330+ ----
1331+
1332+ exec-ddl
1333+ ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
1334+ ----
1335+
1336+ # Validate that without any RLS policies, direct reads from orders are denied.
1337+ build
1338+ SELECT id, customer_id FROM orders
1339+ ----
1340+ project
1341+ ├── columns: id:1!null customer_id:2
1342+ └── select
1343+ ├── columns: id:1!null customer_id:2 crdb_internal_mvcc_timestamp:3 tableoid:4
1344+ ├── scan orders
1345+ │ └── columns: id:1!null customer_id:2 crdb_internal_mvcc_timestamp:3 tableoid:4
1346+ └── filters
1347+ └── false
1348+
1349+ # Update the customer ID. This should cascade the update to orders, bypassing
1350+ # RLS on orders.
1351+ build-post-queries
1352+ UPDATE customers SET id = 2 WHERE id = 1 RETURNING id, name
1353+ ----
1354+ root
1355+ ├── update customers
1356+ │ ├── columns: id:1!null name:2
1357+ │ ├── fetch columns: id:5 name:6
1358+ │ ├── update-mapping:
1359+ │ │ └── id_new:9 => id:1
1360+ │ ├── return-mapping:
1361+ │ │ ├── id_new:9 => id:1
1362+ │ │ └── name:6 => name:2
1363+ │ ├── input binding: &1
1364+ │ ├── cascades
1365+ │ │ └── orders_customer_id_fkey
1366+ │ └── project
1367+ │ ├── columns: id_new:9!null id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1368+ │ ├── select
1369+ │ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1370+ │ │ ├── scan customers
1371+ │ │ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1372+ │ │ │ └── flags: avoid-full-scan
1373+ │ │ └── filters
1374+ │ │ └── id:5 = 1
1375+ │ └── projections
1376+ │ └── 2 [as=id_new:9]
1377+ └── cascade
1378+ └── update orders
1379+ ├── columns: <none>
1380+ ├── fetch columns: orders.id:14 orders.customer_id:15
1381+ ├── update-mapping:
1382+ │ └── customer_id_new:19 => orders.customer_id:11
1383+ ├── check columns: rls:20
1384+ ├── input binding: &2
1385+ ├── project
1386+ │ ├── columns: rls:20!null orders.id:14!null orders.customer_id:15!null customer_id_old:18!null customer_id_new:19!null
1387+ │ ├── inner-join (hash)
1388+ │ │ ├── columns: orders.id:14!null orders.customer_id:15!null customer_id_old:18!null customer_id_new:19!null
1389+ │ │ ├── scan orders
1390+ │ │ │ ├── columns: orders.id:14!null orders.customer_id:15
1391+ │ │ │ └── flags: avoid-full-scan disabled not visible index feature
1392+ │ │ ├── select
1393+ │ │ │ ├── columns: customer_id_old:18!null customer_id_new:19!null
1394+ │ │ │ ├── with-scan &1
1395+ │ │ │ │ ├── columns: customer_id_old:18!null customer_id_new:19!null
1396+ │ │ │ │ └── mapping:
1397+ │ │ │ │ ├── customers.id:5 => customer_id_old:18
1398+ │ │ │ │ └── id_new:9 => customer_id_new:19
1399+ │ │ │ └── filters
1400+ │ │ │ └── customer_id_old:18 IS DISTINCT FROM customer_id_new:19
1401+ │ │ └── filters
1402+ │ │ └── orders.customer_id:15 = customer_id_old:18
1403+ │ └── projections
1404+ │ └── true [as=rls:20]
1405+ └── f-k-checks
1406+ └── f-k-checks-item: orders(customer_id) -> customers(id)
1407+ └── anti-join (hash)
1408+ ├── columns: customer_id:21!null
1409+ ├── with-scan &2
1410+ │ ├── columns: customer_id:21!null
1411+ │ └── mapping:
1412+ │ └── customer_id_new:19 => customer_id:21
1413+ ├── scan customers
1414+ │ ├── columns: customers.id:22!null
1415+ │ └── flags: avoid-full-scan disabled not visible index feature
1416+ └── filters
1417+ └── customer_id:21 = customers.id:22
1418+
1419+ # Delete the customer. This should set customer_id in orders to NULL, bypassing
1420+ # RLS on orders.
1421+ build-post-queries
1422+ DELETE FROM customers WHERE id = 2 RETURNING id, name
1423+ ----
1424+ root
1425+ ├── delete customers
1426+ │ ├── columns: id:1!null name:2
1427+ │ ├── fetch columns: id:5 name:6
1428+ │ ├── return-mapping:
1429+ │ │ ├── id:5 => id:1
1430+ │ │ └── name:6 => name:2
1431+ │ ├── input binding: &1
1432+ │ ├── cascades
1433+ │ │ └── orders_customer_id_fkey
1434+ │ └── select
1435+ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1436+ │ ├── scan customers
1437+ │ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1438+ │ │ └── flags: avoid-full-scan
1439+ │ └── filters
1440+ │ └── id:5 = 2
1441+ └── cascade
1442+ └── update orders
1443+ ├── columns: <none>
1444+ ├── fetch columns: orders.id:13 customer_id:14
1445+ ├── update-mapping:
1446+ │ └── customer_id_new:18 => customer_id:10
1447+ ├── check columns: rls:19
1448+ └── project
1449+ ├── columns: rls:19!null orders.id:13!null customer_id:14 customer_id_new:18
1450+ ├── project
1451+ │ ├── columns: customer_id_new:18 orders.id:13!null customer_id:14
1452+ │ ├── semi-join (hash)
1453+ │ │ ├── columns: orders.id:13!null customer_id:14
1454+ │ │ ├── scan orders
1455+ │ │ │ ├── columns: orders.id:13!null customer_id:14
1456+ │ │ │ └── flags: avoid-full-scan disabled not visible index feature
1457+ │ │ ├── with-scan &1
1458+ │ │ │ ├── columns: id:17!null
1459+ │ │ │ └── mapping:
1460+ │ │ │ └── customers.id:5 => id:17
1461+ │ │ └── filters
1462+ │ │ └── customer_id:14 = id:17
1463+ │ └── projections
1464+ │ └── NULL::INT8 [as=customer_id_new:18]
1465+ └── projections
1466+ └── true [as=rls:19]
1467+
1468+ # Repeat the operations but using different actions for ON UPDATE and ON DELETE
1469+ exec-ddl
1470+ DROP TABLE orders
1471+ ----
1472+
1473+ exec-ddl
1474+ CREATE TABLE orders (
1475+ id INT PRIMARY KEY,
1476+ customer_id INT REFERENCES customers(id) ON UPDATE SET NULL ON DELETE CASCADE
1477+ );
1478+ ----
1479+
1480+ exec-ddl
1481+ ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
1482+ ----
1483+
1484+ build-post-queries
1485+ UPDATE customers SET id = 2 WHERE id = 1 RETURNING id, name
1486+ ----
1487+ root
1488+ ├── update customers
1489+ │ ├── columns: id:1!null name:2
1490+ │ ├── fetch columns: id:5 name:6
1491+ │ ├── update-mapping:
1492+ │ │ └── id_new:9 => id:1
1493+ │ ├── return-mapping:
1494+ │ │ ├── id_new:9 => id:1
1495+ │ │ └── name:6 => name:2
1496+ │ ├── input binding: &1
1497+ │ ├── cascades
1498+ │ │ └── orders_customer_id_fkey
1499+ │ └── project
1500+ │ ├── columns: id_new:9!null id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1501+ │ ├── select
1502+ │ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1503+ │ │ ├── scan customers
1504+ │ │ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1505+ │ │ │ └── flags: avoid-full-scan
1506+ │ │ └── filters
1507+ │ │ └── id:5 = 1
1508+ │ └── projections
1509+ │ └── 2 [as=id_new:9]
1510+ └── cascade
1511+ └── update orders
1512+ ├── columns: <none>
1513+ ├── fetch columns: orders.id:14 customer_id:15
1514+ ├── update-mapping:
1515+ │ └── customer_id_new:20 => customer_id:11
1516+ ├── check columns: rls:21
1517+ └── project
1518+ ├── columns: rls:21!null orders.id:14!null customer_id:15!null customer_id_old:18!null customer_id_new:19!null customer_id_new:20
1519+ ├── project
1520+ │ ├── columns: customer_id_new:20 orders.id:14!null customer_id:15!null customer_id_old:18!null customer_id_new:19!null
1521+ │ ├── inner-join (hash)
1522+ │ │ ├── columns: orders.id:14!null customer_id:15!null customer_id_old:18!null customer_id_new:19!null
1523+ │ │ ├── scan orders
1524+ │ │ │ ├── columns: orders.id:14!null customer_id:15
1525+ │ │ │ └── flags: avoid-full-scan disabled not visible index feature
1526+ │ │ ├── select
1527+ │ │ │ ├── columns: customer_id_old:18!null customer_id_new:19!null
1528+ │ │ │ ├── with-scan &1
1529+ │ │ │ │ ├── columns: customer_id_old:18!null customer_id_new:19!null
1530+ │ │ │ │ └── mapping:
1531+ │ │ │ │ ├── customers.id:5 => customer_id_old:18
1532+ │ │ │ │ └── id_new:9 => customer_id_new:19
1533+ │ │ │ └── filters
1534+ │ │ │ └── customer_id_old:18 IS DISTINCT FROM customer_id_new:19
1535+ │ │ └── filters
1536+ │ │ └── customer_id:15 = customer_id_old:18
1537+ │ └── projections
1538+ │ └── NULL::INT8 [as=customer_id_new:20]
1539+ └── projections
1540+ └── true [as=rls:21]
1541+
1542+ build-post-queries
1543+ DELETE FROM customers WHERE id = 2 RETURNING id, name
1544+ ----
1545+ root
1546+ ├── delete customers
1547+ │ ├── columns: id:1!null name:2
1548+ │ ├── fetch columns: id:5 name:6
1549+ │ ├── return-mapping:
1550+ │ │ ├── id:5 => id:1
1551+ │ │ └── name:6 => name:2
1552+ │ ├── cascades
1553+ │ │ └── orders_customer_id_fkey
1554+ │ └── select
1555+ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1556+ │ ├── scan customers
1557+ │ │ ├── columns: id:5!null name:6 crdb_internal_mvcc_timestamp:7 tableoid:8
1558+ │ │ └── flags: avoid-full-scan
1559+ │ └── filters
1560+ │ └── id:5 = 2
1561+ └── cascade
1562+ └── delete orders
1563+ ├── columns: <none>
1564+ ├── fetch columns: orders.id:13 customer_id:14
1565+ └── select
1566+ ├── columns: orders.id:13!null customer_id:14!null
1567+ ├── scan orders
1568+ │ ├── columns: orders.id:13!null customer_id:14
1569+ │ └── flags: avoid-full-scan disabled not visible index feature
1570+ └── filters
1571+ ├── customer_id:14 = 2
1572+ └── customer_id:14 IS DISTINCT FROM CAST(NULL AS INT8)
0 commit comments