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()();
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
Set<Column> get primaryKey => {id};
}
@@ -148,7 +152,7 @@ class MeloDb extends _$MeloDb {
MeloDb([QueryExecutor? executor]) : super(executor ?? _open());
@override
int get schemaVersion => 10;
int get schemaVersion => 11;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -190,6 +194,9 @@ class MeloDb extends _$MeloDb {
if (from < 10) {
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 ===
Stream<List<Song>> watchPlaylistSongs(String playlistId) {
final query = select(songs).join([
+81 -4
View File
@@ -1322,6 +1322,17 @@ class $PlaylistsTable extends Playlists
),
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
List<GeneratedColumn> get $columns => [
id,
@@ -1330,6 +1341,7 @@ class $PlaylistsTable extends Playlists
createdAtMs,
updatedAtMs,
deleted,
cloudId,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -1393,6 +1405,12 @@ class $PlaylistsTable extends Playlists
deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta),
);
}
if (data.containsKey('cloud_id')) {
context.handle(
_cloudIdMeta,
cloudId.isAcceptableOrUnknown(data['cloud_id']!, _cloudIdMeta),
);
}
return context;
}
@@ -1426,6 +1444,10 @@ class $PlaylistsTable extends Playlists
DriftSqlType.bool,
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 updatedAtMs;
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({
required this.id,
required this.name,
@@ -1449,6 +1475,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
required this.createdAtMs,
required this.updatedAtMs,
required this.deleted,
this.cloudId,
});
@override
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['updated_at_ms'] = Variable<int>(updatedAtMs);
map['deleted'] = Variable<bool>(deleted);
if (!nullToAbsent || cloudId != null) {
map['cloud_id'] = Variable<String>(cloudId);
}
return map;
}
@@ -1474,6 +1504,9 @@ class Playlist extends DataClass implements Insertable<Playlist> {
createdAtMs: Value(createdAtMs),
updatedAtMs: Value(updatedAtMs),
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']),
updatedAtMs: serializer.fromJson<int>(json['updatedAtMs']),
deleted: serializer.fromJson<bool>(json['deleted']),
cloudId: serializer.fromJson<String?>(json['cloudId']),
);
}
@override
@@ -1501,6 +1535,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
'createdAtMs': serializer.toJson<int>(createdAtMs),
'updatedAtMs': serializer.toJson<int>(updatedAtMs),
'deleted': serializer.toJson<bool>(deleted),
'cloudId': serializer.toJson<String?>(cloudId),
};
}
@@ -1511,6 +1546,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
int? createdAtMs,
int? updatedAtMs,
bool? deleted,
Value<String?> cloudId = const Value.absent(),
}) => Playlist(
id: id ?? this.id,
name: name ?? this.name,
@@ -1518,6 +1554,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
createdAtMs: createdAtMs ?? this.createdAtMs,
updatedAtMs: updatedAtMs ?? this.updatedAtMs,
deleted: deleted ?? this.deleted,
cloudId: cloudId.present ? cloudId.value : this.cloudId,
);
Playlist copyWithCompanion(PlaylistsCompanion data) {
return Playlist(
@@ -1533,6 +1570,7 @@ class Playlist extends DataClass implements Insertable<Playlist> {
? data.updatedAtMs.value
: this.updatedAtMs,
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('createdAtMs: $createdAtMs, ')
..write('updatedAtMs: $updatedAtMs, ')
..write('deleted: $deleted')
..write('deleted: $deleted, ')
..write('cloudId: $cloudId')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, name, description, createdAtMs, updatedAtMs, deleted);
int get hashCode => Object.hash(
id,
name,
description,
createdAtMs,
updatedAtMs,
deleted,
cloudId,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -1561,7 +1607,8 @@ class Playlist extends DataClass implements Insertable<Playlist> {
other.description == this.description &&
other.createdAtMs == this.createdAtMs &&
other.updatedAtMs == this.updatedAtMs &&
other.deleted == this.deleted);
other.deleted == this.deleted &&
other.cloudId == this.cloudId);
}
class PlaylistsCompanion extends UpdateCompanion<Playlist> {
@@ -1571,6 +1618,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
final Value<int> createdAtMs;
final Value<int> updatedAtMs;
final Value<bool> deleted;
final Value<String?> cloudId;
final Value<int> rowid;
const PlaylistsCompanion({
this.id = const Value.absent(),
@@ -1579,6 +1627,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
this.createdAtMs = const Value.absent(),
this.updatedAtMs = const Value.absent(),
this.deleted = const Value.absent(),
this.cloudId = const Value.absent(),
this.rowid = const Value.absent(),
});
PlaylistsCompanion.insert({
@@ -1588,6 +1637,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
required int createdAtMs,
required int updatedAtMs,
this.deleted = const Value.absent(),
this.cloudId = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
name = Value(name),
@@ -1600,6 +1650,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
Expression<int>? createdAtMs,
Expression<int>? updatedAtMs,
Expression<bool>? deleted,
Expression<String>? cloudId,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
@@ -1609,6 +1660,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
if (createdAtMs != null) 'created_at_ms': createdAtMs,
if (updatedAtMs != null) 'updated_at_ms': updatedAtMs,
if (deleted != null) 'deleted': deleted,
if (cloudId != null) 'cloud_id': cloudId,
if (rowid != null) 'rowid': rowid,
});
}
@@ -1620,6 +1672,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
Value<int>? createdAtMs,
Value<int>? updatedAtMs,
Value<bool>? deleted,
Value<String?>? cloudId,
Value<int>? rowid,
}) {
return PlaylistsCompanion(
@@ -1629,6 +1682,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
createdAtMs: createdAtMs ?? this.createdAtMs,
updatedAtMs: updatedAtMs ?? this.updatedAtMs,
deleted: deleted ?? this.deleted,
cloudId: cloudId ?? this.cloudId,
rowid: rowid ?? this.rowid,
);
}
@@ -1654,6 +1708,9 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
if (deleted.present) {
map['deleted'] = Variable<bool>(deleted.value);
}
if (cloudId.present) {
map['cloud_id'] = Variable<String>(cloudId.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@@ -1669,6 +1726,7 @@ class PlaylistsCompanion extends UpdateCompanion<Playlist> {
..write('createdAtMs: $createdAtMs, ')
..write('updatedAtMs: $updatedAtMs, ')
..write('deleted: $deleted, ')
..write('cloudId: $cloudId, ')
..write('rowid: $rowid')
..write(')'))
.toString();
@@ -4216,6 +4274,7 @@ typedef $$PlaylistsTableCreateCompanionBuilder =
required int createdAtMs,
required int updatedAtMs,
Value<bool> deleted,
Value<String?> cloudId,
Value<int> rowid,
});
typedef $$PlaylistsTableUpdateCompanionBuilder =
@@ -4226,6 +4285,7 @@ typedef $$PlaylistsTableUpdateCompanionBuilder =
Value<int> createdAtMs,
Value<int> updatedAtMs,
Value<bool> deleted,
Value<String?> cloudId,
Value<int> rowid,
});
@@ -4291,6 +4351,11 @@ class $$PlaylistsTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get cloudId => $composableBuilder(
column: $table.cloudId,
builder: (column) => ColumnFilters(column),
);
Expression<bool> playlistSongsRefs(
Expression<bool> Function($$PlaylistSongsTableFilterComposer f) f,
) {
@@ -4355,6 +4420,11 @@ class $$PlaylistsTableOrderingComposer
column: $table.deleted,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get cloudId => $composableBuilder(
column: $table.cloudId,
builder: (column) => ColumnOrderings(column),
);
}
class $$PlaylistsTableAnnotationComposer
@@ -4390,6 +4460,9 @@ class $$PlaylistsTableAnnotationComposer
GeneratedColumn<bool> get deleted =>
$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> Function($$PlaylistSongsTableAnnotationComposer a) f,
) {
@@ -4450,6 +4523,7 @@ class $$PlaylistsTableTableManager
Value<int> createdAtMs = const Value.absent(),
Value<int> updatedAtMs = const Value.absent(),
Value<bool> deleted = const Value.absent(),
Value<String?> cloudId = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => PlaylistsCompanion(
id: id,
@@ -4458,6 +4532,7 @@ class $$PlaylistsTableTableManager
createdAtMs: createdAtMs,
updatedAtMs: updatedAtMs,
deleted: deleted,
cloudId: cloudId,
rowid: rowid,
),
createCompanionCallback:
@@ -4468,6 +4543,7 @@ class $$PlaylistsTableTableManager
required int createdAtMs,
required int updatedAtMs,
Value<bool> deleted = const Value.absent(),
Value<String?> cloudId = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => PlaylistsCompanion.insert(
id: id,
@@ -4476,6 +4552,7 @@ class $$PlaylistsTableTableManager
createdAtMs: createdAtMs,
updatedAtMs: updatedAtMs,
deleted: deleted,
cloudId: cloudId,
rowid: rowid,
),
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);
});
}