mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-02 16:20:57 +02:00
fix(database): cache unavailable FTS5 capability
This commit is contained in:
@@ -74,7 +74,9 @@ class HistoryDatabase {
|
||||
static final HistoryDatabase instance = HistoryDatabase._init();
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
bool _searchFtsAvailable = false;
|
||||
// null means this database connection has not attempted FTS setup yet.
|
||||
// false is a completed capability/setup result and must not be retried.
|
||||
bool? _searchFtsAvailable;
|
||||
|
||||
HistoryDatabase._init();
|
||||
|
||||
@@ -89,14 +91,12 @@ class HistoryDatabase {
|
||||
// onCreate normally initializes this derived index. Retry once after
|
||||
// opening an existing database in case an earlier setup was
|
||||
// interrupted; unsupported SQLite builds remain on the LIKE fallback.
|
||||
if (!_searchFtsAvailable) {
|
||||
_searchFtsAvailable = await _createSearchFts(db);
|
||||
}
|
||||
_searchFtsAvailable ??= await _createSearchFts(db);
|
||||
return db;
|
||||
});
|
||||
}
|
||||
|
||||
bool get searchFtsAvailable => _searchFtsAvailable;
|
||||
bool get searchFtsAvailable => _searchFtsAvailable ?? false;
|
||||
|
||||
Future<void> _createDB(Database db, int version) async {
|
||||
_log.i('Creating database schema v$version');
|
||||
@@ -1079,6 +1079,7 @@ class HistoryDatabase {
|
||||
final db = await database;
|
||||
await db.close();
|
||||
_database.reset();
|
||||
_searchFtsAvailable = null;
|
||||
}
|
||||
|
||||
Future<void> updateFilePath(
|
||||
|
||||
@@ -26,7 +26,9 @@ class LibraryDatabase {
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
bool _historyAttached = false;
|
||||
bool _searchFtsAvailable = false;
|
||||
// null means this database connection has not attempted FTS setup yet.
|
||||
// false is a completed capability/setup result and must not be retried.
|
||||
bool? _searchFtsAvailable;
|
||||
|
||||
LibraryDatabase._init();
|
||||
|
||||
@@ -41,14 +43,12 @@ class LibraryDatabase {
|
||||
// onCreate normally initializes this derived index. Retry once after
|
||||
// opening an existing database in case an earlier setup was
|
||||
// interrupted; unsupported SQLite builds remain on the LIKE fallback.
|
||||
if (!_searchFtsAvailable) {
|
||||
_searchFtsAvailable = await _createSearchFts(db);
|
||||
}
|
||||
_searchFtsAvailable ??= await _createSearchFts(db);
|
||||
return db;
|
||||
});
|
||||
}
|
||||
|
||||
bool get searchFtsAvailable => _searchFtsAvailable;
|
||||
bool get searchFtsAvailable => _searchFtsAvailable ?? false;
|
||||
|
||||
Future<void> _ensureHistoryAttached(Database db) async {
|
||||
if (_historyAttached) return;
|
||||
@@ -1659,6 +1659,7 @@ class LibraryDatabase {
|
||||
await db.close();
|
||||
_database.reset();
|
||||
_historyAttached = false;
|
||||
_searchFtsAvailable = null;
|
||||
}
|
||||
|
||||
Future<Map<String, int>> getFileModTimes({String? sourceId}) async {
|
||||
|
||||
@@ -6,6 +6,9 @@ import 'package:sqflite/sqflite.dart';
|
||||
|
||||
final _log = AppLogger('AppSqlite');
|
||||
|
||||
Future<bool>? _trigramFts5Capability;
|
||||
const _trigramFts5ProbeTable = 'spotiflac_trigram_fts5_probe';
|
||||
|
||||
/// Caches an asynchronously-created value while also coalescing concurrent
|
||||
/// callers onto the same in-flight initialization.
|
||||
class SingleFlightInitializer<T extends Object> {
|
||||
@@ -108,6 +111,57 @@ String? ftsPhraseSearchQuery(String value) {
|
||||
return '"${value.replaceAll('"', '""')}"';
|
||||
}
|
||||
|
||||
/// Whether [error] means the current SQLite runtime cannot provide the
|
||||
/// FTS5/trigram combination used by the search indexes.
|
||||
bool isTrigramFts5UnavailableError(Object error) {
|
||||
final message = error.toString().toLowerCase();
|
||||
return message.contains('no such module: fts5') ||
|
||||
message.contains('no such tokenizer: trigram') ||
|
||||
message.contains('unknown tokenizer: trigram');
|
||||
}
|
||||
|
||||
/// Probes FTS5 + trigram once for the current process.
|
||||
///
|
||||
/// Android's sqflite implementation uses the platform SQLite runtime, so
|
||||
/// compile-time modules can vary by device. A real temporary virtual table is
|
||||
/// more reliable than PRAGMA compile_options because it also verifies that the
|
||||
/// trigram tokenizer is registered. All app databases use the same sqflite
|
||||
/// runtime, so subsequent history/library initialization can reuse the result.
|
||||
Future<bool> _supportsTrigramFts5(DatabaseExecutor db) {
|
||||
return _trigramFts5Capability ??= _probeTrigramFts5(db);
|
||||
}
|
||||
|
||||
Future<bool> _probeTrigramFts5(DatabaseExecutor db) async {
|
||||
try {
|
||||
await db.execute('''
|
||||
CREATE VIRTUAL TABLE temp.$_trigramFts5ProbeTable USING fts5(
|
||||
search_text,
|
||||
tokenize='trigram'
|
||||
)
|
||||
''');
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isTrigramFts5UnavailableError(error)) {
|
||||
_log.i(
|
||||
'Trigram FTS5 is unavailable in this SQLite runtime; '
|
||||
'search will use the LIKE fallback',
|
||||
);
|
||||
} else {
|
||||
_log.w(
|
||||
'Could not probe trigram FTS5; search will use the LIKE fallback: '
|
||||
'$error',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
await db.execute('DROP TABLE IF EXISTS temp.$_trigramFts5ProbeTable');
|
||||
} catch (error) {
|
||||
_log.d('Could not remove the temporary FTS5 probe table: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an external-content FTS5 index that preserves substring search
|
||||
/// semantics through SQLite's trigram tokenizer.
|
||||
///
|
||||
@@ -122,6 +176,8 @@ Future<bool> createTrigramFtsIndex(
|
||||
required String contentTable,
|
||||
required String triggerPrefix,
|
||||
}) async {
|
||||
if (!await _supportsTrigramFts5(db)) return false;
|
||||
|
||||
try {
|
||||
final existingIndex = await db.rawQuery(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1",
|
||||
|
||||
@@ -103,5 +103,35 @@ void main() {
|
||||
expect(ftsPhraseSearchQuery('a\u0000b'), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('recognizes missing FTS5 and trigram capability errors', () {
|
||||
expect(
|
||||
isTrigramFts5UnavailableError(
|
||||
Exception('DatabaseException(no such module: fts5)'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
isTrigramFts5UnavailableError(
|
||||
Exception('DatabaseException(no such tokenizer: trigram)'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
isTrigramFts5UnavailableError(Exception('database is locked')),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('probes the runtime with a temporary trigram FTS5 table', () {
|
||||
final source = File(
|
||||
'lib/services/sqlite_helpers.dart',
|
||||
).readAsStringSync();
|
||||
|
||||
expect(source, contains('CREATE VIRTUAL TABLE temp.'));
|
||||
expect(source, contains("tokenize='trigram'"));
|
||||
expect(source, contains('DROP TABLE IF EXISTS temp.'));
|
||||
expect(source, contains('_trigramFts5Capability ??='));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user