clockworklabs/SpacetimeDB · error · Error
Subscriptions must be SQL strings or typed queries
Error message
Subscriptions must be SQL strings or typed queries
What it means
Each element passed to subscribe must be either a SQL string or a RowTypedQuery produced by the typed query builder, checked with isRowTypedQuery from lib/query. Elements of any other shape — raw table definition objects, numbers, plain objects with a toString — throw immediately after the empty-list check.
Solutions
- Pass SQL strings ('SELECT * FROM players') or query-builder results from db.from(...)
- Validate elements with the exported isRowTypedQuery(q) before subscribing
- Type your query arrays as Array<string | RowTypedQuery<any, any>> so the compiler rejects other shapes
Example fix
// before
sub.subscribe([db.tables.players]); // table def, not a query -> throws
// after
sub.subscribe(['SELECT * FROM players']); // or db.from('players')... builder result Defensive patterns
Strategy: type-guard
Validate before calling
import { isRowTypedQuery } from '../lib/query';
const ok = queries.every(q => typeof q === 'string' || isRowTypedQuery(q));
if (!ok) {
throw new Error('subscribe expects SQL strings or RowTypedQuery objects only');
}
sub.onApplied(onApplied).subscribe(queries); Type guard
import { isRowTypedQuery } from '../lib/query';
type SubscribableQuery = string | RowTypedQuery<any, any>;
function isSubscribableQuery(q: unknown): q is SubscribableQuery {
return typeof q === 'string' || isRowTypedQuery(q);
} Prevention
- Build queries exclusively via db.from(...) or plain SQL strings
- Type query arrays as Array<string | RowTypedQuery<any, any>>
- Never pass table definition objects (db.tables.x) to subscribe
When it happens
Trigger: Passing a table accessor object (e.g. db.tables.players) instead of a query built from db.from(...); spreading a mixed array; passing results of a custom helper that does not return RowTypedQuery objects.
Common situations: Migrating from raw-SQL code and mixing string and object shapes; assuming any stringifyable object is accepted; older API versions that took table objects directly.
Related errors
- Subscriptions must have at least one query
- Could not determine return type during RLS resolution
- Estimated cardinality
- Failed to determine TableId for query
- `fromCounterV7` requires `randomBytes.length == 4`
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/e03f6732a257fcea.
Report an issue: GitHub.
Appendix: source
Thrown at crates/bindings-typescript/src/sdk/subscription_builder_impl.ts:125
| RowTypedQuery<any, any>
| Array<string | RowTypedQuery<any, any>>
| ((tables: any) => RowTypedQuery<any, any> | RowTypedQuery<any, any>[])
): SubscriptionHandleImpl<RemoteModule> {
let queries: Array<string | RowTypedQuery<any, any>>;
if (typeof query_sql === 'function') {
const tables = this.db.getFromBuilder<RemoteModule & UntypedSchemaDef>();
const result = query_sql(tables);
queries = Array.isArray(result) ? result : [result];
} else {
queries = Array.isArray(query_sql) ? query_sql : [query_sql];
}
if (queries.length === 0) {
throw new Error('Subscriptions must have at least one query');
}
const queryStrings = queries.map(q => {
if (typeof q === 'string') return q;
if (isRowTypedQuery(q)) return toSql(q);
throw new Error('Subscriptions must be SQL strings or typed queries');
});
return new SubscriptionHandleImpl(
this.db,
queryStrings,
this.#onApplied,
this.#onError
);
}
/**
* Subscribes to all rows from all tables.
*
* This method is intended as a convenience
* for applications where client-side memory use and network bandwidth are not concerns.
* Applications where these resources are a constraint
* should register more precise queries via `subscribe`
* in order to replicate only the subset of data which the client needs to function.
*View on GitHub (pinned to 6dee26c6ef)