{"record":{"id":"f6db69c19e757ef3","repo":"clockworklabs/SpacetimeDB","slug":"subscriptions-must-have-at-least-one-query","errorCode":null,"errorMessage":"Subscriptions must have at least one query","messagePattern":"Subscriptions must have at least one query","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/bindings-typescript/src/sdk/subscription_builder_impl.ts","lineNumber":120,"sourceCode":"    ) => RowTypedQuery<any, any> | RowTypedQuery<any, any>[]\n  ): SubscriptionHandleImpl<RemoteModule>;\n  subscribe(\n    query_sql:\n      | string\n      | RowTypedQuery<any, any>\n      | Array<string | RowTypedQuery<any, any>>\n      | ((tables: any) => RowTypedQuery<any, any> | RowTypedQuery<any, any>[])\n  ): SubscriptionHandleImpl<RemoteModule> {\n    let queries: Array<string | RowTypedQuery<any, any>>;\n    if (typeof query_sql === 'function') {\n      const tables = this.db.getFromBuilder<RemoteModule & UntypedSchemaDef>();\n      const result = query_sql(tables);\n      queries = Array.isArray(result) ? result : [result];\n    } else {\n      queries = Array.isArray(query_sql) ? query_sql : [query_sql];\n    }\n    if (queries.length === 0) {\n      throw new Error('Subscriptions must have at least one query');\n    }\n    const queryStrings = queries.map(q => {\n      if (typeof q === 'string') return q;\n      if (isRowTypedQuery(q)) return toSql(q);\n      throw new Error('Subscriptions must be SQL strings or typed queries');\n    });\n    return new SubscriptionHandleImpl(\n      this.db,\n      queryStrings,\n      this.#onApplied,\n      this.#onError\n    );\n  }\n\n  /**\n   * Subscribes to all rows from all tables.\n   *\n   * This method is intended as a convenience","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings-typescript/src/sdk/subscription_builder_impl.ts#L102-L138","documentation":"SubscriptionBuilder's subscribe normalizes its argument into an array of queries (single value, array, or the result of a tables-callback) and requires at least one entry before converting them to SQL strings. An empty list would send a meaningless subscribe message, so it is rejected.","triggerScenarios":"Passing [] to .subscribe(...); a callback like tables => myFilteredQueries where the filter produced an empty array; a function callback that returns [] by mistake or falls through without returning a query.","commonSituations":"Dynamically building a subscription list from user selections where everything is filtered out; an early-return in the query-builder callback; refactoring removed the hardcoded query and left an empty array.","solutions":["Guard before subscribing: if the query list is empty, fall back to subscribeToAllTables() or skip subscribing","Fix the callback so it always returns at least one query","Log the computed query list when it is empty to catch filter bugs early"],"exampleFix":"// before\nconst qs = selected.filter(t => t.enabled).map(t => `SELECT * FROM ${t}`);\nbuilder.subscribe(qs); // throws when nothing is enabled\n\n// after\nconst qs = selected.filter(t => t.enabled).map(t => `SELECT * FROM ${t}`);\nif (qs.length === 0) builder.subscribeToAllTables();\nelse builder.subscribe(qs);","handlingStrategy":"validation","validationCode":"const queries = buildQueries();\nif (queries.length === 0) {\n  builder.subscribeToAllTables();\n} else {\n  builder.subscribe(queries);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never build the query list and subscribe in one expression - keep them separate so the empty case is visible","Log a warning when the computed query list is empty; it usually means a filter removed everything"],"tags":["subscription","query","validation","typescript"],"backgroundTag":"empty-subscription-query","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}