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

  1. Use two different tables for the source and filter sides
  2. Restructure the query (e.g. plain .where(...) plus client-side matching) until aliasing is supported
  3. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/befda0e3d7ae5c74. Report an issue: GitHub.