Skip to content

Edition 3: aliasing synced table in JOIN query causes 0 rows to sync #565

@jmalmo

Description

@jmalmo

Bug description

In Sync Streams edition 3, aliasing the synced (source) table in an explicit INNER JOIN query causes zero rows to sync on the client, with no validation or runtime errors.

Reproduction

Sync config (failing)

config:
  edition: 3

streams:
  user_chat:
    priority: 2
    auto_subscribe: true
    queries:
      - SELECT * FROM chat_conversations WHERE user_id = auth.user_id()
      - >
        SELECT cm.* FROM chat_messages cm
        INNER JOIN chat_conversations ON cm.conversation_id = chat_conversations.id
        WHERE chat_conversations.user_id = auth.user_id()

The second query syncs 0 rows. Removing the alias cm (using chat_messages directly) fixes it.

Test matrix

Variant Query pattern Source table aliased Joined table aliased Result
A SELECT cm.* FROM chat_messages cm INNER JOIN chat_conversations cc ON ... yes yes 0 rows
B SELECT chat_messages.* FROM chat_messages INNER JOIN chat_conversations ON ... no no All rows
C SELECT * FROM chat_messages WHERE conversation_id IN (SELECT ...) n/a n/a All rows
D SELECT cm.* FROM chat_messages cm INNER JOIN chat_conversations ON ... yes no 0 rows
E SELECT chat_messages.* FROM chat_messages INNER JOIN chat_conversations cc ON ... no yes All rows

The bug occurs specifically when the synced (FROM) table is aliased in an explicit JOIN. Aliasing the joined table is fine. Subquery-based queries (variant C) are unaffected.

Minimal DDL and seed data

CREATE TABLE chat_conversations (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id uuid NOT NULL,
  kind text NOT NULL,
  created_at timestamptz DEFAULT now()
);

CREATE TABLE chat_messages (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  conversation_id uuid NOT NULL REFERENCES chat_conversations(id),
  user_id uuid,
  role text NOT NULL,
  content text NOT NULL,
  created_at timestamptz DEFAULT now()
);

ALTER TABLE chat_conversations REPLICA IDENTITY FULL;
ALTER TABLE chat_messages REPLICA IDENTITY FULL;
CREATE PUBLICATION powersync FOR TABLE chat_conversations, chat_messages;

-- Seed data
INSERT INTO chat_conversations (id, user_id, kind)
VALUES ('a0000000-0000-0000-0000-000000000001', 'u0000000-0000-0000-0000-000000000001', 'coach');

INSERT INTO chat_messages (id, conversation_id, user_id, role, content)
VALUES
  ('m0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'u0000000-0000-0000-0000-000000000001', 'user', 'Hello'),
  ('m0000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', null, 'assistant', 'Hi there');

With this data and auth.user_id() = 'u0000000-0000-0000-0000-000000000001':

  • The chat_conversations query syncs 1 row (works)
  • The chat_messages query with alias syncs 0 rows (bug) — should sync 2 rows

Root cause

RowEvaluator.outputName in packages/sync-rules/src/compiler/rows.ts returns the SQL alias (e.g. "cm") as the output table name for evaluated rows. WAL event matching works correctly (the sourceTable pattern uses the real table name), but the output rows are labeled with the alias. The client expects rows in chat_messages, receives rows in cm, so nothing syncs.

The alias-as-rename behavior is intentional for single-table queries (e.g. SELECT * FROM new_table AS old_client_name), but in explicit JOINs, table aliases serve SQL column disambiguation — not output renaming.

Environment

  • PowerSync Cloud, Sync Streams edition 3
  • Tested March 2026

PR

Fix submitted in #566. The fix:

  • Adds hasExplicitJoins tracking in StreamQueryParser, set when the AST FROM list contains a physical table entry (from.type == 'table') with a .join clause
  • Passes this to RowEvaluator so outputName returns the real table name when explicit physical table JOINs are present
  • Alias-as-rename preserved for: single-table queries, subquery-backed streams (lowered to joins internally), and source table with TVF on right (FROM stores s INNER JOIN json_each(s.tags))
  • Alias dropped for: any query where a physical table entry has an explicit JOIN — including TVF-left/source-right (FROM json_each(...) j INNER JOIN users u)
  • Includes 4 evaluator tests covering all scenarios, updates 2 snapshot files
  • 768 tests pass

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