Skip to content

Commit bf8b3a4

Browse files
authored
Merge pull request #57 from powersync-ja/fix-schema-changes
Fix schema changes
2 parents 8bbb184 + 728b958 commit bf8b3a4

File tree

4 files changed

+178
-21
lines changed

4 files changed

+178
-21
lines changed

packages/powersync/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.2.1
2+
3+
- Fix indexes incorrectly dropped after the first run.
4+
- Fix `viewName` override causing `view "..." already exists` errors after the first run.
5+
16
## 1.2.0
27

38
This release improves the default log output and errors to better assist in debugging.

packages/powersync/lib/src/schema_logic.dart

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,13 @@ void updateSchema(sqlite.Database db, Schema schema) {
187187
Set<String> toRemove = {for (var row in existingViewRows) row['name']};
188188

189189
for (var table in schema.tables) {
190-
toRemove.remove(table.name);
190+
toRemove.remove(table.viewName);
191191

192192
var createViewOp = createViewStatement(table);
193193
var triggers = createViewTriggerStatements(table);
194194
var existingRows = db.select(
195195
"SELECT sql FROM sqlite_master WHERE (type = 'view' AND name = ?) OR (type = 'trigger' AND tbl_name = ?) ORDER BY type DESC, name ASC",
196-
[table.name, table.name]);
196+
[table.viewName, table.viewName]);
197197
if (existingRows.isNotEmpty) {
198198
final dbSql = existingRows.map((row) => row['sql']).join('\n\n');
199199
final generatedSql =
@@ -203,7 +203,7 @@ void updateSchema(sqlite.Database db, Schema schema) {
203203
continue;
204204
} else {
205205
// View and/or triggers changed - delete and re-create.
206-
db.execute('DROP VIEW ${quoteIdentifier(table.name)}');
206+
db.execute('DROP VIEW ${quoteIdentifier(table.viewName)}');
207207
}
208208
} else {
209209
// New - create
@@ -239,12 +239,33 @@ void _createTablesAndIndexes(sqlite.Database db, Schema schema) {
239239
"SELECT name, sql FROM sqlite_master WHERE type='index' AND name GLOB 'ps_data_*'");
240240

241241
final Set<String> remainingTables = {};
242-
final Map<String, String> remainingIndexes = {};
242+
final Map<String, String> indexesToDrop = {};
243+
final List<String> createIndexes = [];
243244
for (final row in existingTableRows) {
244245
remainingTables.add(row['name'] as String);
245246
}
246247
for (final row in existingIndexRows) {
247-
remainingIndexes[row['name'] as String] = row['sql'] as String;
248+
indexesToDrop[row['name'] as String] = row['sql'] as String;
249+
}
250+
251+
for (final table in schema.tables) {
252+
for (final index in table.indexes) {
253+
final fullName = index.fullName(table);
254+
final sql = index.toSqlDefinition(table);
255+
if (indexesToDrop.containsKey(fullName)) {
256+
final existingSql = indexesToDrop[fullName];
257+
if (existingSql == sql) {
258+
// No change (don't drop)
259+
indexesToDrop.remove(fullName);
260+
} else {
261+
// Drop and create
262+
createIndexes.add(sql);
263+
}
264+
} else {
265+
// New index - create
266+
createIndexes.add(sql);
267+
}
268+
}
248269
}
249270

250271
for (final table in schema.tables) {
@@ -274,26 +295,16 @@ void _createTablesAndIndexes(sqlite.Database db, Schema schema) {
274295
FROM ps_untyped
275296
WHERE type = ?""", [table.name]);
276297
}
277-
278-
for (final index in table.indexes) {
279-
final fullName = index.fullName(table);
280-
final sql = index.toSqlDefinition(table);
281-
if (remainingIndexes.containsKey(fullName)) {
282-
final existingSql = remainingIndexes[fullName];
283-
if (existingSql == sql) {
284-
continue;
285-
} else {
286-
db.execute('DROP INDEX ${quoteIdentifier(fullName)}');
287-
}
288-
}
289-
db.execute(sql);
290-
}
291298
}
292299

293-
for (final indexName in remainingIndexes.keys) {
300+
for (final indexName in indexesToDrop.keys) {
294301
db.execute('DROP INDEX ${quoteIdentifier(indexName)}');
295302
}
296303

304+
for (final sql in createIndexes) {
305+
db.execute(sql);
306+
}
307+
297308
for (final tableName in remainingTables) {
298309
final typeMatch = RegExp("^ps_data__(.+)\$").firstMatch(tableName);
299310
if (typeMatch != null) {

packages/powersync/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: powersync
2-
version: 1.2.0
2+
version: 1.2.1
33
homepage: https://powersync.com
44
repository: https://github.com/powersync-ja/powersync.dart
55
description: PowerSync Flutter SDK - keep PostgreSQL databases in sync with on-device SQLite databases.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import 'package:powersync/powersync.dart';
2+
import 'package:test/test.dart';
3+
4+
import 'util.dart';
5+
6+
const testId = "2290de4f-0488-4e50-abed-f8e8eb1d0b42";
7+
final schema = Schema([
8+
Table('assets', [
9+
Column.text('created_at'),
10+
Column.text('make'),
11+
Column.text('model'),
12+
Column.text('serial_number'),
13+
Column.integer('quantity'),
14+
Column.text('user_id'),
15+
Column.real('weight'),
16+
Column.text('description'),
17+
], indexes: [
18+
Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')])
19+
]),
20+
Table('customers', [Column.text('name'), Column.text('email')]),
21+
Table.insertOnly('logs', [Column.text('level'), Column.text('content')]),
22+
Table.localOnly('credentials', [Column.text('key'), Column.text('value')]),
23+
Table('aliased', [Column.text('name')], viewName: 'test1')
24+
]);
25+
26+
void main() {
27+
group('Schema Tests', () {
28+
late String path;
29+
30+
setUp(() async {
31+
path = dbPath();
32+
await cleanDb(path: path);
33+
});
34+
35+
test('Schema versioning', () async {
36+
// Test that powersync_replace_schema() is a no-op when the schema is not
37+
// modified.
38+
39+
final powersync = await setupPowerSync(path: path, schema: schema);
40+
41+
final versionBefore = await powersync.get('PRAGMA schema_version');
42+
await powersync.updateSchema(schema);
43+
final versionAfter = await powersync.get('PRAGMA schema_version');
44+
45+
// No change
46+
expect(versionAfter['schema_version'],
47+
equals(versionBefore['schema_version']));
48+
49+
final schema2 = Schema([
50+
Table('assets', [
51+
Column.text('created_at'),
52+
Column.text('make'),
53+
Column.text('model'),
54+
Column.text('serial_number'),
55+
Column.integer('quantity'),
56+
Column.text('user_id'),
57+
Column.real('weights'),
58+
Column.text('description'),
59+
], indexes: [
60+
Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')])
61+
]),
62+
Table('customers', [Column.text('name'), Column.text('email')]),
63+
Table.insertOnly(
64+
'logs', [Column.text('level'), Column.text('content')]),
65+
Table.localOnly(
66+
'credentials', [Column.text('key'), Column.text('value')]),
67+
Table('aliased', [Column.text('name')], viewName: 'test1')
68+
]);
69+
70+
await powersync.updateSchema(schema2);
71+
72+
final versionAfter2 = await powersync.get('PRAGMA schema_version');
73+
74+
// Updated
75+
expect(versionAfter2['schema_version'],
76+
greaterThan(versionAfter['schema_version']));
77+
78+
final schema3 = Schema([
79+
Table('assets', [
80+
Column.text('created_at'),
81+
Column.text('make'),
82+
Column.text('model'),
83+
Column.text('serial_number'),
84+
Column.integer('quantity'),
85+
Column.text('user_id'),
86+
Column.real('weights'),
87+
Column.text('description'),
88+
], indexes: [
89+
Index('makemodel',
90+
[IndexedColumn('make'), IndexedColumn.descending('model')])
91+
]),
92+
Table('customers', [Column.text('name'), Column.text('email')]),
93+
Table.insertOnly(
94+
'logs', [Column.text('level'), Column.text('content')]),
95+
Table.localOnly(
96+
'credentials', [Column.text('key'), Column.text('value')]),
97+
Table('aliased', [Column.text('name')], viewName: 'test1')
98+
]);
99+
100+
await powersync.updateSchema(schema3);
101+
102+
final versionAfter3 = await powersync.get('PRAGMA schema_version');
103+
104+
// Updated again (index)
105+
expect(versionAfter3['schema_version'],
106+
greaterThan(versionAfter2['schema_version']));
107+
});
108+
109+
test('Indexing', () async {
110+
final powersync = await setupPowerSync(path: path, schema: schema);
111+
112+
final results = await powersync.execute(
113+
'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', ['test']);
114+
115+
expect(results[0]['detail'],
116+
contains('USING INDEX ps_data__assets__makemodel'));
117+
118+
// Now drop the index
119+
final schema2 = Schema([
120+
Table('assets', [
121+
Column.text('created_at'),
122+
Column.text('make'),
123+
Column.text('model'),
124+
Column.text('serial_number'),
125+
Column.integer('quantity'),
126+
Column.text('user_id'),
127+
Column.real('weight'),
128+
Column.text('description'),
129+
], indexes: []),
130+
]);
131+
await powersync.updateSchema(schema2);
132+
133+
// Execute instead of getAll so that we don't get a cached query plan
134+
// from a different connection
135+
final results2 = await powersync.execute(
136+
'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', ['test']);
137+
138+
expect(results2[0]['detail'], contains('SCAN'));
139+
});
140+
});
141+
}

0 commit comments

Comments
 (0)