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

  1. Rewrite the row filter so it does not query tables that (transitively) apply row filters for the same identity
  2. Break the cycle: make one of the filters read from an unfiltered staging/table or use a constant/literal predicate
  3. 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

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


AI-assisted analysis of prestodb/presto@55bb57d202 (2026-09-04). Data as JSON: /api/errors/1468e2ed7db71b03. Report an issue: GitHub.