Skip to content

Commit a98cecb

Browse files
authored
Sync streams: Support aliases (#377)
1 parent 5328802 commit a98cecb

14 files changed

+217
-75
lines changed

.changeset/good-moles-confess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/service-sync-rules': patch
3+
---
4+
5+
Sync streams: Support table aliases in subqueries.

packages/sync-rules/src/BaseSqlDataQuery.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SelectedColumn } from 'pgsql-ast-parser';
22
import { SqlRuleError } from './errors.js';
33
import { ColumnDefinition } from './ExpressionType.js';
44
import { SourceTableInterface } from './SourceTableInterface.js';
5-
import { SqlTools } from './sql_filters.js';
5+
import { AvailableTable, SqlTools } from './sql_filters.js';
66
import { TablePattern } from './TablePattern.js';
77
import {
88
BucketIdTransformer,
@@ -31,7 +31,7 @@ export interface EvaluateRowOptions {
3131

3232
export interface BaseSqlDataQueryOptions {
3333
sourceTable: TablePattern;
34-
table: string;
34+
table: AvailableTable;
3535
sql: string;
3636
columns: SelectedColumn[];
3737
extractors: RowValueExtractor[];
@@ -52,7 +52,7 @@ export class BaseSqlDataQuery {
5252
*
5353
* This is used for the output table name.
5454
*/
55-
readonly table: string;
55+
readonly table: AvailableTable;
5656

5757
/**
5858
* The source SQL query, for debugging purposes.
@@ -121,12 +121,12 @@ export class BaseSqlDataQuery {
121121
// Wildcard without alias - use source
122122
return sourceTable;
123123
} else {
124-
return this.table;
124+
return this.table.sqlName;
125125
}
126126
}
127127

128128
isUnaliasedWildcard() {
129-
return this.sourceTable.isWildcard && this.table == this.sourceTable.tablePattern;
129+
return this.sourceTable.isWildcard && !this.table.isAliased;
130130
}
131131

132132
columnOutputNames(): string[] {
@@ -157,7 +157,7 @@ export class BaseSqlDataQuery {
157157
this.getColumnOutputsFor(schemaTable, output);
158158
}
159159
result.push({
160-
name: this.table,
160+
name: this.table.sqlName,
161161
columns: Object.values(output)
162162
});
163163
}
@@ -181,7 +181,7 @@ export class BaseSqlDataQuery {
181181
try {
182182
const { table, row, bucketIds } = options;
183183

184-
const tables = { [this.table]: this.addSpecialParameters(table, row) };
184+
const tables = { [this.table.nameInSchema]: this.addSpecialParameters(table, row) };
185185
const resolvedBucketIds = bucketIds(tables);
186186

187187
const data = this.transformRow(tables);
@@ -221,15 +221,15 @@ export class BaseSqlDataQuery {
221221
protected getColumnOutputsFor(schemaTable: SourceSchemaTable, output: Record<string, ColumnDefinition>) {
222222
const querySchema: QuerySchema = {
223223
getColumn: (table, column) => {
224-
if (table == this.table) {
224+
if (table == this.table.nameInSchema) {
225225
return schemaTable.getColumn(column);
226226
} else {
227227
// TODO: bucket parameters?
228228
return undefined;
229229
}
230230
},
231231
getColumns: (table) => {
232-
if (table == this.table) {
232+
if (table == this.table.nameInSchema) {
233233
return schemaTable.getColumns();
234234
} else {
235235
return [];

packages/sync-rules/src/SqlBucketDescriptor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,12 @@ export class SqlBucketDescriptor implements BucketSource {
217217

218218
debugWriteOutputTables(result: Record<string, { query: string }[]>): void {
219219
for (let q of this.dataQueries) {
220-
result[q.table!] ??= [];
220+
result[q.table!.sqlName] ??= [];
221221
const r = {
222222
query: q.sql
223223
};
224224

225-
result[q.table!].push(r);
225+
result[q.table!.sqlName].push(r);
226226
}
227227
}
228228

packages/sync-rules/src/SqlDataQuery.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { BaseSqlDataQuery, BaseSqlDataQueryOptions, RowValueExtractor } from './
44
import { SqlRuleError } from './errors.js';
55
import { ExpressionType } from './ExpressionType.js';
66
import { SourceTableInterface } from './SourceTableInterface.js';
7-
import { SqlTools } from './sql_filters.js';
7+
import { AvailableTable, SqlTools } from './sql_filters.js';
88
import { checkUnsupportedFeatures, isClauseError } from './sql_support.js';
99
import { SyncRulesOptions } from './SqlSyncRules.js';
1010
import { TablePattern } from './TablePattern.js';
@@ -48,7 +48,7 @@ export class SqlDataQuery extends BaseSqlDataQuery {
4848
if (tableRef?.name == null) {
4949
throw new SqlRuleError('Must SELECT from a single table', sql, q.from?.[0]._location);
5050
}
51-
const alias: string = tableRef.alias ?? tableRef.name;
51+
const alias = AvailableTable.fromAst(tableRef);
5252

5353
const sourceTable = new TablePattern(tableRef.schema ?? options.defaultSchema, tableRef.name);
5454
let querySchema: QuerySchema | undefined = undefined;
@@ -71,7 +71,7 @@ export class SqlDataQuery extends BaseSqlDataQuery {
7171
const where = q.where;
7272
const tools = new SqlTools({
7373
table: alias,
74-
parameterTables: ['bucket'],
74+
parameterTables: [new AvailableTable('bucket')],
7575
valueTables: [alias],
7676
compatibilityContext: compatibility,
7777
sql,
@@ -123,7 +123,7 @@ export class SqlDataQuery extends BaseSqlDataQuery {
123123
} else {
124124
extractors.push({
125125
extract: (tables, output) => {
126-
const row = tables[alias];
126+
const row = tables[alias.nameInSchema];
127127
for (let key in row) {
128128
if (key.startsWith('_')) {
129129
continue;
@@ -132,7 +132,7 @@ export class SqlDataQuery extends BaseSqlDataQuery {
132132
}
133133
},
134134
getTypes(schema, into) {
135-
for (let column of schema.getColumns(alias)) {
135+
for (let column of schema.getColumns(alias.nameInSchema)) {
136136
into[column.name] ??= column;
137137
}
138138
}
@@ -146,7 +146,7 @@ export class SqlDataQuery extends BaseSqlDataQuery {
146146
// Not performing schema-based validation - assume there is an id
147147
hasId = true;
148148
} else {
149-
const idType = querySchema.getColumn(alias, 'id')?.type ?? ExpressionType.NONE;
149+
const idType = querySchema.getColumn(alias.nameInSchema, 'id')?.type ?? ExpressionType.NONE;
150150
if (!idType.isNone()) {
151151
hasId = true;
152152
}

packages/sync-rules/src/SqlParameterQuery.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { BucketParameterQuerier, ParameterLookup, ParameterLookupSource } from './BucketParameterQuerier.js';
99
import { SqlRuleError } from './errors.js';
1010
import { SourceTableInterface } from './SourceTableInterface.js';
11-
import { SqlTools } from './sql_filters.js';
11+
import { AvailableTable, SqlTools } from './sql_filters.js';
1212
import { checkUnsupportedFeatures, isClauseError, isParameterValueClause } from './sql_support.js';
1313
import { StaticSqlParameterQuery } from './StaticSqlParameterQuery.js';
1414
import { TablePattern } from './TablePattern.js';
@@ -33,7 +33,7 @@ import { filterJsonRow, getBucketId, isJsonValue, isSelectStatement, normalizePa
3333

3434
export interface SqlParameterQueryOptions {
3535
sourceTable: TablePattern;
36-
table: string;
36+
table: AvailableTable;
3737
sql: string;
3838
lookupExtractors: Record<string, RowValueClause>;
3939
parameterExtractors: Record<string, ParameterValueClause>;
@@ -95,8 +95,8 @@ export class SqlParameterQuery {
9595
if (tableRef?.name == null) {
9696
throw new SqlRuleError('Must SELECT from a single table', sql, q.from?.[0]._location);
9797
}
98-
const alias: string = q.from?.[0].name.alias ?? tableRef.name;
99-
if (tableRef.name != alias) {
98+
const alias = new AvailableTable(tableRef.name, q.from?.[0].name.alias);
99+
if (alias.isAliased) {
100100
errors.push(new SqlRuleError('Table aliases not supported in parameter queries', sql, q.from?.[0]._location));
101101
}
102102
const sourceTable = new TablePattern(tableRef.schema ?? options.defaultSchema, tableRef.name);
@@ -119,7 +119,7 @@ export class SqlParameterQuery {
119119

120120
const tools = new SqlTools({
121121
table: alias,
122-
parameterTables: ['token_parameters', 'user_parameters'],
122+
parameterTables: [new AvailableTable('token_parameters'), new AvailableTable('user_parameters')],
123123
sql,
124124
supportsExpandingParameters: true,
125125
supportsParameterExpressions: true,
@@ -212,9 +212,10 @@ export class SqlParameterQuery {
212212
* The table name or alias, as referred to in the SQL query.
213213
* Not used directly outside the query.
214214
*
215-
* Currently, this always matches sourceTable.name.
215+
* Since aliases aren't allowed in parameter queries, this always matches sourceTable.name (checked by
216+
* {@link fromSql}).
216217
*/
217-
readonly table: string;
218+
readonly table: AvailableTable;
218219

219220
/**
220221
* The source SQL query, for debugging purposes.
@@ -308,7 +309,7 @@ export class SqlParameterQuery {
308309
*/
309310
evaluateParameterRow(row: SqliteRow): EvaluatedParametersResult[] {
310311
const tables = {
311-
[this.table]: row
312+
[this.table.nameInSchema]: row
312313
};
313314
try {
314315
const filterParameters = this.filter.filterRow(tables);
@@ -336,7 +337,7 @@ export class SqlParameterQuery {
336337
}
337338

338339
private transformRows(row: SqliteRow): SqliteRow[] {
339-
const tables = { [this.table]: row };
340+
const tables = { [this.table.sqlName]: row };
340341
let result: SqliteRow = {};
341342
for (let key in this.lookupExtractors) {
342343
const extractor = this.lookupExtractors[key];

packages/sync-rules/src/StaticSqlParameterQuery.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SelectedColumn, SelectFromStatement } from 'pgsql-ast-parser';
22
import { BucketDescription, BucketPriority, DEFAULT_BUCKET_PRIORITY } from './BucketDescription.js';
33
import { SqlRuleError } from './errors.js';
4-
import { SqlTools } from './sql_filters.js';
4+
import { AvailableTable, SqlTools } from './sql_filters.js';
55
import { checkUnsupportedFeatures, isClauseError, isParameterValueClause, sqliteBool } from './sql_support.js';
66
import {
77
BucketIdTransformer,
@@ -43,7 +43,7 @@ export class StaticSqlParameterQuery {
4343

4444
const tools = new SqlTools({
4545
table: undefined,
46-
parameterTables: ['token_parameters', 'user_parameters'],
46+
parameterTables: [new AvailableTable('token_parameters'), new AvailableTable('user_parameters')],
4747
supportsParameterExpressions: true,
4848
compatibilityContext: options.compatibility,
4949
sql

packages/sync-rules/src/TableQuerySchema.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { ColumnDefinition } from './ExpressionType.js';
2+
import { AvailableTable } from './sql_filters.js';
23
import { QuerySchema, SourceSchemaTable } from './types.js';
34

5+
/**
6+
* Exposes a list of {@link SourceSchemaTable}s as a {@link QuerySchema} by only exposing the subset of the schema
7+
* referenced in a `FROM` clause.
8+
*/
49
export class TableQuerySchema implements QuerySchema {
510
constructor(
611
private tables: SourceSchemaTable[],
7-
private alias: string
12+
private alias: AvailableTable
813
) {}
914

1015
getColumn(table: string, column: string): ColumnDefinition | undefined {
11-
if (table != this.alias) {
16+
if (table != this.alias.nameInSchema) {
1217
return undefined;
1318
}
1419
for (let table of this.tables) {
@@ -21,7 +26,7 @@ export class TableQuerySchema implements QuerySchema {
2126
}
2227

2328
getColumns(table: string): ColumnDefinition[] {
24-
if (table != this.alias) {
29+
if (table != this.alias.nameInSchema) {
2530
return [];
2631
}
2732
let columns: Record<string, ColumnDefinition> = {};

packages/sync-rules/src/TableValuedFunctionSqlParameterQuery.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FromCall, SelectFromStatement } from 'pgsql-ast-parser';
22
import { SqlRuleError } from './errors.js';
3-
import { SqlTools } from './sql_filters.js';
3+
import { AvailableTable, SqlTools } from './sql_filters.js';
44
import { checkUnsupportedFeatures, isClauseError, isParameterValueClause, sqliteBool } from './sql_support.js';
55
import { generateTableValuedFunctions, TableValuedFunction } from './TableValuedFunctions.js';
66
import {
@@ -26,7 +26,7 @@ export interface TableValuedFunctionSqlParameterQueryOptions {
2626
filter: ParameterValueClause | undefined;
2727
callClause: ParameterValueClause | undefined;
2828
function: TableValuedFunction;
29-
callTableName: string;
29+
callTable: AvailableTable;
3030

3131
errors: SqlRuleError[];
3232
}
@@ -59,12 +59,12 @@ export class TableValuedFunctionSqlParameterQuery {
5959
throw new SqlRuleError(`Table-valued function ${call.function.name} is not defined.`, sql, call);
6060
}
6161

62-
const callTable = call.alias?.name ?? call.function.name;
62+
const callTable = AvailableTable.fromCall(call);
6363
const callExpression = call.args[0];
6464

6565
const tools = new SqlTools({
6666
table: callTable,
67-
parameterTables: ['token_parameters', 'user_parameters', callTable],
67+
parameterTables: [new AvailableTable('token_parameters'), new AvailableTable('user_parameters'), callTable],
6868
supportsParameterExpressions: true,
6969
compatibilityContext: compatibility,
7070
sql
@@ -108,7 +108,7 @@ export class TableValuedFunctionSqlParameterQuery {
108108
filter: isClauseError(filter) ? undefined : filter,
109109
callClause: isClauseError(callClause) ? undefined : callClause,
110110
function: functionImpl,
111-
callTableName: callTable,
111+
callTable,
112112
priority: priority ?? DEFAULT_BUCKET_PRIORITY,
113113
queryId,
114114
errors
@@ -186,7 +186,7 @@ export class TableValuedFunctionSqlParameterQuery {
186186
*
187187
* Only used internally.
188188
*/
189-
readonly callTableName: string;
189+
readonly callTable: AvailableTable;
190190

191191
readonly errors: SqlRuleError[];
192192

@@ -201,7 +201,7 @@ export class TableValuedFunctionSqlParameterQuery {
201201
this.filter = options.filter;
202202
this.callClause = options.callClause;
203203
this.function = options.function;
204-
this.callTableName = options.callTableName;
204+
this.callTable = options.callTable;
205205

206206
this.errors = options.errors;
207207
}
@@ -232,7 +232,7 @@ export class TableValuedFunctionSqlParameterQuery {
232232
const mergedParams: ParameterValueSet = {
233233
...parameters,
234234
lookup: (table, column) => {
235-
if (table == this.callTableName) {
235+
if (table == this.callTable.nameInSchema) {
236236
return row[column]!;
237237
} else {
238238
return parameters.lookup(table, column);

packages/sync-rules/src/events/SqlEventSourceQuery.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BaseSqlDataQuery, BaseSqlDataQueryOptions, RowValueExtractor } from '..
33
import { SqlRuleError } from '../errors.js';
44
import { ExpressionType } from '../ExpressionType.js';
55
import { SourceTableInterface } from '../SourceTableInterface.js';
6-
import { SqlTools } from '../sql_filters.js';
6+
import { AvailableTable, SqlTools } from '../sql_filters.js';
77
import { checkUnsupportedFeatures, isClauseError } from '../sql_support.js';
88
import { SyncRulesOptions } from '../SqlSyncRules.js';
99
import { TablePattern } from '../TablePattern.js';
@@ -49,7 +49,7 @@ export class SqlEventSourceQuery extends BaseSqlDataQuery {
4949
if (tableRef?.name == null) {
5050
throw new SqlRuleError('Must SELECT from a single table', sql, q.from?.[0]._location);
5151
}
52-
const alias: string = tableRef.alias ?? tableRef.name;
52+
const alias = AvailableTable.fromAst(tableRef);
5353

5454
const sourceTable = new TablePattern(tableRef.schema ?? options.defaultSchema, tableRef.name);
5555
let querySchema: QuerySchema | undefined = undefined;
@@ -99,7 +99,7 @@ export class SqlEventSourceQuery extends BaseSqlDataQuery {
9999
} else {
100100
extractors.push({
101101
extract: (tables, output) => {
102-
const row = tables[alias];
102+
const row = tables[alias.nameInSchema];
103103
for (let key in row) {
104104
if (key.startsWith('_')) {
105105
continue;
@@ -108,7 +108,7 @@ export class SqlEventSourceQuery extends BaseSqlDataQuery {
108108
}
109109
},
110110
getTypes(schema, into) {
111-
for (let column of schema.getColumns(alias)) {
111+
for (let column of schema.getColumns(alias.nameInSchema)) {
112112
into[column.name] ??= column;
113113
}
114114
}
@@ -136,7 +136,7 @@ export class SqlEventSourceQuery extends BaseSqlDataQuery {
136136

137137
evaluateRowWithErrors(table: SourceTableInterface, row: SqliteRow): EvaluatedEventRowWithErrors {
138138
try {
139-
const tables = { [this.table!]: this.addSpecialParameters(table, row) };
139+
const tables = { [this.table!.nameInSchema]: this.addSpecialParameters(table, row) };
140140

141141
const data = this.transformRow(tables);
142142
return {

0 commit comments

Comments
 (0)