Skip to content

Commit 4747b8b

Browse files
committed
Add test migrating between local and synced
1 parent 48d69d4 commit 4747b8b

File tree

2 files changed

+117
-25
lines changed

2 files changed

+117
-25
lines changed

crates/core/src/schema/management.rs

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,36 +38,55 @@ fn update_tables(db: *mut sqlite::sqlite3, schema: &Schema) -> Result<(), PowerS
3838
{
3939
// In a block so that all statements are finalized before dropping tables.
4040
for table in &schema.tables {
41-
if let Some(_) = existing_tables.remove(&*table.name) {
42-
// This table exists already, nothing to do.
43-
// TODO: Handle switch between local only <-> regular tables?
44-
} else {
45-
// New table.
46-
let quoted_internal_name = quote_identifier(&table.internal_name());
47-
48-
db.exec_safe(&format!(
49-
"CREATE TABLE {:}(id TEXT PRIMARY KEY NOT NULL, data TEXT)",
50-
quoted_internal_name
51-
))
52-
.into_db_result(db)?;
41+
if let Some(existing) = existing_tables.remove(&*table.name) {
42+
if existing.local_only && !table.local_only() {
43+
// Migrate from a local-only to a synced table. Because none of the local writes
44+
// would have created CRUD entries, we'll have to re-create the table from
45+
// scratch.
46+
47+
// To delete the old existing table in the end.
48+
existing_tables.insert(&existing.name, existing);
49+
} else if !existing.local_only && table.local_only() {
50+
// Migrate from a synced table to a local-only table. We can keep existing rows
51+
// and will also keep existing CRUD data to be uploaded before the switch.
52+
db.exec_safe(&format!(
53+
"ALTER TABLE {} RENAME TO {}",
54+
quote_identifier(&existing.internal_name),
55+
quote_identifier(&table.internal_name()),
56+
))
57+
.into_db_result(db)?;
58+
continue;
59+
} else {
60+
// Identical table exists already, nothing to do.
61+
continue;
62+
}
63+
}
64+
65+
// New table.
66+
let quoted_internal_name = quote_identifier(&table.internal_name());
67+
68+
db.exec_safe(&format!(
69+
"CREATE TABLE {:}(id TEXT PRIMARY KEY NOT NULL, data TEXT)",
70+
quoted_internal_name
71+
))
72+
.into_db_result(db)?;
5373

54-
if !table.local_only() {
55-
// MOVE data if any
56-
db.exec_text(
57-
&format!(
58-
"INSERT INTO {:}(id, data)
74+
if !table.local_only() {
75+
// MOVE data if any
76+
db.exec_text(
77+
&format!(
78+
"INSERT INTO {:}(id, data)
5979
SELECT id, data
6080
FROM ps_untyped
6181
WHERE type = ?",
62-
quoted_internal_name
63-
),
64-
&table.name,
65-
)
66-
.into_db_result(db)?;
82+
quoted_internal_name
83+
),
84+
&table.name,
85+
)
86+
.into_db_result(db)?;
6787

68-
// language=SQLite
69-
db.exec_text("DELETE FROM ps_untyped WHERE type = ?", &table.name)?;
70-
}
88+
// language=SQLite
89+
db.exec_text("DELETE FROM ps_untyped WHERE type = ?", &table.name)?;
7190
}
7291
}
7392

dart/test/schema_test.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,79 @@ void main() {
4545
greaterThan(versionAfter2['schema_version'] as int));
4646
});
4747

48+
group('migrate tables', () {
49+
final local = {
50+
"tables": [
51+
{
52+
"name": "users",
53+
"local_only": true,
54+
"insert_only": false,
55+
"columns": [
56+
{"name": "name", "type": "TEXT"},
57+
],
58+
},
59+
]
60+
};
61+
62+
final synced = {
63+
"tables": [
64+
{
65+
"name": "users",
66+
"local_only": false,
67+
"insert_only": false,
68+
"columns": [
69+
{"name": "name", "type": "TEXT"},
70+
],
71+
},
72+
]
73+
};
74+
75+
test('from synced to local', () {
76+
// Start with synced table, and sync row
77+
db.execute('SELECT powersync_replace_schema(?)', [json.encode(synced)]);
78+
db.execute(
79+
'INSERT INTO ps_data__users (id, data) VALUES (?, ?)',
80+
[
81+
'synced-id',
82+
json.encode({'name': 'name'})
83+
],
84+
);
85+
86+
// Migrate to local table.
87+
db.execute('SELECT powersync_replace_schema(?)', [json.encode(local)]);
88+
89+
// The synced table should not exist anymore.
90+
expect(() => db.select('SELECT * FROM ps_data__users'),
91+
throwsA(isA<SqliteException>()));
92+
93+
// Data should still be there.
94+
expect(db.select('SELECT * FROM users'), [
95+
{'id': 'synced-id', 'name': 'name'}
96+
]);
97+
98+
// Inserting into local-only table should not record CRUD item.
99+
db.execute(
100+
'INSERT INTO users (id, name) VALUES (uuid(), ?)', ['local']);
101+
expect(db.select('SELECT * FROM ps_crud'), isEmpty);
102+
});
103+
104+
test('from local to synced', () {
105+
// Start with local table, and local row
106+
db.execute('SELECT powersync_replace_schema(?)', [json.encode(local)]);
107+
db.execute(
108+
'INSERT INTO users (id, name) VALUES (uuid(), ?)', ['local']);
109+
110+
// Migrate to synced table. Because the previous local write would never
111+
// get uploaded, this clears local data.
112+
db.execute('SELECT powersync_replace_schema(?)', [json.encode(synced)]);
113+
expect(db.select('SELECT * FROM users'), isEmpty);
114+
115+
// The local table should not exist anymore.
116+
expect(() => db.select('SELECT * FROM ps_data_local__users'),
117+
throwsA(isA<SqliteException>()));
118+
});
119+
});
120+
48121
group('metadata', () {
49122
// This is a special because we have two delete triggers when
50123
// include_metadata is true (one for actual `DELETE` statements and one

0 commit comments

Comments
 (0)