diff --git a/packages/powersync/CHANGELOG.md b/packages/powersync/CHANGELOG.md index c323560e..d54482bb 100644 --- a/packages/powersync/CHANGELOG.md +++ b/packages/powersync/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.2.1 + +- Fix indexes incorrectly dropped after the first run. +- Fix `viewName` override causing `view "..." already exists` errors after the first run. + ## 1.2.0 This release improves the default log output and errors to better assist in debugging. diff --git a/packages/powersync/lib/src/schema_logic.dart b/packages/powersync/lib/src/schema_logic.dart index af4a4d4d..0641540c 100644 --- a/packages/powersync/lib/src/schema_logic.dart +++ b/packages/powersync/lib/src/schema_logic.dart @@ -187,13 +187,13 @@ void updateSchema(sqlite.Database db, Schema schema) { Set toRemove = {for (var row in existingViewRows) row['name']}; for (var table in schema.tables) { - toRemove.remove(table.name); + toRemove.remove(table.viewName); var createViewOp = createViewStatement(table); var triggers = createViewTriggerStatements(table); var existingRows = db.select( "SELECT sql FROM sqlite_master WHERE (type = 'view' AND name = ?) OR (type = 'trigger' AND tbl_name = ?) ORDER BY type DESC, name ASC", - [table.name, table.name]); + [table.viewName, table.viewName]); if (existingRows.isNotEmpty) { final dbSql = existingRows.map((row) => row['sql']).join('\n\n'); final generatedSql = @@ -203,7 +203,7 @@ void updateSchema(sqlite.Database db, Schema schema) { continue; } else { // View and/or triggers changed - delete and re-create. - db.execute('DROP VIEW ${quoteIdentifier(table.name)}'); + db.execute('DROP VIEW ${quoteIdentifier(table.viewName)}'); } } else { // New - create @@ -239,12 +239,33 @@ void _createTablesAndIndexes(sqlite.Database db, Schema schema) { "SELECT name, sql FROM sqlite_master WHERE type='index' AND name GLOB 'ps_data_*'"); final Set remainingTables = {}; - final Map remainingIndexes = {}; + final Map indexesToDrop = {}; + final List createIndexes = []; for (final row in existingTableRows) { remainingTables.add(row['name'] as String); } for (final row in existingIndexRows) { - remainingIndexes[row['name'] as String] = row['sql'] as String; + indexesToDrop[row['name'] as String] = row['sql'] as String; + } + + for (final table in schema.tables) { + for (final index in table.indexes) { + final fullName = index.fullName(table); + final sql = index.toSqlDefinition(table); + if (indexesToDrop.containsKey(fullName)) { + final existingSql = indexesToDrop[fullName]; + if (existingSql == sql) { + // No change (don't drop) + indexesToDrop.remove(fullName); + } else { + // Drop and create + createIndexes.add(sql); + } + } else { + // New index - create + createIndexes.add(sql); + } + } } for (final table in schema.tables) { @@ -274,26 +295,16 @@ void _createTablesAndIndexes(sqlite.Database db, Schema schema) { FROM ps_untyped WHERE type = ?""", [table.name]); } - - for (final index in table.indexes) { - final fullName = index.fullName(table); - final sql = index.toSqlDefinition(table); - if (remainingIndexes.containsKey(fullName)) { - final existingSql = remainingIndexes[fullName]; - if (existingSql == sql) { - continue; - } else { - db.execute('DROP INDEX ${quoteIdentifier(fullName)}'); - } - } - db.execute(sql); - } } - for (final indexName in remainingIndexes.keys) { + for (final indexName in indexesToDrop.keys) { db.execute('DROP INDEX ${quoteIdentifier(indexName)}'); } + for (final sql in createIndexes) { + db.execute(sql); + } + for (final tableName in remainingTables) { final typeMatch = RegExp("^ps_data__(.+)\$").firstMatch(tableName); if (typeMatch != null) { diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index 66f53cbc..dc4cf58f 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -1,5 +1,5 @@ name: powersync -version: 1.2.0 +version: 1.2.1 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Flutter SDK - keep PostgreSQL databases in sync with on-device SQLite databases. diff --git a/packages/powersync/test/schema_test.dart b/packages/powersync/test/schema_test.dart new file mode 100644 index 00000000..613808ce --- /dev/null +++ b/packages/powersync/test/schema_test.dart @@ -0,0 +1,141 @@ +import 'package:powersync/powersync.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +const testId = "2290de4f-0488-4e50-abed-f8e8eb1d0b42"; +final schema = Schema([ + Table('assets', [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.real('weight'), + Column.text('description'), + ], indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) + ]), + Table('customers', [Column.text('name'), Column.text('email')]), + Table.insertOnly('logs', [Column.text('level'), Column.text('content')]), + Table.localOnly('credentials', [Column.text('key'), Column.text('value')]), + Table('aliased', [Column.text('name')], viewName: 'test1') +]); + +void main() { + group('Schema Tests', () { + late String path; + + setUp(() async { + path = dbPath(); + await cleanDb(path: path); + }); + + test('Schema versioning', () async { + // Test that powersync_replace_schema() is a no-op when the schema is not + // modified. + + final powersync = await setupPowerSync(path: path, schema: schema); + + final versionBefore = await powersync.get('PRAGMA schema_version'); + await powersync.updateSchema(schema); + final versionAfter = await powersync.get('PRAGMA schema_version'); + + // No change + expect(versionAfter['schema_version'], + equals(versionBefore['schema_version'])); + + final schema2 = Schema([ + Table('assets', [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.real('weights'), + Column.text('description'), + ], indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) + ]), + Table('customers', [Column.text('name'), Column.text('email')]), + Table.insertOnly( + 'logs', [Column.text('level'), Column.text('content')]), + Table.localOnly( + 'credentials', [Column.text('key'), Column.text('value')]), + Table('aliased', [Column.text('name')], viewName: 'test1') + ]); + + await powersync.updateSchema(schema2); + + final versionAfter2 = await powersync.get('PRAGMA schema_version'); + + // Updated + expect(versionAfter2['schema_version'], + greaterThan(versionAfter['schema_version'])); + + final schema3 = Schema([ + Table('assets', [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.real('weights'), + Column.text('description'), + ], indexes: [ + Index('makemodel', + [IndexedColumn('make'), IndexedColumn.descending('model')]) + ]), + Table('customers', [Column.text('name'), Column.text('email')]), + Table.insertOnly( + 'logs', [Column.text('level'), Column.text('content')]), + Table.localOnly( + 'credentials', [Column.text('key'), Column.text('value')]), + Table('aliased', [Column.text('name')], viewName: 'test1') + ]); + + await powersync.updateSchema(schema3); + + final versionAfter3 = await powersync.get('PRAGMA schema_version'); + + // Updated again (index) + expect(versionAfter3['schema_version'], + greaterThan(versionAfter2['schema_version'])); + }); + + test('Indexing', () async { + final powersync = await setupPowerSync(path: path, schema: schema); + + final results = await powersync.execute( + 'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', ['test']); + + expect(results[0]['detail'], + contains('USING INDEX ps_data__assets__makemodel')); + + // Now drop the index + final schema2 = Schema([ + Table('assets', [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.real('weight'), + Column.text('description'), + ], indexes: []), + ]); + await powersync.updateSchema(schema2); + + // Execute instead of getAll so that we don't get a cached query plan + // from a different connection + final results2 = await powersync.execute( + 'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', ['test']); + + expect(results2[0]['detail'], contains('SCAN')); + }); + }); +}