Skip to content

Commit c7c3f8c

Browse files
authored
Merge pull request #42 from powersync-ja/validate-id-column
Validate id column for "select *"
2 parents 979ab0c + 876f4a0 commit c7c3f8c

File tree

5 files changed

+44
-2
lines changed

5 files changed

+44
-2
lines changed

.changeset/brown-horses-whisper.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+
Warn when id column is not present in "select \* from ..."

packages/sync-rules/src/ExpressionType.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,8 @@ export class ExpressionType {
6767
isNumericOnly() {
6868
return this.typeFlags != TYPE_NONE && (this.typeFlags & (TYPE_INTEGER | TYPE_REAL)) == this.typeFlags;
6969
}
70+
71+
isNone() {
72+
return this.typeFlags == TYPE_NONE;
73+
}
7074
}

packages/sync-rules/src/SqlDataQuery.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ export class SqlDataQuery {
106106
rows.tools = tools;
107107

108108
let hasId = false;
109+
let hasWildcard = false;
110+
109111
for (let column of q.columns ?? []) {
110112
const name = tools.getOutputName(column);
111113
if (name != '*') {
@@ -140,12 +142,28 @@ export class SqlDataQuery {
140142
}
141143
});
142144
}
143-
if (name == 'id' || name == '*') {
145+
if (name == 'id') {
144146
hasId = true;
147+
} else if (name == '*') {
148+
hasWildcard = true;
149+
if (querySchema == null) {
150+
// Not performing schema-based validation - assume there is an id
151+
hasId = true;
152+
} else {
153+
const idType = querySchema.getType(alias, 'id');
154+
if (!idType.isNone()) {
155+
hasId = true;
156+
}
157+
}
145158
}
146159
}
147160
if (!hasId) {
148-
rows.errors.push(new SqlRuleError(`Query must return an "id" column`, sql, q.columns?.[0]._location));
161+
const error = new SqlRuleError(`Query must return an "id" column`, sql, q.columns?.[0]._location);
162+
if (hasWildcard) {
163+
// Schema-based validations are always warnings
164+
error.type = 'warning';
165+
}
166+
rows.errors.push(error);
149167
}
150168
rows.errors.push(...tools.errors);
151169
return rows;

packages/sync-rules/test/src/data_queries.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ describe('data queries', () => {
142142
type: 'warning'
143143
}
144144
]);
145+
146+
const q4 = SqlDataQuery.fromSql('q4', [], 'SELECT * FROM other', schema);
147+
expect(q4.errors).toMatchObject([
148+
{
149+
message: `Query must return an "id" column`,
150+
type: 'warning'
151+
}
152+
]);
153+
154+
const q5 = SqlDataQuery.fromSql('q5', [], 'SELECT other_id as id, * FROM other', schema);
155+
expect(q5.errors).toMatchObject([]);
145156
});
146157

147158
test('invalid query - invalid IN', function () {

packages/sync-rules/test/src/util.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export const BASIC_SCHEMA = new StaticSchema([
3232
{ name: 'count', pg_type: 'int4' },
3333
{ name: 'owner_id', pg_type: 'uuid' }
3434
]
35+
},
36+
{
37+
name: 'other',
38+
columns: [{ name: 'other_id', pg_type: 'uuid' }]
3539
}
3640
]
3741
}

0 commit comments

Comments
 (0)