{"record":{"id":"befda0e3d7ae5c74","repo":"clockworklabs/SpacetimeDB","slug":"cannot-semijoin-a-table-to-itself","errorCode":null,"errorMessage":"Cannot semijoin a table to itself","messagePattern":"Cannot semijoin a table to itself","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/bindings-typescript/src/lib/query.ts","lineNumber":117,"sourceCode":"      predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>\n    ): SemijoinBuilder<TableDef>;\n    /** @deprecated No longer needed — builder is already a valid query. */\n    build(): Query<TableDef>;\n  }>;\n\nclass SemijoinImpl<TableDef extends TypedTableDef>\n  implements SemijoinBuilder<TableDef>, TableTypedQuery<TableDef>\n{\n  readonly [QueryBrand] = true;\n  readonly type = 'semijoin' as const;\n  constructor(\n    readonly sourceQuery: FromBuilder<TableDef>,\n    readonly filterQuery: FromBuilder<any>,\n    readonly joinCondition: BooleanExpr<any>\n  ) {\n    if (sourceQuery.table.sourceName === filterQuery.table.sourceName) {\n      // TODO: Handle aliasing properly instead of just forbidding it.\n      throw new Error('Cannot semijoin a table to itself');\n    }\n  }\n\n  build(): Query<TableDef> {\n    return this as Query<TableDef>;\n  }\n\n  where(\n    predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>\n  ): SemijoinImpl<TableDef> {\n    const nextSourceQuery = this.sourceQuery.where(predicate);\n    return new SemijoinImpl<TableDef>(\n      nextSourceQuery,\n      this.filterQuery,\n      this.joinCondition\n    );\n  }\n","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings-typescript/src/lib/query.ts#L99-L135","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ntables.employees.leftSemijoin(tables.employees, (l, r) => l.managerId.eq(r.id)); // throws\n\n// after\ntables.employees.leftSemijoin(tables.managers, (l, r) => l.managerId.eq(r.id));","handlingStrategy":"validation","validationCode":"const sourceNameOf = (t: { table?: { sourceName?: string }; sourceName?: string }): string | undefined =>\n  t.table?.sourceName ?? t.sourceName;\n\nfunction canSemijoin(left: any, right: any): boolean {\n  return sourceNameOf(left) !== sourceNameOf(right);\n}\n// if (!canSemijoin(tables.a, tables.b)) throw new Error('self-semijoin unsupported');","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["query","spacetimedb","semijoin","sql","self-join"],"backgroundTag":"self-join-unsupported","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}