prestodb/presto · error · SemanticException

DUPLICATE_COLUMN_NAME

DUPLICATE_COLUMN_NAME

Error message

Duplicate field name '%s' in ROW

What it means

visitRow rejects a ROW type literal whose field names repeat. Duplicates are disallowed because a row type with repeated field names cannot be round-tripped through its TypeSignature, which the native worker relies on. The check uses the delimited or lowercased identifier value when comparing names.

Solutions

  1. Rename the duplicated field in the ROW type definition
  2. Unquote deliberately-cased names so they differ, or change casing so the lowercased names differ
  3. Regenerate the SQL/schema so column aliases are unique

Example fix

// before
CAST(ROW(1, 'x') AS ROW(a INTEGER, a VARCHAR))
// after
CAST(ROW(1, 'x') AS ROW(a INTEGER, b VARCHAR))
Defensive patterns

Strategy: validation

Validate before calling

Set<String> names = rowFields.stream()
    .map(f -> f.getName().toLowerCase())
    .collect(Collectors.toSet());
if (names.size() != rowFields.size()) throw new IllegalArgumentException("Duplicate ROW field names");

Try / catch

try {
    return session.execute(sql);
} catch (SemanticException e) {
    if (e.getCode() == DUPLICATE_COLUMN_NAME) {
        // auto-rename or report to user with offending field
    } else throw e;
}

Prevention

When it happens

Trigger: Writing a ROW literal or CAST to ROW with two fields sharing the same name, e.g. CAST(ROW(1,2) AS ROW(a INTEGER, a VARCHAR)) or ROW('x' AS k, 'y' AS k).

Common situations: Hand-written SQL with copy-pasted field lists, generated SQL from ORMs producing duplicate aliases, schema evolution tools emitting a ROW type with duplicated column names.

Related errors


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

Appendix: source

Thrown at presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalyzer.java:447

            Set<String> declaredNames = new HashSet<>();
            for (Row.Field field : node.getFields()) {
                Type fieldType = process(field.getExpression(), context);
                Optional<Identifier> name = field.getName();
                if (!name.isPresent()) {
                    fields.add(RowType.field(fieldType));
                    continue;
                }
                // Fold the name the same way CAST(... AS ROW(...)) does. Cast.transformCase
                // lowercases everything outside double quotes, so an undelimited name is folded to
                // lower case and a delimited one is kept verbatim. Doing the same here keeps
                // ROW(1 AS Abc) and CAST(ROW(1) AS ROW(Abc integer)) producing the same type.
                // Note this is deliberately not getCanonicalValue(), which upper cases instead.
                Identifier identifier = name.get();
                String fieldName = identifier.isDelimited() ? identifier.getValue() : identifier.getValueLowerCase();
                // Duplicates are rejected because a row type with repeated field names cannot be
                // round-tripped through its TypeSignature, which the native worker relies on.
                if (!declaredNames.add(fieldName)) {
                    throw new SemanticException(DUPLICATE_COLUMN_NAME, node, "Duplicate field name '%s' in ROW", fieldName);
                }
                fields.add(RowType.field(fieldName, fieldType, identifier.isDelimited()));
            }

            return setExpressionType(node, RowType.from(fields.build()));
        }

        @Override
        protected Type visitCurrentTime(CurrentTime node, StackableAstVisitorContext<Context> context)
        {
            if (node.getPrecision() != null) {
                throw new SemanticException(NOT_SUPPORTED, node, "non-default precision not yet supported");
            }

            Type type;
            switch (node.getFunction()) {
                case DATE:
                    type = DATE;

View on GitHub (pinned to 55bb57d202)