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
Bug description
In Sync Streams edition 3, aliasing the synced (source) table in an explicit
INNER JOINquery causes zero rows to sync on the client, with no validation or runtime errors.Reproduction
Sync config (failing)
The second query syncs 0 rows. Removing the alias
cm(usingchat_messagesdirectly) fixes it.Test matrix
SELECT cm.* FROM chat_messages cm INNER JOIN chat_conversations cc ON ...SELECT chat_messages.* FROM chat_messages INNER JOIN chat_conversations ON ...SELECT * FROM chat_messages WHERE conversation_id IN (SELECT ...)SELECT cm.* FROM chat_messages cm INNER JOIN chat_conversations ON ...SELECT chat_messages.* FROM chat_messages INNER JOIN chat_conversations cc ON ...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
With this data and
auth.user_id() = 'u0000000-0000-0000-0000-000000000001':chat_conversationsquery syncs 1 row (works)chat_messagesquery with alias syncs 0 rows (bug) — should sync 2 rowsRoot cause
RowEvaluator.outputNameinpackages/sync-rules/src/compiler/rows.tsreturns the SQL alias (e.g."cm") as the output table name for evaluated rows. WAL event matching works correctly (thesourceTablepattern uses the real table name), but the output rows are labeled with the alias. The client expects rows inchat_messages, receives rows incm, 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
PR
Fix submitted in #566. The fix:
hasExplicitJoinstracking inStreamQueryParser, set when the ASTFROMlist contains a physical table entry (from.type == 'table') with a.joinclauseRowEvaluatorsooutputNamereturns the real table name when explicit physical table JOINs are presentFROM stores s INNER JOIN json_each(s.tags))FROM json_each(...) j INNER JOIN users u)