Skip to content

Commit 7755700

Browse files
committed
Merge remote-tracking branch 'origin/main' into update-core-lib
2 parents a2538f6 + 4feb65c commit 7755700

File tree

8 files changed

+254
-4
lines changed

8 files changed

+254
-4
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,35 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## 2024-08-23
7+
8+
### Changes
9+
10+
---
11+
12+
Packages with breaking changes:
13+
14+
- There are no breaking changes in this release.
15+
16+
Packages with other changes:
17+
18+
- [`powersync` - `v1.7.0`](#powersync---v170)
19+
- [`powersync_attachments_helper` - `v0.6.5+1`](#powersync_attachments_helper---v0651)
20+
21+
Packages with dependency updates only:
22+
23+
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
24+
25+
- `powersync_attachments_helper` - `v0.6.5+1`
26+
27+
---
28+
29+
#### `powersync` - `v1.7.0`
30+
31+
- **FEAT**: Include schema validation check
32+
- **FEAT**: Include new table check for maximum number of columns allowed
33+
34+
635
## 2024-08-21
736

837
### Changes

RELEASING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ e.g powersync-v1.6.4, powersync_attachments_helper-v0.6.3+1, etc.
2525
git push --follow-tags
2626
```
2727

28+
**Note: This will launch the `release.yaml` and `publish.yaml` github actions in `.github/workflows`. So only run it when you are absolutely sure you want to release.**
29+
2830
A version bump and tag push for `powersync` will also create a draft github release for the powersync web worker. The worker needs to be manually published in the GitHub [releases](https://github.com/powersync-ja/powersync.dart/releases).

melos.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ command:
2727

2828
scripts:
2929
prepare:
30-
description: Download SQLite3 WASM for demos
30+
description: Download and prepare assets for demos
3131
run: |
32-
melos bootstrap && melos prepare:assets && dart ./scripts/compile_webworker.dart && dart ./scripts/init_powersync_core_binary.dart
33-
dart ./scripts/download_core_binary_demos.dart && melos prepare:demos
32+
melos bootstrap && melos prepare:assets && dart ./scripts/compile_webworker.dart && dart ./scripts/init_powersync_core_binary.dart && dart ./scripts/download_core_binary_demos.dart && melos prepare:demos
3433
3534
prepare:demos:
3635
description: Download SQLite3 wasm for demos

packages/powersync/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.7.0
2+
3+
- **FEAT**: Include schema validation check
4+
- **FEAT**: Include new table check for maximum number of columns allowed
5+
16
## 1.6.7
27

38
- **CHORE**: Update dependency powersync_flutter_libs

packages/powersync/lib/src/database/native/native_powersync_database.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ class PowerSyncDatabaseImpl
261261
if (disconnecter != null) {
262262
throw AssertionError('Cannot update schema while connected');
263263
}
264+
schema.validate();
264265
this.schema = schema;
265266
return updateSchemaInIsolate(database, schema);
266267
}

packages/powersync/lib/src/database/web/web_powersync_database.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ class PowerSyncDatabaseImpl
207207
if (disconnecter != null) {
208208
throw AssertionError('Cannot update schema while connected');
209209
}
210+
schema.validate();
210211
this.schema = schema;
211212
return database.writeLock((tx) => schema_logic.updateSchema(tx, schema));
212213
}

packages/powersync/lib/src/schema.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ class Schema {
1111
const Schema(this.tables);
1212

1313
Map<String, dynamic> toJson() => {'tables': tables};
14+
15+
void validate() {
16+
for (var table in tables) {
17+
table.validate();
18+
}
19+
}
1420
}
1521

1622
/// A single table in the schema.
@@ -33,6 +39,10 @@ class Table {
3339
/// Override the name for the view
3440
final String? _viewNameOverride;
3541

42+
/// There is maximum of 127 arguments for any function in SQLite. Currently we use json_object which uses 1 arg per key (column name)
43+
/// and one per value, which limits it to 63 arguments.
44+
final int maxNumberOfColumns = 63;
45+
3646
/// Internal use only.
3747
///
3848
/// Name of the table that stores the underlying data.
@@ -84,9 +94,16 @@ class Table {
8494

8595
/// Check that there are no issues in the table definition.
8696
void validate() {
97+
if (columns.length > maxNumberOfColumns) {
98+
throw AssertionError(
99+
"Table $name has more than $maxNumberOfColumns columns, which is not supported");
100+
}
101+
87102
if (invalidSqliteCharacters.hasMatch(name)) {
88103
throw AssertionError("Invalid characters in table name: $name");
89-
} else if (_viewNameOverride != null &&
104+
}
105+
106+
if (_viewNameOverride != null &&
90107
invalidSqliteCharacters.hasMatch(_viewNameOverride)) {
91108
throw AssertionError(
92109
"Invalid characters in view name: $_viewNameOverride");

packages/powersync/test/schema_test.dart

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,201 @@ void main() {
142142

143143
expect(results2[0]['detail'], contains('SCAN'));
144144
});
145+
146+
test('Validation runs on setup', () async {
147+
final schema = Schema([
148+
Table('#assets', [
149+
Column.text('name'),
150+
]),
151+
]);
152+
153+
try {
154+
await testUtils.setupPowerSync(path: path, schema: schema);
155+
} catch (e) {
156+
expect(
157+
e,
158+
isA<AssertionError>().having((e) => e.message, 'message',
159+
'Invalid characters in table name: #assets'));
160+
}
161+
});
162+
163+
test('Validation runs on update', () async {
164+
final schema = Schema([
165+
Table('works', [
166+
Column.text('name'),
167+
]),
168+
]);
169+
170+
final powersync =
171+
await testUtils.setupPowerSync(path: path, schema: schema);
172+
173+
final schema2 = Schema([
174+
Table('#notworking', [
175+
Column.text('created_at'),
176+
]),
177+
]);
178+
179+
try {
180+
powersync.updateSchema(schema2);
181+
} catch (e) {
182+
expect(
183+
e,
184+
isA<AssertionError>().having((e) => e.message, 'message',
185+
'Invalid characters in table name: #notworking'));
186+
}
187+
});
188+
});
189+
190+
group('Table', () {
191+
test('Create a synced table', () {
192+
final table = Table('users', [
193+
Column('name', ColumnType.text),
194+
Column('age', ColumnType.integer),
195+
]);
196+
197+
expect(table.name, equals('users'));
198+
expect(table.columns.length, equals(2));
199+
expect(table.localOnly, isFalse);
200+
expect(table.insertOnly, isFalse);
201+
expect(table.internalName, equals('ps_data__users'));
202+
expect(table.viewName, equals('users'));
203+
});
204+
205+
test('Create a local-only table', () {
206+
final table = Table.localOnly(
207+
'local_users',
208+
[
209+
Column('name', ColumnType.text),
210+
],
211+
viewName: 'local_user_view');
212+
213+
expect(table.name, equals('local_users'));
214+
expect(table.localOnly, isTrue);
215+
expect(table.insertOnly, isFalse);
216+
expect(table.internalName, equals('ps_data_local__local_users'));
217+
expect(table.viewName, equals('local_user_view'));
218+
});
219+
220+
test('Create an insert-only table', () {
221+
final table = Table.insertOnly('logs', [
222+
Column('message', ColumnType.text),
223+
Column('timestamp', ColumnType.integer),
224+
]);
225+
226+
expect(table.name, equals('logs'));
227+
expect(table.localOnly, isFalse);
228+
expect(table.insertOnly, isTrue);
229+
expect(table.internalName, equals('ps_data__logs'));
230+
expect(table.indexes, isEmpty);
231+
});
232+
233+
test('Access column by name', () {
234+
final table = Table('products', [
235+
Column('name', ColumnType.text),
236+
Column('price', ColumnType.real),
237+
]);
238+
239+
expect(table['name'].type, equals(ColumnType.text));
240+
expect(table['price'].type, equals(ColumnType.real));
241+
expect(() => table['nonexistent'], throwsStateError);
242+
});
243+
244+
test('Validate table name', () {
245+
final invalidTableName =
246+
Table('#invalid_table_name', [Column('name', ColumnType.text)]);
247+
248+
expect(
249+
() => invalidTableName.validate(),
250+
throwsA(
251+
isA<AssertionError>().having(
252+
(e) => e.message,
253+
'message',
254+
'Invalid characters in table name: #invalid_table_name',
255+
),
256+
),
257+
);
258+
});
259+
260+
test('Validate view name', () {
261+
final invalidTableName = Table(
262+
'valid_table_name', [Column('name', ColumnType.text)],
263+
viewName: '#invalid_view_name');
264+
265+
expect(
266+
() => invalidTableName.validate(),
267+
throwsA(
268+
isA<AssertionError>().having(
269+
(e) => e.message,
270+
'message',
271+
'Invalid characters in view name: #invalid_view_name',
272+
),
273+
),
274+
);
275+
});
276+
277+
test('Validate table definition', () {
278+
final validTable = Table('valid_table', [
279+
Column('name', ColumnType.text),
280+
Column('age', ColumnType.integer),
281+
]);
282+
283+
expect(() => validTable.validate(), returnsNormally);
284+
});
285+
286+
test('Table with id column', () {
287+
final invalidTable = Table('invalid_table', [
288+
Column('id', ColumnType.integer), // Duplicate 'id' column
289+
Column('name', ColumnType.text),
290+
]);
291+
292+
expect(
293+
() => invalidTable.validate(),
294+
throwsA(
295+
isA<AssertionError>().having(
296+
(e) => e.message,
297+
'message',
298+
'invalid_table: id column is automatically added, custom id columns are not supported',
299+
),
300+
),
301+
);
302+
});
303+
304+
test('Table with too many columns', () {
305+
final List<Column> manyColumns = List.generate(
306+
64, // Exceeds MAX_NUMBER_OF_COLUMNS
307+
(index) => Column('col$index', ColumnType.text),
308+
);
309+
310+
final tableTooManyColumns = Table('too_many_columns', manyColumns);
311+
312+
expect(
313+
() => tableTooManyColumns.validate(),
314+
throwsA(
315+
isA<AssertionError>().having(
316+
(e) => e.message,
317+
'message',
318+
'Table too_many_columns has more than 63 columns, which is not supported',
319+
),
320+
),
321+
);
322+
});
323+
324+
test('toJson method', () {
325+
final table = Table('users', [
326+
Column('name', ColumnType.text),
327+
Column('age', ColumnType.integer),
328+
], indexes: [
329+
Index('name_index', [IndexedColumn('name')])
330+
]);
331+
332+
final json = table.toJson();
333+
334+
expect(json['name'], equals('users'));
335+
expect(json['view_name'], isNull);
336+
expect(json['local_only'], isFalse);
337+
expect(json['insert_only'], isFalse);
338+
expect(json['columns'].length, equals(2));
339+
expect(json['indexes'].length, equals(1));
340+
});
145341
});
146342
}

0 commit comments

Comments
 (0)