Schema 11: Playlists.cloudId fuer die einseitige Sicherung

This commit is contained in:
Hermes (Server)
2026-08-27 13:31:33 +02:00
parent 11a01d1a09
commit 3cd88565ec
3 changed files with 166 additions and 5 deletions
+28 -1
View File
@@ -72,6 +72,10 @@ class Playlists extends Table {
IntColumn get updatedAtMs => integer()(); IntColumn get updatedAtMs => integer()();
BoolColumn get deleted => boolean().withDefault(const Constant(false))(); BoolColumn get deleted => boolean().withDefault(const Constant(false))();
/// ID derselben Playlist am Melo-Server, sobald sie einmal gesichert
/// wurde. `null` heißt: nur auf diesem Gerät.
TextColumn get cloudId => text().nullable()();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }
@@ -148,7 +152,7 @@ class MeloDb extends _$MeloDb {
MeloDb([QueryExecutor? executor]) : super(executor ?? _open()); MeloDb([QueryExecutor? executor]) : super(executor ?? _open());
@override @override
int get schemaVersion => 10; int get schemaVersion => 11;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -190,6 +194,9 @@ class MeloDb extends _$MeloDb {
if (from < 10) { if (from < 10) {
await m.createTable(downloads); await m.createTable(downloads);
} }
if (from < 11) {
await m.addColumn(playlists, playlists.cloudId);
}
}, },
); );
@@ -284,6 +291,26 @@ class MeloDb extends _$MeloDb {
); );
} }
Future<Playlist?> playlistById(String id) =>
(select(playlists)..where((p) => p.id.equals(id))).getSingleOrNull();
Future<void> setPlaylistCloudId(String id, String cloudId) async {
await (update(playlists)..where((p) => p.id.equals(id)))
.write(PlaylistsCompanion(cloudId: Value(cloudId)));
}
/// Wie viele Playlisten es hier gibt — **inklusive Grabsteinen**.
///
/// Grundlage der Wiederherstellung: nur eine wirklich leere Tabelle gilt
/// als Neuinstallation. Wer alle Playlisten selbst gelöscht hat, soll sie
/// nicht vom Server zurückbekommen.
Future<int> countPlaylists() async {
final zaehler = playlists.id.count();
final zeile = await (selectOnly(playlists)..addColumns([zaehler]))
.getSingle();
return zeile.read(zaehler) ?? 0;
}
// === PlaylistSongs === // === PlaylistSongs ===
Stream<List<Song>> watchPlaylistSongs(String playlistId) { Stream<List<Song>> watchPlaylistSongs(String playlistId) {
final query = select(songs).join([ final query = select(songs).join([
+81 -4
View File
@@ -1322,6 +1322,17 @@ class $PlaylistsTable extends Playlists
), ),
defaultValue: const Constant(false), defaultValue: const Constant(false),
); );
static const VerificationMeta _cloudIdMeta = const VerificationMeta(
'cloudId',
);
@override
late final GeneratedColumn<String> cloudId = GeneratedColumn<String>(
'cloud_id',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
id, id,
@@ -1330,6 +1341,7 @@ class $PlaylistsTable extends Playlists
createdAtMs, createdAtMs,
updatedAtMs, updatedAtMs,
deleted, deleted,
cloudId,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@@ -1393,6 +1405,12 @@ class $PlaylistsTable extends Playlists
deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta), deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta),
); );
} }
if (data.containsKey('cloud_id')) {
context.handle(
_cloudIdMeta,
cloudId.isAcceptableOrUnknown(data['cloud_id']!, _cloudIdMeta),
);
}
return context; return context;
} }
@@ -1426,6 +1444,10 @@ class $PlaylistsTable extends Playlists
DriftSqlType.bool, DriftSqlType.bool,
data['${effectivePrefix}deleted'], data['${effectivePrefix}deleted'],
)!, )!,
cloudId: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}cloud_id'],
),
); );
} }
@@ -1442,6 +1464,10 @@ class Playlist extends DataClass implements Insertable<Playlist> {
final int createdAtMs; final int createdAtMs;
final int updatedAtMs; final int updatedAtMs;
final bool deleted; final bool deleted;
/// ID derselben Playlist am Melo-Server, sobald sie einmal gesichert
/// wurde. `null` heißt: nur auf diesem Gerät.
final String? cloudId;
const Playlist({ const Playlist({
required this.id, required this.id,
required this.name, required this.name,
@@ -1449,6 +1475,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
required this.createdAtMs, required this.createdAtMs,
required this.updatedAtMs, required this.updatedAtMs,
required this.deleted, required this.deleted,
this.cloudId,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -1461,6 +1488,9 @@ class Playlist extends DataClass implements Insertable<Playlist> {
map['created_at_ms'] = Variable<int>(createdAtMs); map['created_at_ms'] = Variable<int>(createdAtMs);
map['updated_at_ms'] = Variable<int>(updatedAtMs); map['updated_at_ms'] = Variable<int>(updatedAtMs);
map['deleted'] = Variable<bool>(deleted); map['deleted'] = Variable<bool>(deleted);
if (!nullToAbsent || cloudId != null) {
map['cloud_id'] = Variable<String>(cloudId);
}
return map; return map;
} }
@@ -1474,6 +1504,9 @@ class Playlist extends DataClass implements Insertable<Playlist> {
createdAtMs: Value(createdAtMs), createdAtMs: Value(createdAtMs),
updatedAtMs: Value(updatedAtMs), updatedAtMs: Value(updatedAtMs),
deleted: Value(deleted), deleted: Value(deleted),
cloudId: cloudId == null && nullToAbsent
? const Value.absent()
: Value(cloudId),
); );
} }
@@ -1489,6 +1522,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
createdAtMs: serializer.fromJson<int>(json['createdAtMs']), createdAtMs: serializer.fromJson<int>(json['createdAtMs']),
updatedAtMs: serializer.fromJson<int>(json['updatedAtMs']), updatedAtMs: serializer.fromJson<int>(json['updatedAtMs']),
deleted: serializer.fromJson<bool>(json['deleted']), deleted: serializer.fromJson<bool>(json['deleted']),
cloudId: serializer.fromJson<String?>(json['cloudId']),
); );
} }
@override @override
@@ -1501,6 +1535,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
'createdAtMs': serializer.toJson<int>(createdAtMs), 'createdAtMs': serializer.toJson<int>(createdAtMs),
'updatedAtMs': serializer.toJson<int>(updatedAtMs), 'updatedAtMs': serializer.toJson<int>(updatedAtMs),
'deleted': serializer.toJson<bool>(deleted), 'deleted': serializer.toJson<bool>(deleted),
'cloudId': serializer.toJson<String?>(cloudId),
}; };
} }
@@ -1511,6 +1546,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
int? createdAtMs, int? createdAtMs,
int? updatedAtMs, int? updatedAtMs,
bool? deleted, bool? deleted,
Value<String?> cloudId = const Value.absent(),
}) => Playlist( }) => Playlist(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
@@ -1518,6 +1554,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
createdAtMs: createdAtMs ?? this.createdAtMs, createdAtMs: createdAtMs ?? this.createdAtMs,
updatedAtMs: updatedAtMs ?? this.updatedAtMs, updatedAtMs: updatedAtMs ?? this.updatedAtMs,
deleted: deleted ?? this.deleted, deleted: deleted ?? this.deleted,
cloudId: cloudId.present ? cloudId.value : this.cloudId,
); );
Playlist copyWithCompanion(PlaylistsCompanion data) { Playlist copyWithCompanion(PlaylistsCompanion data) {
return Playlist( return Playlist(
@@ -1533,6 +1570,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
? data.updatedAtMs.value ? data.updatedAtMs.value
: this.updatedAtMs, : this.updatedAtMs,
deleted: data.deleted.present ? data.deleted.value : this.deleted, deleted: data.deleted.present ? data.deleted.value : this.deleted,
cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId,
); );
} }
@@ -1544,14 +1582,22 @@ class Playlist extends DataClass implements Insertable<Playlist> {
..write('description: $description, ') ..write('description: $description, ')
..write('createdAtMs: $createdAtMs, ') ..write('createdAtMs: $createdAtMs, ')
..write('updatedAtMs: $updatedAtMs, ') ..write('updatedAtMs: $updatedAtMs, ')
..write('deleted: $deleted') ..write('deleted: $deleted, ')
..write('cloudId: $cloudId')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(id, name, description, createdAtMs, updatedAtMs, deleted); id,
name,
description,
createdAtMs,
updatedAtMs,
deleted,
cloudId,
);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@@ -1561,7 +1607,8 @@ class Playlist extends DataClass implements Insertable<Playlist> {
other.description == this.description && other.description == this.description &&
other.createdAtMs == this.createdAtMs && other.createdAtMs == this.createdAtMs &&
other.updatedAtMs == this.updatedAtMs && other.updatedAtMs == this.updatedAtMs &&
other.deleted == this.deleted); other.deleted == this.deleted &&
other.cloudId == this.cloudId);
} }
class PlaylistsCompanion extends UpdateCompanion<Playlist> { class PlaylistsCompanion extends UpdateCompanion<Playlist> {
@@ -1571,6 +1618,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
final Value<int> createdAtMs; final Value<int> createdAtMs;
final Value<int> updatedAtMs; final Value<int> updatedAtMs;
final Value<bool> deleted; final Value<bool> deleted;
final Value<String?> cloudId;
final Value<int> rowid; final Value<int> rowid;
const PlaylistsCompanion({ const PlaylistsCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
@@ -1579,6 +1627,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
this.createdAtMs = const Value.absent(), this.createdAtMs = const Value.absent(),
this.updatedAtMs = const Value.absent(), this.updatedAtMs = const Value.absent(),
this.deleted = const Value.absent(), this.deleted = const Value.absent(),
this.cloudId = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
PlaylistsCompanion.insert({ PlaylistsCompanion.insert({
@@ -1588,6 +1637,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
required int createdAtMs, required int createdAtMs,
required int updatedAtMs, required int updatedAtMs,
this.deleted = const Value.absent(), this.deleted = const Value.absent(),
this.cloudId = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : id = Value(id), }) : id = Value(id),
name = Value(name), name = Value(name),
@@ -1600,6 +1650,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
Expression<int>? createdAtMs, Expression<int>? createdAtMs,
Expression<int>? updatedAtMs, Expression<int>? updatedAtMs,
Expression<bool>? deleted, Expression<bool>? deleted,
Expression<String>? cloudId,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
@@ -1609,6 +1660,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
if (createdAtMs != null) 'created_at_ms': createdAtMs, if (createdAtMs != null) 'created_at_ms': createdAtMs,
if (updatedAtMs != null) 'updated_at_ms': updatedAtMs, if (updatedAtMs != null) 'updated_at_ms': updatedAtMs,
if (deleted != null) 'deleted': deleted, if (deleted != null) 'deleted': deleted,
if (cloudId != null) 'cloud_id': cloudId,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@@ -1620,6 +1672,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
Value<int>? createdAtMs, Value<int>? createdAtMs,
Value<int>? updatedAtMs, Value<int>? updatedAtMs,
Value<bool>? deleted, Value<bool>? deleted,
Value<String?>? cloudId,
Value<int>? rowid, Value<int>? rowid,
}) { }) {
return PlaylistsCompanion( return PlaylistsCompanion(
@@ -1629,6 +1682,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
createdAtMs: createdAtMs ?? this.createdAtMs, createdAtMs: createdAtMs ?? this.createdAtMs,
updatedAtMs: updatedAtMs ?? this.updatedAtMs, updatedAtMs: updatedAtMs ?? this.updatedAtMs,
deleted: deleted ?? this.deleted, deleted: deleted ?? this.deleted,
cloudId: cloudId ?? this.cloudId,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@@ -1654,6 +1708,9 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
if (deleted.present) { if (deleted.present) {
map['deleted'] = Variable<bool>(deleted.value); map['deleted'] = Variable<bool>(deleted.value);
} }
if (cloudId.present) {
map['cloud_id'] = Variable<String>(cloudId.value);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@@ -1669,6 +1726,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
..write('createdAtMs: $createdAtMs, ') ..write('createdAtMs: $createdAtMs, ')
..write('updatedAtMs: $updatedAtMs, ') ..write('updatedAtMs: $updatedAtMs, ')
..write('deleted: $deleted, ') ..write('deleted: $deleted, ')
..write('cloudId: $cloudId, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@@ -4216,6 +4274,7 @@ typedef $$PlaylistsTableCreateCompanionBuilder =
required int createdAtMs, required int createdAtMs,
required int updatedAtMs, required int updatedAtMs,
Value<bool> deleted, Value<bool> deleted,
Value<String?> cloudId,
Value<int> rowid, Value<int> rowid,
}); });
typedef $$PlaylistsTableUpdateCompanionBuilder = typedef $$PlaylistsTableUpdateCompanionBuilder =
@@ -4226,6 +4285,7 @@ typedef $$PlaylistsTableUpdateCompanionBuilder =
Value<int> createdAtMs, Value<int> createdAtMs,
Value<int> updatedAtMs, Value<int> updatedAtMs,
Value<bool> deleted, Value<bool> deleted,
Value<String?> cloudId,
Value<int> rowid, Value<int> rowid,
}); });
@@ -4291,6 +4351,11 @@ class $$PlaylistsTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get cloudId => $composableBuilder(
column: $table.cloudId,
builder: (column) => ColumnFilters(column),
);
Expression<bool> playlistSongsRefs( Expression<bool> playlistSongsRefs(
Expression<bool> Function($$PlaylistSongsTableFilterComposer f) f, Expression<bool> Function($$PlaylistSongsTableFilterComposer f) f,
) { ) {
@@ -4355,6 +4420,11 @@ class $$PlaylistsTableOrderingComposer
column: $table.deleted, column: $table.deleted,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get cloudId => $composableBuilder(
column: $table.cloudId,
builder: (column) => ColumnOrderings(column),
);
} }
class $$PlaylistsTableAnnotationComposer class $$PlaylistsTableAnnotationComposer
@@ -4390,6 +4460,9 @@ class $$PlaylistsTableAnnotationComposer
GeneratedColumn<bool> get deleted => GeneratedColumn<bool> get deleted =>
$composableBuilder(column: $table.deleted, builder: (column) => column); $composableBuilder(column: $table.deleted, builder: (column) => column);
GeneratedColumn<String> get cloudId =>
$composableBuilder(column: $table.cloudId, builder: (column) => column);
Expression<T> playlistSongsRefs<T extends Object>( Expression<T> playlistSongsRefs<T extends Object>(
Expression<T> Function($$PlaylistSongsTableAnnotationComposer a) f, Expression<T> Function($$PlaylistSongsTableAnnotationComposer a) f,
) { ) {
@@ -4450,6 +4523,7 @@ class $$PlaylistsTableTableManager
Value<int> createdAtMs = const Value.absent(), Value<int> createdAtMs = const Value.absent(),
Value<int> updatedAtMs = const Value.absent(), Value<int> updatedAtMs = const Value.absent(),
Value<bool> deleted = const Value.absent(), Value<bool> deleted = const Value.absent(),
Value<String?> cloudId = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => PlaylistsCompanion( }) => PlaylistsCompanion(
id: id, id: id,
@@ -4458,6 +4532,7 @@ class $$PlaylistsTableTableManager
createdAtMs: createdAtMs, createdAtMs: createdAtMs,
updatedAtMs: updatedAtMs, updatedAtMs: updatedAtMs,
deleted: deleted, deleted: deleted,
cloudId: cloudId,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: createCompanionCallback:
@@ -4468,6 +4543,7 @@ class $$PlaylistsTableTableManager
required int createdAtMs, required int createdAtMs,
required int updatedAtMs, required int updatedAtMs,
Value<bool> deleted = const Value.absent(), Value<bool> deleted = const Value.absent(),
Value<String?> cloudId = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => PlaylistsCompanion.insert( }) => PlaylistsCompanion.insert(
id: id, id: id,
@@ -4476,6 +4552,7 @@ class $$PlaylistsTableTableManager
createdAtMs: createdAtMs, createdAtMs: createdAtMs,
updatedAtMs: updatedAtMs, updatedAtMs: updatedAtMs,
deleted: deleted, deleted: deleted,
cloudId: cloudId,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
+57
View File
@@ -0,0 +1,57 @@
import 'package:drift/drift.dart' show Migrator;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:melo/library/database.dart';
void main() {
test('Bestandsdaten überleben die neue Spalte', () async {
final db = MeloDb(NativeDatabase.memory());
addTearDown(db.close);
// Den Stand von Schema 10 nachbauen: Tabelle ohne cloud_id, mit Daten.
await db.customStatement('DROP TABLE playlist_songs');
await db.customStatement('DROP TABLE playlists');
await db.customStatement(
'CREATE TABLE playlists ('
'id TEXT NOT NULL, '
'name TEXT NOT NULL, '
'description TEXT NULL, '
'created_at_ms INTEGER NOT NULL, '
'updated_at_ms INTEGER NOT NULL, '
'deleted INTEGER NOT NULL DEFAULT 0, '
'PRIMARY KEY (id))',
);
await db.customStatement(
"INSERT INTO playlists (id, name, created_at_ms, updated_at_ms) "
"VALUES ('alt-1', 'Road Trip', 0, 0)",
);
await Migrator(db).addColumn(db.playlists, db.playlists.cloudId);
final rows = await db.select(db.playlists).get();
expect(rows.single.name, 'Road Trip');
expect(rows.single.cloudId, isNull);
});
test('setPlaylistCloudId merkt sich die Server-ID', () async {
final db = MeloDb(NativeDatabase.memory());
addTearDown(db.close);
final id = await db.createPlaylist('Mix');
await db.setPlaylistCloudId(id, '42');
expect((await db.playlistById(id))!.cloudId, '42');
});
test('countPlaylists zählt auch Grabsteine', () async {
final db = MeloDb(NativeDatabase.memory());
addTearDown(db.close);
final id = await db.createPlaylist('Mix');
await db.deletePlaylist(id);
// Sonst hielte die Wiederherstellung ein Gerät, auf dem der Nutzer alle
// Playlisten gelöscht hat, für eine Neuinstallation.
expect(await db.countPlaylists(), 1);
});
}