prestodb/presto · error · PrestoException

INVALID_COLUMN_MASK

INVALID_COLUMN_MASK

Error message

Column mask for '%s.%s' is recursive

What it means

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.

Source

Thrown at presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java:5387

            analysis.recordSubqueries(expression, expressionAnalysis);

            Type actualType = expressionAnalysis.getType(expression);
            if (!actualType.equals(BOOLEAN)) {
                if (!metadata.getFunctionAndTypeManager().canCoerce(actualType, BOOLEAN)) {
                    throw new PrestoException(DATATYPE_MISMATCH, format("Expected row filter for '%s' to be of type BOOLEAN, but was %s", name, actualType), null);
                }

                analysis.addCoercion(expression, BOOLEAN, false);
            }

            analysis.addRowFilter(table, expression);
        }

        private void analyzeColumnMask(String currentIdentity, Table table, QualifiedObjectName tableName, ColumnMetadata columnMetadata, Scope scope, ViewExpression mask)
        {
            String column = columnMetadata.getName();
            if (analysis.hasColumnMask(tableName, column, currentIdentity)) {
                throw new PrestoException(INVALID_COLUMN_MASK, format("Column mask for '%s.%s' is recursive", tableName, column), null);
            }

            Expression expression;
            try {
                expression = sqlParser.createExpression(mask.getExpression(), createParsingOptions(session));
            }
            catch (ParsingException e) {
                throw new PrestoException(INVALID_COLUMN_MASK, format("Invalid column mask for '%s.%s': %s", tableName, column, e.getErrorMessage()), e);
            }

            ExpressionAnalysis expressionAnalysis;
            analysis.registerTableForColumnMasking(tableName, column, currentIdentity);
            try {
                expressionAnalysis = ExpressionAnalyzer.analyzeExpression(
                        createViewSession(mask.getCatalog(), mask.getSchema(), new Identity(mask.getIdentity(), Optional.empty())), // TODO: path should be included in row filter
                        metadata,
                        accessControl,
                        sqlParser,

View on GitHub (pinned to 55bb57d202)

Solutions

  1. Rewrite the mask so its expression does not read the same masked column/table transitively
  2. Break the cycle by having the mask use literals, session functions (e.g. current_user()), or an unmasked lookup table
  3. Restructure policies so view-level and table-level masks don't reference each other

Example fix

// before: mask on t.ssn contains SELECT ssn FROM t WHERE ...
(SELECT ssn FROM t WHERE id = current_user())
// after
CASE WHEN is_admin(current_user()) THEN ssn ELSE '***' END
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the mask expression does not reference the masked column/table itself:
Set<QualifiedObjectName> refs = extractTableReferences(parse(maskExpr));
if (refs.contains(tableName)) {
    throw new IllegalStateException("Column mask must not reference " + tableName);
}

Try / catch

try { session.execute("SELECT masked_col FROM " + table); }
catch (PrestoException e) {
    if ("INVALID_COLUMN_MASK".equals(e.getErrorCode().getName())) {
        // locate the cycling mask and break the reference chain
    } else { throw e; }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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