clockworklabs/SpacetimeDB · error · Error
Cannot semijoin a table to itself
Error message
Cannot semijoin a table to itself
What it means
leftSemijoin/rightSemijoin build a SemijoinImpl whose constructor compares sourceQuery.table.sourceName with filterQuery.table.sourceName and throws when both sides resolve to the same table. The generated SQL has no table aliasing yet (the code carries a TODO to handle aliasing properly), so a self-semijoin cannot be rendered unambiguously.
Source
Thrown at crates/bindings-typescript/src/lib/query.ts:117
predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
): SemijoinBuilder<TableDef>;
/** @deprecated No longer needed — builder is already a valid query. */
build(): Query<TableDef>;
}>;
class SemijoinImpl<TableDef extends TypedTableDef>
implements SemijoinBuilder<TableDef>, TableTypedQuery<TableDef>
{
readonly [QueryBrand] = true;
readonly type = 'semijoin' as const;
constructor(
readonly sourceQuery: FromBuilder<TableDef>,
readonly filterQuery: FromBuilder<any>,
readonly joinCondition: BooleanExpr<any>
) {
if (sourceQuery.table.sourceName === filterQuery.table.sourceName) {
// TODO: Handle aliasing properly instead of just forbidding it.
throw new Error('Cannot semijoin a table to itself');
}
}
build(): Query<TableDef> {
return this as Query<TableDef>;
}
where(
predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
): SemijoinImpl<TableDef> {
const nextSourceQuery = this.sourceQuery.where(predicate);
return new SemijoinImpl<TableDef>(
nextSourceQuery,
this.filterQuery,
this.joinCondition
);
}
View on GitHub (pinned to 524b4487d9)
Solutions
- Use two different tables for the source and filter sides
- Restructure the query (e.g. plain .where(...) plus client-side matching) until aliasing is supported
- Track the upstream aliasing TODO before attempting self-joins
Example fix
// before tables.employees.leftSemijoin(tables.employees, (l, r) => l.managerId.eq(r.id)); // throws // after tables.employees.leftSemijoin(tables.managers, (l, r) => l.managerId.eq(r.id));
Defensive patterns
Strategy: validation
Validate before calling
const sourceNameOf = (t: { table?: { sourceName?: string }; sourceName?: string }): string | undefined =>
t.table?.sourceName ?? t.sourceName;
function canSemijoin(left: any, right: any): boolean {
return sourceNameOf(left) !== sourceNameOf(right);
}
// if (!canSemijoin(tables.a, tables.b)) throw new Error('self-semijoin unsupported'); Prevention
- Check both sides' table sourceNames before building a semijoin
- Watch the upstream aliasing TODO - self-joins will stay broken until it lands
- Model two roles of one entity as separate tables if you need join-like behavior today
When it happens
Trigger: tables.users.leftSemijoin(tables.users, (l, r) => l.managerId.eq(r.id)) - the same TableRef (or two refs with the same sourceName) passed as both the source and the filter side.
Common situations: Modeling hierarchies (employee to manager) or dedup patterns on a single table; copy-pasting a working semijoin and forgetting to change one side's table.
Related errors
- Too many positional arguments for interactive mode. Usage: s
- Multiple databases found in config: {}. Please specify which
- Estimated cardinality ({estimate} rows) exceeds limit ({limi
- no such module
- Unsupported predicate type '{value.GetType().Name}'. Expecte
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/befda0e3d7ae5c74.
Report an issue: GitHub.