Skip to content

Offline-first Mode With Drift - Suggested doc improvements #125

@Musta-Pollo

Description

@Musta-Pollo

It would be a good idea to update documenation with handling this scenario. Here is AI generated info:

Drift Watch Streams with PowerSync Optional Sync Mode

The Problem

When using Drift with PowerSync's optional sync mode (local-only tables with viewName override), Drift's watch() streams don't receive table update notifications. This causes watched queries to never re-execute when data changes.

Why This Happens

PowerSync's optional sync mode uses a pattern where local-only tables have different internal names than their user-facing view names:

// In PowerSync schema
Table.localOnly(
  'local_items',    // Internal table name
  [...columns],
  viewName: 'items', // User-facing view name (different!)
)

This creates a table name mismatch in the update notification flow:

  1. SQLite stores data in physical table: ps_data_local__local_items
  2. PowerSync converts to friendly name: "local_items"
  3. SqliteAsyncDriftConnection receives notification: {"local_items"}
  4. Drift is listening for updates to: "items" (the viewName)
  5. "local_items" ≠ "items"NO MATCH → Watch stream never updates!

Why Synced Mode Works

In synced mode, the table name and view name are the same:

Table(
  'items',        // Internal table name
  [...columns],
  viewName: 'items', // Same as table name
)

So notifications arrive as "items" and Drift is listening for "items"MATCH!


The Solution

Use the transformTableUpdates parameter in SqliteAsyncDriftConnection to convert the internal table names to their corresponding view names:

import 'package:drift/drift.dart' show TableUpdate;
import 'package:drift_sqlite_async/drift_sqlite_async.dart';

final connection = SqliteAsyncDriftConnection(
  powerSyncDatabase,
  transformTableUpdates: (notification) {
    // Convert local_* table names to their viewName equivalents
    // e.g., local_items → items, local_labels → labels
    return notification.tables.map((tableName) {
      if (tableName.startsWith('local_')) {
        // Remove 'local_' prefix to match the viewName
        return TableUpdate(tableName.substring(6));
      }
      return TableUpdate(tableName);
    }).toSet();
  },
);

final database = AppDatabase(connection);

How It Works

  1. Notification arrives: {"local_items"}
  2. transformTableUpdates converts: "local_items""items"
  3. Drift receives: {"items"}
  4. Drift is listening for: {"items"}
  5. MATCH! → Watch stream re-executes query

Complete Example with PowerSync Optional Sync

// schema.dart - PowerSync schema with optional sync
Schema makeSchema({required bool synced}) {
  String localViewName(String table) {
    return synced ? 'inactive_local_$table' : table;
  }

  return Schema([
    // Local-only tables with viewName override
    Table.localOnly(
      'local_items',
      [...columns],
      viewName: localViewName('items'), // "items" when synced=false
    ),
    Table.localOnly(
      'local_labels',
      [...columns],
      viewName: localViewName('labels'),
    ),
    // ... more tables
  ]);
}

// database_providers.dart - Drift connection with fix
final driftDatabaseProvider = Provider<AppDatabase>((ref) {
  final powerSync = ref.watch(powerSyncInstanceProvider);

  return AppDatabase(
    SqliteAsyncDriftConnection(
      powerSync,
      transformTableUpdates: (notification) {
        return notification.tables.map((tableName) {
          if (tableName.startsWith('local_')) {
            return TableUpdate(tableName.substring(6));
          }
          return TableUpdate(tableName);
        }).toSet();
      },
    ),
  );
});

Key Takeaways

Scenario Table Name View Name Notification Drift Expects Result
Synced mode items items items items ✅ Works
Local-only (no fix) local_items items local_items items ❌ Fails
Local-only (with fix) local_items items items (transformed) items ✅ Works

The transformTableUpdates parameter exists specifically for this use case: mapping internal table names to user-facing names when they differ.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions