prestodb/presto · error · PrestoException
INVALID_ROW_FILTER
INVALID_ROW_FILTER
Error message
Row filter for '%s' is recursive
What it means
Row filters (fine-grained access control) can themselves reference tables that have row filters. To prevent infinite recursion, the analyzer records filters per (table, identity); if analyzing a filter re-enters the same table's filter for the same identity, it throws INVALID_ROW_FILTER with this message.
Source
Thrown at presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java:5336
analyzeFiltersAndMasks(table, name, accessControlScope, columnsMetadata);
}
private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, Scope accessControlScope, List<ColumnMetadata> columnsMetadata)
{
Map<ColumnMetadata, ViewExpression> masks = accessControl.getColumnMasks(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), name, columnsMetadata);
for (Map.Entry<ColumnMetadata, ViewExpression> maskEntry : masks.entrySet()) {
analyzeColumnMask(session.getIdentity().getUser(), table, name, maskEntry.getKey(), accessControlScope, maskEntry.getValue());
}
accessControl.getRowFilters(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), name)
.forEach(filter -> analyzeRowFilter(session.getIdentity().getUser(), table, name, accessControlScope, filter));
}
private void analyzeRowFilter(String currentIdentity, Table table, QualifiedObjectName name, Scope scope, ViewExpression filter)
{
if (analysis.hasRowFilter(name, currentIdentity)) {
throw new PrestoException(INVALID_ROW_FILTER, format("Row filter for '%s' is recursive", name), null);
}
Expression expression;
try {
expression = sqlParser.createExpression(filter.getExpression(), createParsingOptions(session));
}
catch (ParsingException e) {
throw new PrestoException(INVALID_ROW_FILTER, format("Invalid row filter for '%s': %s", name, e.getErrorMessage()), e);
}
analysis.registerTableForRowFiltering(name, currentIdentity);
ExpressionAnalysis expressionAnalysis;
try {
expressionAnalysis = ExpressionAnalyzer.analyzeExpression(
createViewSession(filter.getCatalog(), filter.getSchema(), new Identity(filter.getIdentity(), Optional.empty())), // TODO: path should be included in row filter
metadata,
accessControl,
sqlParser,View on GitHub (pinned to 55bb57d202)
Solutions
- Rewrite the row filter so it does not query tables that (transitively) apply row filters for the same identity
- Break the cycle: make one of the filters read from an unfiltered staging/table or use a constant/literal predicate
- Inline the needed lookup values into the filter expression instead of joining the filtered table
Example fix
// before: filter on orders queries customers, and customers' filter queries orders
SELECT * FROM customers WHERE ... -- in orders filter
// after: make orders filter reference an unfiltered table or literals
region IN ('US','EU') Defensive patterns
Strategy: validation
Validate before calling
// Before installing a row filter, ensure it doesn't reference filtered tables:
Set<QualifiedObjectName> deps = extractTableReferences(parse(filterExpr));
if (deps.contains(tableName)) {
throw new IllegalStateException("Row filter for " + tableName + " must not reference itself");
} Try / catch
try { session.execute("SELECT * FROM " + table); }
catch (PrestoException e) {
if ("INVALID_ROW_FILTER".equals(e.getErrorCode().getName()) && e.getMessage().contains("recursive")) {
// disable the cycling filter pair and redefine one of them
} else { throw e; }
} Prevention
- Review row filter dependencies as a graph before deploying policies
- Never write filters that query the table being filtered
- Use literal/session-function-based filters instead of subqueries
When it happens
Trigger: A row filter expression on table A selects from table B whose row filter selects from A (direct or indirect cycle), under the same user identity; thrown at the start of analyzeRowFilter when analysis.hasRowFilter(name, currentIdentity) is true.
Common situations: Security administrators stacking row filters across related tables without noticing the cycle; a filter defined on a view's base table that queries the view; self-referencing filter where the filter SQL itself queries the filtered table.
Related errors
- INVALID_COLUMN_MASK
- PERMISSION_DENIED
- INVALID_COLUMN_MASK
- DATATYPE_MISMATCH
- Full data access is restricted by row filters and column mas
AI-assisted analysis of prestodb/presto@55bb57d202 (2026-09-04).
Data as JSON: /api/errors/1468e2ed7db71b03.
Report an issue: GitHub.