{"record":{"id":"1468e2ed7db71b03","repo":"prestodb/presto","slug":"invalid-row-filter","errorCode":"INVALID_ROW_FILTER","errorMessage":"Row filter for '%s' is recursive","messagePattern":"Row filter for '(.+?)' is recursive","errorType":"error_code","errorClass":"PrestoException","httpStatus":null,"severity":"error","filePath":"presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java","lineNumber":5336,"sourceCode":"            analyzeFiltersAndMasks(table, name, accessControlScope, columnsMetadata);\n        }\n\n        private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, Scope accessControlScope, List<ColumnMetadata> columnsMetadata)\n        {\n            Map<ColumnMetadata, ViewExpression> masks = accessControl.getColumnMasks(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), name, columnsMetadata);\n\n            for (Map.Entry<ColumnMetadata, ViewExpression> maskEntry : masks.entrySet()) {\n                analyzeColumnMask(session.getIdentity().getUser(), table, name, maskEntry.getKey(), accessControlScope, maskEntry.getValue());\n            }\n\n            accessControl.getRowFilters(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), name)\n                    .forEach(filter -> analyzeRowFilter(session.getIdentity().getUser(), table, name, accessControlScope, filter));\n        }\n\n        private void analyzeRowFilter(String currentIdentity, Table table, QualifiedObjectName name, Scope scope, ViewExpression filter)\n        {\n            if (analysis.hasRowFilter(name, currentIdentity)) {\n                throw new PrestoException(INVALID_ROW_FILTER, format(\"Row filter for '%s' is recursive\", name), null);\n            }\n\n            Expression expression;\n            try {\n                expression = sqlParser.createExpression(filter.getExpression(), createParsingOptions(session));\n            }\n            catch (ParsingException e) {\n                throw new PrestoException(INVALID_ROW_FILTER, format(\"Invalid row filter for '%s': %s\", name, e.getErrorMessage()), e);\n            }\n\n            analysis.registerTableForRowFiltering(name, currentIdentity);\n            ExpressionAnalysis expressionAnalysis;\n            try {\n                expressionAnalysis = ExpressionAnalyzer.analyzeExpression(\n                        createViewSession(filter.getCatalog(), filter.getSchema(), new Identity(filter.getIdentity(), Optional.empty())), // TODO: path should be included in row filter\n                        metadata,\n                        accessControl,\n                        sqlParser,","sourceCodeStart":5318,"sourceCodeEnd":5354,"githubUrl":"https://github.com/prestodb/presto/blob/55bb57d202de3b926896fa966c2c4a44c779634e/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java#L5318-L5354","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: filter on orders queries customers, and customers' filter queries orders\nSELECT * FROM customers WHERE ...  -- in orders filter\n// after: make orders filter reference an unfiltered table or literals\nregion IN ('US','EU')","handlingStrategy":"validation","validationCode":"// Before installing a row filter, ensure it doesn't reference filtered tables:\nSet<QualifiedObjectName> deps = extractTableReferences(parse(filterExpr));\nif (deps.contains(tableName)) {\n    throw new IllegalStateException(\"Row filter for \" + tableName + \" must not reference itself\");\n}","typeGuard":null,"tryCatchPattern":"try { session.execute(\"SELECT * FROM \" + table); }\ncatch (PrestoException e) {\n    if (\"INVALID_ROW_FILTER\".equals(e.getErrorCode().getName()) && e.getMessage().contains(\"recursive\")) {\n        // disable the cycling filter pair and redefine one of them\n    } else { throw e; }\n}","preventionTips":["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"],"tags":["security","row-filter","recursion","access-control"],"backgroundTag":"row-filter-recursive","analyzedSha":"55bb57d202de3b926896fa966c2c4a44c779634e","analyzedAt":"2026-09-04T12:50:26.162Z","contentChangedAt":"2026-09-04T12:50:26.162Z","schemaVersion":2},"datasetVersion":"2026-09-11T21:17:09.523Z"}