Enforcement

When federiq query or federiq serve sees a SQL statement against a table with active policies, it parses the SQL with sqlparser-rs, applies each policy as an AST transformation, and emits the rewritten SQL for DuckDB to execute. The rewriter is schema-aware: it walks the full AST (CTEs, subqueries, set operations, JOINs) and uses the attached engine to resolve column lists for wildcard expansion.

Row filters

-- User input
SELECT id FROM events WHERE user_id = 42;

-- After policy `row_filter: "status = 'active'"`
SELECT id FROM events WHERE user_id = 42 AND status = 'active';

Column masking

-- User input
SELECT name, email FROM users;

-- After policy `mask_columns: { email: "'***@***.com'" }`
SELECT name, '***@***.com' AS email FROM users;

The masking expression is valid SQL. The column is aliased back to its original name so callers don't see a schema change.

SELECT * is expanded automatically

The schema-aware rewriter resolves the column list for each table in scope and expands the wildcard explicitly, applying masks per-column.

-- User input
SELECT * FROM users;            -- columns: id, name, email, created_at

-- After policy `mask_columns: { email: "'***@***.com'" }`
SELECT id, name, '***@***.com' AS email, created_at FROM users;

t.* (qualified wildcard) expands only that table's columns, even inside a JOIN.

JOINs are walked, not rejected

JOINs across the federation seam work under the default fail-closed policy, with masks applied per source:

-- pg.public.users has mask_pii on email
SELECT u.name, u.email, e.event_type
FROM pg.public.users u
JOIN events e ON u.id = e.user_id;

-- becomes
SELECT u.name, '***@***.com' AS email, e.event_type
FROM pg.public.users u
JOIN events e ON u.id = e.user_id;

CTEs, UNIONs, subqueries

The rewriter recurses into all of these:

WITH active AS (SELECT email FROM pg.public.users)
SELECT email FROM active;

-- Mask applies inside the CTE body — not after.
WITH active AS (SELECT '***@***.com' AS email FROM pg.public.users)
SELECT email FROM active;

UNION / INTERSECT / EXCEPT walk each branch independently. Subqueries in WHERE (IN (SELECT …), EXISTS (SELECT …)) get the same treatment.

Ambiguous columns

If an unqualified column name matches a mask in multiple tables in scope, the rewriter refuses with a structured error and asks you to qualify:

could not resolve which table column 'email' belongs to
(multiple tables in scope: ["u", "archive"]);
qualify the column reference (e.g. `t.email`)

Region pinning

Region mismatches fail the rewrite:

policy 'pin_to_us_east' requires region='us-east-1'
but current context has eu-west-1

Static checking

Before running a query, preview violations:

federiq policy check "SELECT email, ssn FROM users"

Disabling enforcement

Policies auto-apply when the catalog declares any. To opt out on a per-call basis:

federiq query --enforce-policy=false "SELECT ..."

Use sparingly — this bypass is primarily for development and debugging.