PRQL/prql · error · Error

expected a table or query, found an empty tuple

Error message

expected a table or query, found an empty tuple `{}`

What it means

Same validation as the empty-array case: when a relation argument is a bare empty tuple `{}` it has no lineage and cannot act as a table, so the resolver throws this specific error instead of bug #4317 (which is reserved for other lineage-less arguments).

Solutions

  1. Replace the empty tuple with a real relation (`from table` or `from_text ...`)
  2. Check the upstream expression producing the tuple — it is likely losing its columns
  3. Remove the empty tuple argument if it was accidental

Example fix

// before
from {}
// after
from employees
Defensive patterns

Strategy: validation

Validate before calling

function validateRelationArg(arg) {
  if (arg && typeof arg === "object" && !Array.isArray(arg) && Object.keys(arg).length === 0) {
    throw new Error("empty tuple {} is not a table; use from_text or a real table");
  }
}

Type guard

function isEmptyTupleLiteral(expr) {
  return expr && expr.kind === "tuple" && expr.items.length === 0;
}

Try / catch

try {
  compile(query);
} catch (e) {
  if (e.message.includes("an empty tuple")) {
    console.error("Tuple literals are not relations; source from a table or from_text.");
  } else { throw e; }
}

Prevention

When it happens

Trigger: `from {}` or passing an empty tuple literal `{}` to a function/transform expecting a table or query.

Common situations: Misusing tuple literals as data sources, or an intermediate variable that ended up an empty tuple being fed to `from`/`join`.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at prqlc/prqlc/src/semantic/resolver/functions.rs:280

                log::debug!("resolved arg to {}", arg.kind.as_ref());

                resolved_relations.push((index, arg, is_last));
            }

            // Then, add relation frames into scope
            for (index, arg, is_last) in resolved_relations {
                if partial_application_position.is_none() {
                    let frame = arg.lineage.as_ref().ok_or_else(|| {
                        // Provide helpful error for empty arrays/tuples used directly
                        // (not from functions like std.from_text which set lineage properly)
                        match &arg.kind {
                            ExprKind::Array(v) if v.is_empty() => Error::new(Reason::Expected {
                                who: None,
                                expected: "a table or query".to_string(),
                                found: "an empty array `[]`".to_string(),
                            })
                            .with_span(arg.span),
                            ExprKind::Tuple(v) if v.is_empty() => Error::new(Reason::Expected {
                                who: None,
                                expected: "a table or query".to_string(),
                                found: "an empty tuple `{}`".to_string(),
                            })
                            .with_span(arg.span),
                            _ => Error::new_bug(4317).with_span(closure.body.span),
                        }
                    })?;
                    if is_last {
                        self.root_mod.module.insert_frame(frame, NS_THIS);
                    } else {
                        self.root_mod.module.insert_frame(frame, NS_THAT);
                    }
                }

                closure.args[index] = arg;
            }
        }

View on GitHub (pinned to e164e249b9)