{"record":{"id":"6ad48de3fc8a493c","repo":"clockworklabs/SpacetimeDB","slug":"invalid-number-of-tables-in-subscription","errorCode":null,"errorMessage":"Invalid number of tables in subscription: {}","messagePattern":"Invalid number of tables in subscription: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/subscription/src/lib.rs","lineNumber":236,"sourceCode":"                    )?,\n                    new_plan(\n                        // R'ds(-)\n                        plan,\n                        &[(*ds, Delta::Deletes)],\n                    )?,\n                    new_plan(\n                        // dr(+)ds(+)\n                        plan,\n                        &[(*dr, Delta::Inserts), (*ds, Delta::Inserts)],\n                    )?,\n                    new_plan(\n                        // dr(-)ds(-)\n                        plan,\n                        &[(*dr, Delta::Deletes), (*ds, Delta::Deletes)],\n                    )?,\n                ],\n            }),\n            _ => bail!(\"Invalid number of tables in subscription: {}\", tables.len()),\n        }\n    }\n}\n\n/// A join edge is used for pruning queries when evaluating subscription updates.\n///\n/// If we have the following subscriptions:\n/// ```sql\n/// SELECT a.* FROM a JOIN b ON a.id = b.id WHERE b.x = 1\n/// SELECT a.* FROM a JOIN b ON a.id = b.id WHERE b.x = 2\n/// ...\n/// SELECT a.* FROM a JOIN b ON a.id = b.id WHERE b.x = n\n/// ```\n///\n/// Whenever `a` is updated, only the relevant queries are evaluated.\n#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]\npub struct JoinEdge {\n    /// The [`TableId`] for `a`","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/6dee26c6efc2856793e12b148a59742964f5d783/crates/subscription/src/lib.rs#L218-L254","documentation":"Subscription fragment compilation (Fragments::compile_from_plan) only implements single-table and two-table join plans: the match covers exactly [dr] and [dr, ds] and bails for any other count. Subscriptions joining three or more tables are therefore unsupported.","triggerScenarios":"Subscribing to a SELECT whose FROM/JOIN involves 3 or more tables, producing a tables slice with length > 2 (or 0).","commonSituations":"Widening a working two-table join subscription with one more lookup table; porting analytical multi-join SQL into subscriptions.","solutions":["Reduce each subscription to at most two tables: split the multi-join into several subscriptions and stitch results together client-side.","Denormalize: maintain a join-result table updated by reducers and subscribe to that single table.","Move the extra lookup into module logic (reducer-written helper columns) so the client query needs fewer joins."],"exampleFix":"-- before: three-table join\nSELECT a.* FROM a JOIN b ON a.b_id = b.id JOIN c ON b.c_id = c.id;\n\n-- after: two subscriptions joined client-side, or a denormalized table\nSELECT * FROM a JOIN b ON a.b_id = b.id;\nSELECT * FROM c;\n-- better: reducer-maintained `a_b_c` table, subscribe to it alone","handlingStrategy":"validation","validationCode":"-- count FROM/JOIN tables in every subscription before shipping; max is 2:\n-- SELECT a.* FROM a JOIN b ON ...;  -- ok (2 tables)\n-- SELECT ... FROM a JOIN b JOIN c;  -- will fail: 3 tables","typeGuard":null,"tryCatchPattern":"try {\n  await db.subscription.build([sql]).subscribe();\n} catch (e: any) {\n  if (String(e.message).includes(\"Invalid number of tables in subscription\")) {\n    // split into <=2-table subscriptions, or denormalize into one table\n  }\n}","preventionTips":["Keep subscriptions to single tables or one join; combine results client-side.","Denormalize multi-entity reads into reducer-maintained tables.","Lint generated subscription SQL for join count in CI."],"tags":["subscription","join","query-limits"],"backgroundTag":"unsupported-subscription-query","analyzedSha":"6dee26c6efc2856793e12b148a59742964f5d783","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}