Skip to content

E2E Tests for Query-Driven Sync with Predicate Push-Down #772

@KyleAMathews

Description

@KyleAMathews

Overview

Implement comprehensive end-to-end tests for the query-driven sync feature with on-demand collection loading (PR #763, RFC #676). These tests will verify that predicate push-down, deduplication, pagination, joins, and other distributed system behaviors work correctly across different collection types and syncModes.

Related Links:

These new e2e tests will branch off PR #763 and be merged into it.

Goals

  1. Create a shared e2e test suite that can be reused across different collection implementations
  2. Test critical distributed systems scenarios: concurrent loadSubset calls, race conditions, deduplication
  3. Verify predicate push-down works correctly with various query patterns
  4. Test pagination, ordering, and multi-collection joins
  5. Catch integration bugs like those found during early testing (see Known Bugs section)
  6. Keep execution time under 5 minutes for CI/CD

Architecture

Package Structure

Create a new package: @tanstack/db-collection-e2e

This package will export:

  • Individual test scenario groups organized by feature
  • Standard seed data schema and fixtures
  • Utility functions for common assertions
  • Configuration types/interfaces

Test Organization

Tests will be organized into feature-based scenario groups:

  1. Predicates Suite - Basic where clause functionality
  2. Pagination Suite - Order by, limit, offset, setWindow
  3. Joins Suite - Single and multi-collection joins
  4. Deduplication Suite - Concurrent loadSubset scenarios
  5. Collation Suite - String comparison configuration
  6. Mutations Suite - Data mutations with on-demand mode
  7. Live Updates Suite (optional) - Reactive updates for sync-enabled collections
  8. Regression Suite - Explicit tests for known bugs

Collection Integration

Each collection package (e.g., electric-db-collection, query-db-collection) will:

  • Import the shared test suite
  • Provide collection instances configured for both eager and on-demand syncModes
  • Implement collection-specific setup/teardown hooks
  • Choose which optional test suites to run

Example structure:

packages/
  db-collection-e2e/          # Shared test suite package
    src/
      suites/
        predicates.test.ts
        pagination.test.ts
        joins.test.ts
        deduplication.test.ts
        collation.test.ts
        mutations.test.ts
        live-updates.test.ts (optional)
        regressions.test.ts
      fixtures/
        seed-data.ts
        test-schema.ts
      utils/
        assertions.ts
      types.ts
      index.ts
  electric-db-collection/
    e2e/
      setup.ts              # Docker, Postgres, Electric setup
      electric.e2e.test.ts  # Imports and runs shared suites
  query-db-collection/
    e2e/
      setup.ts              # Mock backend setup
      query.e2e.test.ts     # Imports and runs shared suites

Standard Test Data Schema

Entities and Relationships

Design a schema that exercises edge cases across various data types:

// Users table
interface User {
  id: string;              // UUID
  name: string;            // For collation testing
  email: string | null;    // Nullable field
  age: number;             // Numeric comparisons
  isActive: boolean;       // Boolean predicates
  createdAt: Date;         // Date comparisons
  metadata: object | null; // JSON field (if supported)
  deletedAt: Date | null;  // Soft delete pattern
}

// Posts table
interface Post {
  id: string;
  userId: string;          // Foreign key to User
  title: string;
  content: string | null;
  viewCount: number;
  publishedAt: Date | null;
  deletedAt: Date | null;
}

// Comments table
interface Comment {
  id: string;
  postId: string;          // Foreign key to Post
  userId: string;          // Foreign key to User
  text: string;
  createdAt: Date;
  deletedAt: Date | null;
}

Seed Data Volume

  • Users: ~100 records
  • Posts: ~100 records (distributed across users)
  • Comments: ~100 records (distributed across posts)

This provides enough data to test pagination effectively while keeping tests fast.

Data Distribution

Ensure data includes:

  • Mix of null and non-null values
  • Various string cases for collation testing (uppercase, lowercase, special chars)
  • Date ranges (past, present, future)
  • Boolean distributions (true/false/null if applicable)
  • Numeric ranges (negative, zero, positive, large numbers)
  • Some soft-deleted records (deletedAt not null)

Test Configuration Interface

interface E2ETestConfig {
  // Collection instances configured for testing
  collections: {
    eager: {
      users: Collection<User>;
      posts: Collection<Post>;
      comments: Collection<Comment>;
    };
    onDemand: {
      users: Collection<User>;
      posts: Collection<Post>;
      comments: Collection<Comment>;
    };
  };

  // Lifecycle hooks
  setup: () => Promise<void>;
  teardown: () => Promise<void>;

  // Per-test hooks (optional)
  beforeEach?: () => Promise<void>;
  afterEach?: () => Promise<void>;
}

Note: Collections handle their own mutation logic internally, so no separate mutator is needed in the config.

Test Scenario Groups

1. Predicates Suite

Test basic predicate functionality across all data types:

Test cases:

  • eq() with various types (string, number, boolean, date, UUID, null)
  • ne() with various types
  • gt(), gte(), lt(), lte() with numbers and dates
  • in() with arrays
  • isNull() and isNotNull()
  • Complex boolean logic (AND, OR combinations)
  • Nested predicates

Assertions:

  • Query returns correct data matching predicates
  • Only necessary data is loaded (check collection state)
  • No errors thrown

2. Pagination Suite

Test ordering, limits, offsets, and window management:

Test cases:

  • Basic orderBy (ascending/descending)
  • Multiple orderBy fields
  • Limit without offset
  • Limit with offset
  • liveQuery.utils.setWindow() - changing windows
  • setWindow() while data is loading
  • Overlapping windows (page 2 before page 1 completes)
  • Edge cases: limit=0, offset beyond dataset, negative values

Assertions:

  • Correct page of data returned
  • Proper ordering maintained
  • Only requested data loaded (not entire dataset)

3. Joins Suite

Test multi-collection joins with various syncMode combinations:

Test cases:

  • Two-collection join (Users + Posts)
  • Three-collection join (Users + Posts + Comments)
  • Mixed syncModes: one on-demand, one eager
  • Both collections on-demand
  • Predicates on joined collections (verify pushdown)
  • Ordering across joined collections
  • Pagination on joined results
  • setWindow() on joins requiring loads from multiple collections

Assertions:

  • Correct joined data returned
  • Each collection only loads required subset (predicate pushdown working)
  • Query result matches expected join
  • No extra data loaded

Example test:

// Join users and posts where userId = 123
// Verify only user 123's posts loaded, not all posts

4. Deduplication Suite

Test concurrent loadSubset calls and deduplication behavior:

Test cases:

  • Two queries with identical predicates calling loadSubset simultaneously
  • Overlapping predicates (one is subset of another)
  • Queries arriving while data is still loading
  • Multiple concurrent queries with different predicates
  • Deduplication with limit/offset variations

Assertions:

  • Use deduplication callback to count actual vs deduplicated loads
  • Verify expected number of backend requests
  • All queries receive correct data
  • No race conditions or data corruption

5. Collation Suite

Test string collation configuration:

Test cases:

  • Default collation behavior
  • Custom defaultStringCollation at collection level
  • Custom collation at query level
  • Collation inheritance in nested queries
  • String comparisons with different collations (case-sensitive vs case-insensitive)

Assertions:

  • String predicates respect collation settings
  • Correct data returned based on collation
  • Query-level collation overrides collection-level

6. Mutations Suite

Test mutations with on-demand syncMode:

Test cases:

  • Mutate loaded data (verify sync back)
  • Create new record (verify appears in matching queries)
  • Update record to match/unmatch query predicates
  • Delete record
  • Concurrent mutations

Assertions:

  • Mutations to loaded records sync correctly
  • Query results update reactively after mutations
  • Cannot mutate records that aren't loaded (verify error/behavior)

Note: Mutation logic is collection-specific, handled internally.

7. Live Updates Suite (Optional)

For collections that support sync, test reactive updates:

Test cases:

  • Load subset via query
  • Mutate data on backend (outside client)
  • Verify query reactively updates
  • Updates during active loadSubset
  • Multiple queries watching same data

Assertions:

  • Query data updates when backend changes
  • Updates don't trigger unnecessary reloads
  • Correct data maintained throughout

8. Regression Suite

Explicit tests for known bugs found during development:

Critical bugs to test:

  1. Missing subset_ params bug

    • Query with no where or limit should include proper params
    • Server shouldn't treat as normal shape request
    • Test: Create query without predicates, verify URL params
  2. eq(deletedAt, null) SQL syntax error

    • Test: eq(deletedAt, null) should work without SQL errors
    • Verify correct records returned
  3. eq(id, uuid) syntax error

    • Test: eq(id, '{uuid-value}') should work
    • Verify UUID fields work in predicates
  4. Unnecessary offset in URL

    • Verify offset only added when needed
    • Test URL construction
  5. JSON parse inefficiency

    • Ensure snapshot responses aren't parsed multiple times
    • (May need internal inspection or performance monitoring)

9. Progressive Mode (Deferred)

Not included in initial implementation, but placeholder for future:

  • Test mode transition from on-demand → eager when full sync completes
  • Verify subset loads work while background sync proceeds

Infrastructure Setup

Docker Orchestration

Follow Electric's e2e pattern (see generated docs):

Services needed:

  • PostgreSQL (port 54321)
  • Electric server (port 3000)
  • tmpfs for performance
  • Health checks with proper timeouts

docker-compose.yml example:

services:
  postgres:
    image: postgres:14-alpine
    environment:
      POSTGRES_DB: electric
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "54321:5432"
    tmpfs: /var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 10s
      retries: 5

  electric:
    image: electricsql/electric:latest
    environment:
      DATABASE_URL: postgresql://postgres:password@postgres:5432/electric
      ELECTRIC_WRITE_TO_PG_MODE: direct_writes
      PG_PROXY_PORT: "65432"
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 2s
      timeout: 10s
      retries: 5

Database Isolation

Use schema-based isolation with unique table names per test:

// Shared database: electric_test schema
// Unique table names: `table_for_{taskId}_{randomSuffix}`

This prevents test collisions while allowing parallel execution within Vitest's constraints.

Vitest Configuration

Critical settings:

export default defineConfig({
  test: {
    fileParallelism: false, // Serial execution for shared DB
    globalSetup: './e2e/global-setup.ts',
    timeout: 30000, // Extended for Docker operations
  }
})

Global Setup

Implement health check for Docker services:

// global-setup.ts
export async function setup() {
  // Wait for Postgres and Electric to be healthy
  await waitForHealthCheck('http://localhost:3000/health')
  await waitForPostgres('postgresql://postgres:password@localhost:54321/electric')
}

Test Fixtures

Use Vitest's test.extend() for composable fixtures:

const testWithDb = test.extend({
  db: async ({}, use) => {
    const db = await setupDatabase()
    await use(db)
    await cleanupDatabase(db)
  }
})

const testWithCollections = testWithDb.extend({
  collections: async ({ db }, use) => {
    const collections = await seedAndCreateCollections(db)
    await use(collections)
  }
})

Test Flow Pattern

Typical test structure:

test('should load correct data with predicates', async ({ collections }) => {
  // 1. Seed data happens in fixture setup

  // 2. Create query with predicates
  const query = collections.onDemand.users.liveQuery({
    where: eq(users.age, 25)
  })

  // 3. Await preload
  await query.preload()

  // 4. Assert correct data
  const result = query.getResult()
  expect(result).toHaveLength(expectedCount)
  expect(result.every(u => u.age === 25)).toBe(true)

  // 5. Verify only necessary data loaded
  const loadedIds = getLoadedUserIds(collections.onDemand.users)
  expect(loadedIds).toEqual(expectedUserIds)
})

Mock Backend (Query Collection)

For query-db-collection, mock the backend fetch:

// Mock TanStack Query backend
const mockBackend = {
  fetchUsers: vi.fn(async ({ where, orderBy, limit, offset }) => {
    // Return filtered/paginated seed data
    return filterData(seedData.users, { where, orderBy, limit, offset })
  })
}

Implementation Checklist

Phase 1: Infrastructure

  • Create @tanstack/db-collection-e2e package
  • Set up Docker Compose for Postgres + Electric
  • Implement global setup with health checks
  • Create test fixtures for DB and collections
  • Define standard schema and seed data
  • Create config interface and types

Phase 2: Core Test Suites

  • Implement Predicates Suite
  • Implement Pagination Suite
  • Implement Joins Suite
  • Implement Deduplication Suite

Phase 3: Additional Suites

  • Implement Collation Suite
  • Implement Mutations Suite
  • Implement Regression Suite (known bugs)
  • Implement Live Updates Suite (optional)

Phase 4: Collection Integration

  • Set up e2e tests for electric-db-collection
    • Electric-specific setup/teardown
    • Run all applicable suites
  • Set up e2e tests for query-db-collection
    • Mock backend setup
    • Run all applicable suites (skip Electric-specific)

Phase 5: CI/CD

  • Verify execution time < 5 minutes
  • Add to CI pipeline
  • Document how to run locally

Success Criteria

  • All test suites pass for both electric-db-collection and query-db-collection
  • Known bugs from early testing are caught by regression tests
  • Deduplication verified via callback assertions
  • Predicate pushdown verified (collections don't load extra data)
  • Joins work correctly with mixed syncModes
  • Pagination and ordering work correctly
  • String collation respected
  • Total execution time < 5 minutes
  • Tests are reliable (no flakes)
  • New collections can easily adopt the test suite

Future Enhancements

  • Progressive mode transition testing
  • Performance benchmarks
  • Subscription lifecycle edge cases (unsubscribe during load)
  • Network failure scenarios
  • More complex join patterns (self-joins, multiple paths)

References


Notes for Implementation

  1. Start small: Implement infrastructure + Predicates Suite first to validate approach
  2. Copy Electric patterns: Leverage proven patterns from Electric's e2e setup
  3. Keep tests strict: No retries, fast timeouts - rely on setup hooks for consistency
  4. Test through public API: Don't expose internal state unless necessary
  5. Document as you go: Add examples for future collections to reference

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    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