clockworklabs/SpacetimeDB · error

Invalid number of tables in subscription: {}

Error message

Invalid number of tables in subscription: {}

What it means

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.

Source

Thrown at crates/subscription/src/lib.rs:236

                    )?,
                    new_plan(
                        // R'ds(-)
                        plan,
                        &[(*ds, Delta::Deletes)],
                    )?,
                    new_plan(
                        // dr(+)ds(+)
                        plan,
                        &[(*dr, Delta::Inserts), (*ds, Delta::Inserts)],
                    )?,
                    new_plan(
                        // dr(-)ds(-)
                        plan,
                        &[(*dr, Delta::Deletes), (*ds, Delta::Deletes)],
                    )?,
                ],
            }),
            _ => bail!("Invalid number of tables in subscription: {}", tables.len()),
        }
    }
}

/// A join edge is used for pruning queries when evaluating subscription updates.
///
/// If we have the following subscriptions:
/// ```sql
/// SELECT a.* FROM a JOIN b ON a.id = b.id WHERE b.x = 1
/// SELECT a.* FROM a JOIN b ON a.id = b.id WHERE b.x = 2
/// ...
/// SELECT a.* FROM a JOIN b ON a.id = b.id WHERE b.x = n
/// ```
///
/// Whenever `a` is updated, only the relevant queries are evaluated.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct JoinEdge {
    /// The [`TableId`] for `a`

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Reduce each subscription to at most two tables: split the multi-join into several subscriptions and stitch results together client-side.
  2. Denormalize: maintain a join-result table updated by reducers and subscribe to that single table.
  3. Move the extra lookup into module logic (reducer-written helper columns) so the client query needs fewer joins.

Example fix

-- before: three-table join
SELECT a.* FROM a JOIN b ON a.b_id = b.id JOIN c ON b.c_id = c.id;

-- after: two subscriptions joined client-side, or a denormalized table
SELECT * FROM a JOIN b ON a.b_id = b.id;
SELECT * FROM c;
-- better: reducer-maintained `a_b_c` table, subscribe to it alone
Defensive patterns

Strategy: validation

Validate before calling

-- count FROM/JOIN tables in every subscription before shipping; max is 2:
-- SELECT a.* FROM a JOIN b ON ...;  -- ok (2 tables)
-- SELECT ... FROM a JOIN b JOIN c;  -- will fail: 3 tables

Try / catch

try {
  await db.subscription.build([sql]).subscribe();
} catch (e: any) {
  if (String(e.message).includes("Invalid number of tables in subscription")) {
    // split into <=2-table subscriptions, or denormalize into one table
  }
}

Prevention

When it happens

Trigger: Subscribing to a SELECT whose FROM/JOIN involves 3 or more tables, producing a tables slice with length > 2 (or 0).

Common situations: Widening a working two-table join subscription with one more lookup table; porting analytical multi-join SQL into subscriptions.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/6ad48de3fc8a493c. Report an issue: GitHub.