Estado: ✅ Aceptada Fecha: 2026-05-29 Bloque: Z3b (follow-up de Z3 — write-side de RLS para INSERT) Bump on-disk: VERSION 26 → 27

🧭 Contexto

Z3 (ADR-0052) entregó RLS para SELECT/UPDATE/DELETE vía rewriting del WHERE con el OR de los predicados USING. INSERT quedó explícitamente deferido porque requiere semántica distinta: el predicado se evalúa contra la fila a insertar (no contra filas existentes), y se llama WITH CHECK en lugar de USING.

Z3b cubre ese hueco: FOR INSERT permitido en CREATE POLICY, cláusula WITH CHECK (expr) opcional, y enforcement en exec_insert antes de persistir. Mantiene compat 100% con DBs Z3.

💡 Decisión

1. PolicyMeta extendido con with_check_sql: Option<String>

Layout on-disk: [..campos Z3..][with_check_flag:u8][with_check_sql opcional]. Cuando es None, sólo se serializa el flag byte 0. Cuando es Some, flag 1 + string length-prefixed.

pub struct PolicyMeta {
    pub name: String,
    pub table: String,
    pub action: u8,
    pub roles: Vec<String>,
    pub using_sql: String,           // puede ser "" para FOR INSERT
    pub with_check_sql: Option<String>,  // opcional
}

Nueva constante: POLICY_ACTION_INSERT = 2.

2. Sintaxis DDL

CREATE POLICY name ON table FOR {ALL|SELECT|INSERT|UPDATE|DELETE} [TO role,...]
    [USING (expr)] [WITH CHECK (expr)]

Validaciones del parser:

3. Semántica de enforcement

Action al ejecutar Policies con action ∈ {INSERT, ALL} aplicables al user/role Predicado evaluado
INSERT Si tiene with_check_sql → ese. Sino, policy no aplica (skip).
INSERT Sí, pero ninguna tiene WITH CHECK Deny (todas las policies aplicables son skip → no “passed any”)
INSERT No (no hay policies para INSERT en la tabla) Bypass (compat — si no hay policies relevantes, RLS no aplica)

OR semantics: si al menos una policy aplicable evalúa WITH CHECK como TRUE para la fila, el INSERT pasa. Mismo PERMISSIVE de PG.

Edge cases importantes:

4. UPDATE post-image WITH CHECK — diferido

PostgreSQL chequea WITH CHECK contra la fila post-update (asegura que la modificación no saque la fila del scope visible). Z3b no implementa este chequeo — la lógica de Z3 USING en exec_update ya filtra qué filas se modifican, lo cual es la primera capa de defensa. El post-image check queda como defer Z3c. La razón: hookear post-image en exec_update requiere intervenir en el bucle per-row interno (post-aplicación de assignments, pre-persist), que toca el flujo principal de UPDATE — mejor en su propio bloque.

5. Códigos de error

Código Nombre Caso
4138 POLICY_CHECK_VIOLATION INSERT (o UPDATE futuro) viola WITH CHECK de todas las policies aplicables

📁 Archivos tocados

⛔ Lo que no entra en Z3b (defer)

Ítem Razón del defer
UPDATE post-image WITH CHECK Z3 USING ya gatea qué filas se tocan. Post-image check requiere hookear el bucle interno de UPDATE — defer Z3c.
DEFAULT applied antes del check Si el INSERT no setea una columna, hoy es Value::Null en el check_row (el DEFAULT se aplica después en apply_insert_row_with_conflict). Para policies que referencian col DEFAULT 'x', el WITH CHECK no verá 'x' sino NULL. Limitación documentada.
INSERT ... ON CONFLICT DO UPDATE con WITH CHECK del UPDATE path El INSERT path tira WITH CHECK; si cae al UPDATE path por ON CONFLICT, no se hace otro WITH CHECK. Z3c lo cubre cuando llegue el UPDATE post-image.
INSERT ... RETURNING filtrado por SELECT policies El RETURNING expone columnas del row recién insertado. Hoy no se filtra contra policies SELECT. Defer.

🧪 Tests

11 tests z3b_*:

Suite total: 662 passing (651 → +11 Z3b).

🔗 Referencias