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.