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

  1. Pass SQL strings ('SELECT * FROM players') or query-builder results from db.from(...)
  2. Validate elements with the exported isRowTypedQuery(q) before subscribing
  3. 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

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


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)