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

After collecting the query list, subscribe maps every entry to SQL: strings pass through and RowTypedQuery objects (produced by the query builder obtained via db.getFromBuilder) are converted with toSql(). Anything else - a raw table definition, undefined, null, a number - is rejected.

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 524b4487d9)

Solutions

  1. Finish the typed query chain so each entry is a RowTypedQuery, e.g. tables => tables.user.select().where(...)
  2. Otherwise pass plain SQL strings: builder.subscribe(['SELECT * FROM user'])
  3. Sanitize the array first: drop null/undefined entries and verify each is a string or builder result

Example fix

// before
builder.subscribe([db.getFromBuilder().user, undefined]); // throws

// after
builder.subscribe([db.getFromBuilder().user.where(ctx => ctx.userId).eq(1)]);
Defensive patterns

Strategy: validation

Validate before calling

const ok = queries.every(q => typeof q === 'string' || (typeof q === 'object' && q !== null));
if (!ok) throw new TypeError('every subscription query must be an SQL string or a typed query');
builder.subscribe(queries.filter(Boolean));

Type guard

function isSqlOrTypedQuery(q: unknown): q is string | object {
  return (typeof q === 'string' && q.trim().length > 0) || (typeof q === 'object' && q !== null);
}

Prevention

When it happens

Trigger: Passing the table accessor itself instead of a completed builder chain; an array containing undefined/null (sparse array or a map callback that returns nothing); an object from a different SDK version that fails the isRowTypedQuery check.

Common situations: Forgetting to finish the fluent chain (passing tables.user instead of tables.user.where(...)); mixing string and builder APIs incorrectly; upgrading one of two packages so the query objects no longer match the expected shape.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/e03f6732a257fcea. Report an issue: GitHub.