Skip to content

[PowerSync] Collection breaks on @powersync/common 2.x: row types lose their columns, and sync never starts #1811

Description

@lukiwolski
  • I've validated the bug against the latest version of DB packages

Installed @tanstack/powersync-db-collection 0.1.66. The same code is on main, which is at 0.1.67.

Describe the bug

Against @powersync/common 2.x, the collection breaks in two independent ways.

  1. Row types lose their columns. PowerSync 2.0 removed the public columnMap getter from Table. ExtractedTableColumns, OptionalExtractedTable and AnyTableColumnType in packages/powersync-db-collection/src/helpers.ts all map over TTable['columnMap'], so every row type keeps only id. All three powerSyncCollectionOptions overloads are affected, including the two that take a schema.
  2. Sync never starts. PowerSync 2.x replaced the logger's info, warn and error methods with a single log(record). The first line of start() in dist/esm/powersync.js calls database.logger.info(...), which throws. The .catch handler then calls database.logger.error(...) and throws again, so the original failure is lost. In 0.1.66 the collection calls logger.error 6 times, logger.info 4 times and logger.warn once.

An earlier version of this description called the break types-only. That is true of columnMap, but the logger break stops the collection from working at runtime too.

To Reproduce

Types, with @tanstack/powersync-db-collection@0.1.66, @tanstack/db@0.8.7, @powersync/common@2.2.1 and TypeScript 6.0.3:

import { column, Schema, Table, type AbstractPowerSyncDatabase } from '@powersync/common'
import { createCollection } from '@tanstack/db'
import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'

declare const db: AbstractPowerSyncDatabase

const items = new Table({ name: column.text, created_at: column.text })
const schema = new Schema({ items })

const collection = createCollection(
  powerSyncCollectionOptions({ database: db, table: schema.props.items }),
)

collection.insert({ id: '1', name: 'squat', created_at: new Date().toISOString() })
error TS2353: Object literal may only specify known properties, and 'name' does not exist in type 'OptionalExtractedTable<Table<{ name: BaseColumnType<string | null>; created_at: BaseColumnType<string | null>; }>> | OptionalExtractedTable<...>[]'.

Reading rows fails the same way:

error TS2339: Property 'name' does not exist on type '{ id: string; readonly $synced: boolean; readonly $origin: VirtualOrigin; readonly $key: string; readonly $collectionId: string; }'.

Runtime: create the same collection over a PowerSyncDatabase from @powersync/react-native 2.2.1 on React Native 0.86.3. At startup:

Uncaught (in promise, id: 0) TypeError: undefined is not a function

The stack points at the .catch handler of start() in dist/esm/powersync.js, on the database.logger.error call.

Expected behavior

Row types include the table's columns and sync starts, as they do with @powersync/common 1.x.

Additional context

Table in @powersync/common 1.49.0 and 1.57.3:

export declare class Table<Columns extends ColumnsType = ColumnsType> {
    protected _mappedColumns: Columns;
    get columnMap(): Columns;

Table in @powersync/common 2.2.1, with no columnMap:

export declare class Table<Columns extends ColumnsType = ColumnsType> extends ResolvedTable {
    protected _mappedColumns: Columns;

The logger in @powersync/common 2.2.1, lib/utils/Logger.d.ts:

export interface PowerSyncLogger {
    log(record: LogRecord): void;
}

Possible fixes:

  1. Read the columns from the class's type parameter, which both major versions declare:

    type TableColumns<TTable extends Table> = TTable extends Table<infer Columns> ? Columns : never
    
    export type ExtractedTableColumns<TTable extends Table> = {
      [K in keyof TableColumns<TTable>]: ExtractColumnValueType<TableColumns<TTable>[K]>
    }

    The same substitution would apply in OptionalExtractedTable and AnyTableColumnType. I checked that TableColumns resolves to exactly the declared column names against @powersync/common 2.2.1. I haven't compiled it against 1.x, although the type parameter is declared the same way there.

  2. Log through database.logger.log({ level: LogLevels.error, message, error }), using the LogLevels that @powersync/common 2.x exports.

Separately, the peer range @powersync/common: ^1.41.0 excludes 2.x, so pnpm reports it as unmet.

Workarounds in the meantime, for the types:

declare module '@powersync/common' {
  interface Table<Columns extends ColumnsType = ColumnsType> {
    readonly columnMap: Columns
  }
}

And for the runtime, a logger with both shapes, passed to the database:

import { createConsoleLogger, LogLevels, type PowerSyncLogger } from '@powersync/common'

const base = createConsoleLogger({ prefix: 'PowerSync' })

const logger: PowerSyncLogger & {
  info: (message: string, error?: unknown) => void
  warn: (message: string, error?: unknown) => void
  error: (message: string, error?: unknown) => void
} = {
  log: (record) => base.log(record),
  info: (message, error) => base.log({ level: LogLevels.info, message, error }),
  warn: (message, error) => base.log({ level: LogLevels.warn, message, error }),
  error: (message, error) => base.log({ level: LogLevels.error, message, error }),
}

new PowerSyncDatabase({ schema, logger, database: { dbFilename: 'app.db' } })

Environment: @tanstack/powersync-db-collection 0.1.66, @tanstack/db 0.8.7, @tanstack/react-db 0.3.7, @powersync/common 2.2.1, @powersync/react-native 2.2.1, @powersync/web 2.3.1, TypeScript 6.0.3, Expo SDK 57 with React Native 0.86.3.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions