Skip to content

Commit 23f50d7

Browse files
committed
Test errors
1 parent ae209de commit 23f50d7

File tree

7 files changed

+257
-19
lines changed

7 files changed

+257
-19
lines changed

packages/sync-rules/src/compiler/parser.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,7 @@ export class StreamQueryParser {
107107
return null;
108108
}
109109

110-
let where: Or;
111-
if (this.where) {
112-
where = this.compileFilterClause();
113-
} else {
114-
const emptyAnd: And = { terms: [] };
115-
where = { terms: [emptyAnd] };
116-
}
117-
110+
const where = this.compileFilterClause();
118111
const joined: SourceResultSet[] = [];
119112
for (const source of this.resultSets.values()) {
120113
if (source != this.primaryResultSet) {
@@ -153,7 +146,7 @@ export class StreamQueryParser {
153146
}
154147

155148
if (!options.forSubquery && node.columns) {
156-
this.processResultColumns(node.columns);
149+
this.processResultColumns(node, node.columns);
157150
}
158151

159152
this.warnUnsupported(node.groupBy, 'GROUP BY');
@@ -232,14 +225,14 @@ export class StreamQueryParser {
232225
return new RequestTableValuedResultSet(call.function.name, resolvedArguments, source);
233226
}
234227

235-
private processResultColumns(columns: SelectedColumn[]) {
228+
private processResultColumns(stmt: PGNode, columns: SelectedColumn[]) {
236229
const selectsFrom = (source: SourceResultSet, node: PGNode) => {
237230
if (source instanceof PhysicalSourceResultSet) {
238231
if (this.primaryResultSet == null) {
239232
this.primaryResultSet = source;
240233
} else if (this.primaryResultSet !== source) {
241234
this.errors.report(
242-
`Sync streams can only select from a single table, and this one already selects from '${this.primaryResultSet.tablePattern}'.`,
235+
`Sync streams can only select from a single table, and this one already selects from '${this.primaryResultSet.tablePattern.name}'.`,
243236
node
244237
);
245238
}
@@ -281,6 +274,10 @@ export class StreamQueryParser {
281274
}
282275
}
283276
}
277+
278+
if (this.primaryResultSet == null) {
279+
this.errors.report('Must have a result column selecting from a table', stmt);
280+
}
284281
}
285282

286283
private parseExpression(source: Expr, desugar: boolean): SyncExpression {
@@ -312,7 +309,7 @@ export class StreamQueryParser {
312309
} else {
313310
const result = scope.resolveResultSetForReference(name);
314311
if (result == null) {
315-
this.errors.report(`Table '${name}'has not been added in a FROM clause here.`, node);
312+
this.errors.report(`Table '${name}' has not been added in a FROM clause here.`, node);
316313
return null;
317314
}
318315

packages/sync-rules/src/compiler/sqlite.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,8 @@ export class PostgresToSqlite {
202202
break;
203203
}
204204
case 'unary': {
205-
const supported = supportedUnaryOperators[expr.op];
206-
if (supported == null) {
207-
this.errors.report('Unsupported unary operator', expr);
208-
return this.bogusExpression();
209-
}
205+
const [isSuffix, precedence] = supportedUnaryOperators[expr.op];
210206

211-
const [isSuffix, precedence] = supported;
212207
this.maybeParenthesis(outerPrecedence, precedence, () => {
213208
if (!isSuffix) this.addLexeme(expr.op);
214209
this.addExpression(expr.operand, precedence);
@@ -282,6 +277,7 @@ export class PostgresToSqlite {
282277
this.errors.report('Invalid position for subqueries. Subqueries are only supported in WHERE clauses.', expr);
283278
break;
284279
default:
280+
expr.type;
285281
this.bogusExpression();
286282
this.errors.report('This expression is not supported by PowerSync', expr);
287283
}
@@ -396,7 +392,7 @@ const supportedBinaryOperators: Partial<Record<BinaryOperator, Precedence>> = {
396392
['->>' as BinaryOperator]: Precedence.concat
397393
};
398394

399-
const supportedUnaryOperators: Partial<Record<UnaryOperator, [boolean, Precedence]>> = {
395+
const supportedUnaryOperators: Record<UnaryOperator, [boolean, Precedence]> = {
400396
NOT: [false, Precedence.not],
401397
'IS NULL': [true, Precedence.equals],
402398
'IS NOT NULL': [true, Precedence.equals],

packages/sync-rules/test/src/compiler/__snapshots__/advanced.test.ts.snap

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,62 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`new sync stream features > in array 1`] = `
4+
{
5+
"buckets": [
6+
{
7+
"hash": 270609616,
8+
"sources": [
9+
0,
10+
],
11+
"uniqueName": "stream|0",
12+
},
13+
],
14+
"dataSources": [
15+
{
16+
"columns": [
17+
"star",
18+
],
19+
"filters": [
20+
{
21+
"sql": "? IN ('public', 'archived')",
22+
"values": [
23+
{
24+
"column": "state",
25+
"sqlPosition": [
26+
0,
27+
1,
28+
],
29+
},
30+
],
31+
},
32+
],
33+
"hash": 104743926,
34+
"partition_by": [],
35+
"table": {
36+
"connection": "default",
37+
"schema": "",
38+
"table": "notes",
39+
},
40+
},
41+
],
42+
"parameterIndexes": [],
43+
"queriers": [
44+
{
45+
"bucket": 0,
46+
"lookupStages": [],
47+
"requestFilters": [],
48+
"sourceInstantiation": [],
49+
"stream": {
50+
"isSubscribedByDefault": true,
51+
"name": "stream",
52+
"priority": 3,
53+
},
54+
},
55+
],
56+
"version": "unstable",
57+
}
58+
`;
59+
360
exports[`new sync stream features > joins feedback > response 1 1`] = `
461
{
562
"buckets": [

packages/sync-rules/test/src/compiler/advanced.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,10 @@ where uas.user_id = auth.user_id()
176176
expect(compileSingleStreamAndSerialize(stream)).toMatchSnapshot();
177177
});
178178
});
179+
180+
test('in array', () => {
181+
expect(
182+
compileSingleStreamAndSerialize(`SELECT * FROM notes WHERE state IN ARRAY['public', 'archived']`)
183+
).toMatchSnapshot();
184+
});
179185
});

packages/sync-rules/test/src/compiler/errors.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,97 @@ describe('compilation errors', () => {
1111
]);
1212
});
1313

14+
test('not selecting from anything', () => {
15+
expect(compilationErrorsForSingleStream('SELECT 1, 2, 3')).toStrictEqual([
16+
{
17+
message: 'Must have a result column selecting from a table',
18+
source: 'SELECT 1, 2, 3'
19+
}
20+
]);
21+
});
22+
23+
test('selecting table-valued function', () => {
24+
expect(compilationErrorsForSingleStream("SELECT * FROM json_each(auth.parameter('x'))")).toStrictEqual([
25+
{
26+
message: 'Sync streams can only select from actual tables',
27+
source: '*'
28+
},
29+
{
30+
message: 'Must have a result column selecting from a table',
31+
source: "SELECT * FROM json_each(auth.parameter('x'))"
32+
}
33+
]);
34+
});
35+
36+
test('join with using', () => {
37+
expect(compilationErrorsForSingleStream('SELECT u.* FROM users u INNER JOIN orgs USING (org_id)')).toStrictEqual([
38+
{
39+
message: 'USING is not supported',
40+
source: 'SELECT u.* FROM users u INNER JOIN orgs USING (org_id)'
41+
}
42+
]);
43+
});
44+
45+
test('selecting connection value', () => {
46+
expect(compilationErrorsForSingleStream("SELECT u.*, auth.parameter('x') FROM users u;")).toStrictEqual([
47+
{
48+
message: 'This attempts to sync a connection parameter. Only values from the source database can be synced.',
49+
source: "auth.parameter('x')"
50+
}
51+
]);
52+
});
53+
54+
test('selecting from multiple tables', () => {
55+
expect(
56+
compilationErrorsForSingleStream(
57+
'SELECT u.*, orgs.* FROM users u INNER JOIN orgs ON u.id = auth.user_id() AND u.org = orgs.id'
58+
)
59+
).toStrictEqual([
60+
{
61+
message: "Sync streams can only select from a single table, and this one already selects from 'users'.",
62+
source: 'orgs.*'
63+
}
64+
]);
65+
});
66+
67+
test('subexpressions from different rows', () => {
68+
expect(
69+
compilationErrorsForSingleStream(
70+
"SELECT u.* FROM users u INNER JOIN orgs WHERE u.name || orgs.name = subscription.parameter('a')"
71+
)
72+
).toStrictEqual([
73+
{
74+
message:
75+
"This expression already references 'users', so it can't also reference data from this row unless the two are compared with an equals operator.",
76+
source: 'orgs.name'
77+
}
78+
]);
79+
});
80+
81+
test('ambigious reference', () => {
82+
expect(
83+
compilationErrorsForSingleStream('SELECT u.* FROM users u INNER JOIN orgs ON u.org = orgs.id WHERE is_public')
84+
).toStrictEqual([
85+
{
86+
message: 'Invalid unqualified reference since multiple tables are in scope',
87+
source: 'is_public'
88+
}
89+
]);
90+
});
91+
92+
test('table that has not been added', () => {
93+
expect(compilationErrorsForSingleStream('SELECT users.* FROM orgs;')).toStrictEqual([
94+
{
95+
message: "Table 'users' has not been added in a FROM clause here.",
96+
source: 'users.*'
97+
},
98+
{
99+
message: 'Must have a result column selecting from a table',
100+
source: 'SELECT users.* FROM orgs'
101+
}
102+
]);
103+
});
104+
14105
test('IN operator with static left clause', () => {
15106
expect(
16107
compilationErrorsForSingleStream("SELECT * FROM issues WHERE 'static' IN (SELECT id FROM users WHERE is_admin)")
@@ -78,4 +169,40 @@ describe('compilation errors', () => {
78169
{ message: 'Invalid schema in function name', source: 'request.user_id' }
79170
]);
80171
});
172+
173+
test('full join', () => {
174+
expect(compilationErrorsForSingleStream('select i.* from issues i FULL JOIN users u')).toStrictEqual([
175+
{ message: 'FULL JOIN is not supported', source: 'select i.* from issues i FULL JOIN users u' }
176+
]);
177+
});
178+
179+
test('subquery star', () => {
180+
expect(compilationErrorsForSingleStream('select * from users where org in (select * from orgs)')).toStrictEqual([
181+
{ message: 'Must return a single expression column', source: 'select * from orgs' }
182+
]);
183+
});
184+
185+
test('filter star', () => {
186+
expect(compilationErrorsForSingleStream('select * from users where users.*')).toStrictEqual([
187+
{ message: '* columns are not supported here', source: 'users.*' }
188+
]);
189+
});
190+
191+
test('request parameter wrong count', () => {
192+
expect(
193+
compilationErrorsForSingleStream("select * from users where id = auth.parameter('foo', 'bar')")
194+
).toStrictEqual([{ message: 'Expected a single argument here', source: 'auth.parameter' }]);
195+
});
196+
197+
test('user_id on wrong schema', () => {
198+
expect(compilationErrorsForSingleStream('select * from users where id = subscription.user_id()')).toStrictEqual([
199+
{ message: '.user_id() is only available on auth schema', source: 'subscription.user_id' }
200+
]);
201+
});
202+
203+
test('unknown request function', () => {
204+
expect(compilationErrorsForSingleStream('select * from users where id = subscription.whatever()')).toStrictEqual([
205+
{ message: 'Unknown request function', source: 'subscription.whatever' }
206+
]);
207+
});
81208
});

packages/sync-rules/test/src/compiler/reuse.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ describe('can reuse elements', () => {
2424
expect(compiled.buckets).toHaveLength(1);
2525
});
2626

27+
test('between streams with different outputs', () => {
28+
const compiled = compileToSyncPlanWithoutErrors([
29+
{ name: 'a', queries: ['SELECT id, foo FROM profiles WHERE "user" = auth.user_id()'] },
30+
{
31+
name: 'b',
32+
queries: [
33+
`SELECT id, bar FROM profiles WHERE "user" IN (SELECT member FROM orgs WHERE id = auth.parameter('org'))`
34+
]
35+
}
36+
]);
37+
38+
expect(compiled.buckets).toHaveLength(2);
39+
});
40+
2741
test('no reuse on streams with different sources', () => {
2842
const compiled = compileToSyncPlanWithoutErrors([
2943
{ name: 'a', queries: ['SELECT * FROM products'] },

packages/sync-rules/test/src/compiler/sqlite.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ describe('sqlite conversion', () => {
5555
expectNoErrors("1 -> '$.foo'", "1 -> '$.foo'");
5656
});
5757

58+
test('unary', () => {
59+
expectNoErrors('1 IS NOT NULL', '1 IS NOT NULL');
60+
expectNoErrors('NOT 1', 'NOT 1');
61+
});
62+
5863
describe('errors', () => {
5964
describe('function', () => {
6065
test('too few args', () => {
@@ -92,6 +97,33 @@ describe('sqlite conversion', () => {
9297
}
9398
]);
9499
});
100+
101+
test('unsupported feature', () => {
102+
expect(translate('upper(DISTINCT 1)')[1]).toStrictEqual([
103+
{
104+
message: 'DISTINCT, ORDER BY, FILTER and OVER clauses are not supported',
105+
source: 'upper'
106+
}
107+
]);
108+
});
109+
110+
test('unknown function', () => {
111+
expect(translate('unknown_function()')[1]).toStrictEqual([
112+
{
113+
message: 'Unknown function',
114+
source: 'unknown_function'
115+
}
116+
]);
117+
});
118+
119+
test('explicitly forbidden function', () => {
120+
expect(translate('random()')[1]).toStrictEqual([
121+
{
122+
message: 'Forbidden call: Sync definitions must be deterministic.',
123+
source: 'random'
124+
}
125+
]);
126+
});
95127
});
96128

97129
test('binary', () => {
@@ -120,6 +152,15 @@ describe('sqlite conversion', () => {
120152
}
121153
]);
122154
});
155+
156+
test('unknown expression', () => {
157+
expect(translate('ARRAY[1,2,3]')[1]).toStrictEqual([
158+
{
159+
message: 'This expression is not supported by PowerSync',
160+
source: 'ARRAY[1,2,3]'
161+
}
162+
]);
163+
});
123164
});
124165
});
125166

0 commit comments

Comments
 (0)