Files
SpotiFLAC-Mobile/test/library_schema_test.dart
T
zarzet 2e17963804 refactor(library): isolate schema, row mapping and queue queries
Repair lookup counts during schema migration and resolve local FTS matches through the base table.
2026-10-05 03:28:48 +07:00

191 lines
6.8 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite/sqflite.dart';
import 'package:spotiflac_android/services/library_database_models.dart';
import 'package:spotiflac_android/services/library_row_mapper.dart';
import 'package:spotiflac_android/services/library_schema.dart';
import 'support/sqlite_process_database.dart';
Map<String, dynamic> _track(String id, {String isrc = 'same'}) =>
LocalLibraryItem(
id: id,
trackName: ' Song ',
artistName: 'ARTIST',
albumName: 'Album',
filePath: '/music/$id.flac',
scannedAt: DateTime.utc(2026),
isrc: isrc,
trackNumber: 1,
totalTracks: 2,
hasLyrics: true,
hasReplayGain: true,
).toJson();
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late SqliteProcessDatabase db;
setUp(() async {
db = await SqliteProcessDatabase.open();
await db.execute('PRAGMA recursive_triggers = ON');
});
tearDown(() => db.close());
test(
'fresh schema maintains lookup counts across replace, source move and deletion',
() async {
await LibrarySchema.create(db, LibrarySchema.version);
for (final id in ['a', 'b']) {
await db.insert(
'library',
LibraryRowMapper.encode(_track(id)),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<Map<String, Object?>>> counts() => db.query(
LibrarySchema.lookupSummaryTable,
orderBy: 'source_id, kind, value',
);
expect((await counts()).map((row) => row['ref_count']), [2, 2]);
await db.insert(
'library',
LibraryRowMapper.encode(_track('a', isrc: 'new')),
conflictAlgorithm: ConflictAlgorithm.replace,
);
expect((await counts()).map((row) => row['ref_count']), [1, 1, 2]);
await db.insert('library_sources', {
'id': 'nas',
'path': 'network://nas/music',
'display_name': 'NAS',
});
await db.update(
'library',
{'source_id': 'nas'},
where: 'id = ?',
whereArgs: ['a'],
);
expect((await db.query(LibrarySchema.visibleView)).length, 2);
await db.update(
'library_sources',
{'available': 0},
where: 'id = ?',
whereArgs: ['nas'],
);
expect((await db.query(LibrarySchema.visibleView)).single['id'], 'b');
await db.delete('library', where: 'id = ?', whereArgs: ['a']);
expect((await counts()).map((row) => row['ref_count']), [1, 1]);
await db.delete('library');
expect(await counts(), isEmpty);
},
);
test(
'v1 upgrade preserves tracks and backfills current search and path identities',
() async {
await db.execute('''CREATE TABLE library (
id TEXT PRIMARY KEY, track_name TEXT NOT NULL, artist_name TEXT NOT NULL,
album_name TEXT NOT NULL, album_artist TEXT, file_path TEXT NOT NULL UNIQUE,
scanned_at TEXT NOT NULL, isrc TEXT, track_number INTEGER, disc_number INTEGER,
duration INTEGER, release_date TEXT, bit_depth INTEGER, sample_rate INTEGER,
genre TEXT, format TEXT
)''');
await db.insert('library', {
'id': 'old',
'track_name': ' Song ',
'artist_name': 'ARTIST',
'album_name': 'Album',
'file_path':
'content://example.documents/document/nas%3AMusic%2FSong.flac',
'scanned_at': '2026-01-01T00:00:00.000Z',
'isrc': 'legacy-isrc',
'genre': ' Pop ',
});
await LibrarySchema.upgrade(db, 1, LibrarySchema.version);
final row = (await db.query('library')).single;
expect(row['source_id'], LocalLibraryItem.legacySourceId);
expect(row['search_text'], 'song artist album artist');
expect(row['match_key'], 'song|artist');
expect(row['sort_genre'], 'pop');
expect(row['audio_metadata_scan_version'], 0);
expect(row['has_lyrics'], 0);
expect(row['has_replaygain'], 0);
expect(
(await db.query(
'library_path_keys',
)).every((key) => key['item_id'] == 'old'),
isTrue,
);
expect(await db.query('library_path_keys'), isNotEmpty);
expect((await db.query(LibrarySchema.visibleView)).single['id'], 'old');
expect(
(await db.query(
LibrarySchema.lookupSummaryTable,
)).map((row) => row['ref_count']),
[1, 1],
);
},
);
test(
'v16 upgrade repairs existing lookup counts and replacement triggers',
() async {
await LibrarySchema.create(db, 16);
await db.execute('DROP TRIGGER library_lookup_insert_isrc');
await db.execute('''
CREATE TRIGGER library_lookup_insert_isrc AFTER INSERT ON library
WHEN NEW.isrc IS NOT NULL AND NEW.isrc != ''
BEGIN
INSERT OR IGNORE INTO library_lookup_summary(source_id, kind, value, ref_count)
VALUES (NEW.source_id, 'isrc', NEW.isrc, 0);
UPDATE library_lookup_summary SET ref_count = ref_count + 1
WHERE source_id = NEW.source_id AND kind = 'isrc' AND value = NEW.isrc;
END
''');
for (final id in ['a', 'b']) {
await db.insert(
'library',
LibraryRowMapper.encode(_track(id)),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<Map<String, Object?>>> counts() =>
db.query(LibrarySchema.lookupSummaryTable, orderBy: 'kind');
expect((await counts()).map((row) => row['ref_count']), [1, 2]);
await LibrarySchema.upgrade(db, 16, LibrarySchema.version);
expect((await counts()).map((row) => row['ref_count']), [2, 2]);
await db.insert(
'library',
LibraryRowMapper.encode(_track('a')),
conflictAlgorithm: ConflictAlgorithm.replace,
);
expect((await counts()).map((row) => row['ref_count']), [2, 2]);
await db.delete('library', where: 'id = ?', whereArgs: ['a']);
expect((await counts()).map((row) => row['ref_count']), [1, 1]);
expect((await db.query('library')).single['id'], 'b');
},
);
test('row mapping round-trips metadata and preserves unknown scan flags', () {
final track = _track('full');
expect(LibraryRowMapper.decode(LibraryRowMapper.encode(track)), track);
final filename = LibraryRowMapper.encode({
...track,
'sourceId': 'nas',
'metadataFromFilename': true,
'hasReplayGain': false,
});
expect(filename['source_id'], 'nas');
expect(filename['audio_metadata_scan_version'], 0);
expect(filename['has_replaygain'], 0);
final tagged = LibraryRowMapper.encode({
...track,
'hasReplayGain': false,
'replaygain_track_gain': '-4.5 dB',
'explicit': 1,
'audioMetadataScanVersion': 3,
});
expect(tagged['has_replaygain'], 1);
expect(tagged['audio_metadata_scan_version'], 3);
expect(LibraryRowMapper.decode(tagged)['explicit'], true);
});
}