PRQL/prql · error · Error

` `: expected relation, found

Error message

`{name}`: expected relation, found {found}

What it means

When `append ... by:name` is compiled natively in PRQL, both inputs (`top` and `bottom` relations) must have resolvable lineage, i.e. PRQL must know their concrete columns. This error is thrown when one input's lineage is missing (its columns cannot be determined), so a by-name union cannot be computed.

Solutions

  1. Ensure both sides of `append ... by:name` are full pipelines starting from a relation (e.g. `from x | select [...]`).
  2. Add an explicit `select` to materialize columns on the offending input.
  3. If you don't need by-name matching, use plain `append` (positional), which has weaker column requirements.
  4. Check the input isn't wrapped in a function that returns a non-relation value.

Example fix

// before
from t | append (1 + 1) by:name
// after
from t | append (from other | select [a, b]) by:name
Defensive patterns

Strategy: validation

Validate before calling

// Ensure both append inputs are pipelines that start with `from`
function validateByNameAppend(prql) {
  const parts = prql.split(/\|\s*append\s+/);
  if (parts.length > 1 && /by:name/.test(prql)) {
    for (const p of parts) {
      if (!/(^|\(|\s)from\s/.test(p)) {
        throw new Error('both inputs of append by:name must be table pipelines (from ...)');
      }
    }
  }
}

Type guard

function isTablePipeline(expr) {
  return typeof expr === 'string' && /(^|[\s(])from\s+\w/.test(expr);
}

Try / catch

try {
  const sql = prqlc.compile(query);
} catch (e) {
  if (e.message.includes(': expected relation, found')) {
    // log which input lacks lineage; rewrite it as a from-pipeline
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `append other by:name` where either the top or bottom relation has no lineage — typically when the input is not a table-producing pipeline (e.g. the result of certain function calls, literals, or expressions that do not carry relation/column info).

Common situations: Appending a non-table expression; passing a scalar or literal to `append by:name`; using append on something produced by a function returning an unresolved relation; intermediate pipelines that discard column information.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of PRQL/prql@e164e249b9 (2026-09-09). Data as JSON: /api/errors/42c6f699d70f77ea. Report an issue: GitHub.

Appendix: source

Thrown at prqlc/prqlc/src/semantic/resolver/transforms.rs:268

                        "\"position\"" => false,
                        "\"name\"" => true,
                        _ => {
                            return Err(Error::new(Reason::Expected {
                                who: Some("`by`".to_string()),
                                expected: "position or name".to_string(),
                                found: ident.to_string(),
                            })
                            .with_span(span))
                        }
                    }
                };

                // TODO: support database engine-level UNION ALL BY NAME in PR #6037
                if by_name {
                    // input validation for PRQL-native implementation
                    for (name, rel) in [("top", top.clone()), ("bottom", bottom.clone())] {
                        let lineage = rel.lineage.clone().ok_or_else(|| {
                            Error::new(Reason::Expected {
                                who: Some(format!("`{name}`")),
                                expected: "relation".to_string(),
                                found: write_pl(rel.clone()),
                            })
                            .with_span(rel.span)
                        })?;

                        lineage.columns.iter().try_for_each(|c| match c {
                            LineageColumn::All { .. } => Err(Error::new(Reason::Simple(format!(
                                "{name} input to append by:name must have all columns defined"
                            )))
                            .push_hint("try adding a select earlier in the pipeline")
                            .with_span(rel.span)),
                            LineageColumn::Single {
                                name: None,
                                target_name: None,
                                ..
                            } => Err(Error::new(Reason::Simple(format!(

View on GitHub (pinned to e164e249b9)