{"record":{"id":"23194f0670be223f","repo":"prestodb/presto","slug":"invalid-column-mask-23194f","errorCode":"INVALID_COLUMN_MASK","errorMessage":"Column mask for '%s.%s' is recursive","messagePattern":"Column mask 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":5387,"sourceCode":"            analysis.recordSubqueries(expression, expressionAnalysis);\n\n            Type actualType = expressionAnalysis.getType(expression);\n            if (!actualType.equals(BOOLEAN)) {\n                if (!metadata.getFunctionAndTypeManager().canCoerce(actualType, BOOLEAN)) {\n                    throw new PrestoException(DATATYPE_MISMATCH, format(\"Expected row filter for '%s' to be of type BOOLEAN, but was %s\", name, actualType), null);\n                }\n\n                analysis.addCoercion(expression, BOOLEAN, false);\n            }\n\n            analysis.addRowFilter(table, expression);\n        }\n\n        private void analyzeColumnMask(String currentIdentity, Table table, QualifiedObjectName tableName, ColumnMetadata columnMetadata, Scope scope, ViewExpression mask)\n        {\n            String column = columnMetadata.getName();\n            if (analysis.hasColumnMask(tableName, column, currentIdentity)) {\n                throw new PrestoException(INVALID_COLUMN_MASK, format(\"Column mask for '%s.%s' is recursive\", tableName, column), null);\n            }\n\n            Expression expression;\n            try {\n                expression = sqlParser.createExpression(mask.getExpression(), createParsingOptions(session));\n            }\n            catch (ParsingException e) {\n                throw new PrestoException(INVALID_COLUMN_MASK, format(\"Invalid column mask for '%s.%s': %s\", tableName, column, e.getErrorMessage()), e);\n            }\n\n            ExpressionAnalysis expressionAnalysis;\n            analysis.registerTableForColumnMasking(tableName, column, currentIdentity);\n            try {\n                expressionAnalysis = ExpressionAnalyzer.analyzeExpression(\n                        createViewSession(mask.getCatalog(), mask.getSchema(), new Identity(mask.getIdentity(), Optional.empty())), // TODO: path should be included in row filter\n                        metadata,\n                        accessControl,\n                        sqlParser,","sourceCodeStart":5369,"sourceCodeEnd":5405,"githubUrl":"https://github.com/prestodb/presto/blob/55bb57d202de3b926896fa966c2c4a44c779634e/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java#L5369-L5405","documentation":"Column masks, like row filters, can reference other columns/tables and may nest. The analyzer tracks registered masks per (table, column, identity) and throws INVALID_COLUMN_MASK if applying a mask would re-enter the same mask, preventing infinite recursion during analysis.","triggerScenarios":"A column mask expression on table A column c reads from a table (or same table/column) whose mask chain leads back to A.c under the same identity; thrown at the start of analyzeColumnMask when analysis.hasColumnMask(tableName, column, currentIdentity) is true.","commonSituations":"Masks defined with subqueries over masked tables; a mask on a column that the mask expression itself selects; stacked policies across views and base tables creating a cycle.","solutions":["Rewrite the mask so its expression does not read the same masked column/table transitively","Break the cycle by having the mask use literals, session functions (e.g. current_user()), or an unmasked lookup table","Restructure policies so view-level and table-level masks don't reference each other"],"exampleFix":"// before: mask on t.ssn contains SELECT ssn FROM t WHERE ...\n(SELECT ssn FROM t WHERE id = current_user())\n// after\nCASE WHEN is_admin(current_user()) THEN ssn ELSE '***' END","handlingStrategy":"validation","validationCode":"// Ensure the mask expression does not reference the masked column/table itself:\nSet<QualifiedObjectName> refs = extractTableReferences(parse(maskExpr));\nif (refs.contains(tableName)) {\n    throw new IllegalStateException(\"Column mask must not reference \" + tableName);\n}","typeGuard":null,"tryCatchPattern":"try { session.execute(\"SELECT masked_col FROM \" + table); }\ncatch (PrestoException e) {\n    if (\"INVALID_COLUMN_MASK\".equals(e.getErrorCode().getName())) {\n        // locate the cycling mask and break the reference chain\n    } else { throw e; }\n}","preventionTips":["Design masks to use session functions/literals, not subqueries over masked tables","Map mask dependencies before stacking policies on views over base tables","Review masking policies as a dependency graph in code review"],"tags":["security","column-mask","recursion","access-control"],"backgroundTag":"column-mask-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"}