Don't cache explain statements (#2464)

This commit is contained in:
Simon Binder 2024-02-13 22:53:00 +01:00
parent ae4d2d35f3
commit 11a9b14572
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
5 changed files with 35 additions and 13 deletions

View File

@ -11,6 +11,7 @@
inside of the transaction, instead of causing a deadlock.
- Improve stack traces for errors happening on drift isolates (which includes
usages of `NativeDatabase.createInBackground`).
- Don't cache `EXPLAIN` statements, avoiding schema locks.
## 2.15.0

View File

@ -43,7 +43,7 @@ on how to reproduce this.''');
This database or transaction runner has already been closed and may not be used
anymore.
If this is happening in a transaction, you might be using the transaction
If this is happening in a transaction, you might be using the transaction
without awaiting every statement in it.''');
}

View File

@ -144,48 +144,52 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
if (args.isEmpty) {
database.execute(statement);
} else {
final stmt = _getPreparedStatement(statement);
final (stmt, cached) = _getPreparedStatement(statement);
try {
stmt.execute(args);
} finally {
_returnStatement(stmt);
_returnStatement(stmt, cached);
}
}
}
@override
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
final stmt = _getPreparedStatement(statement);
final (stmt, cached) = _getPreparedStatement(statement);
try {
final result = stmt.select(args);
return QueryResult.fromRows(result.toList());
} finally {
_returnStatement(stmt);
_returnStatement(stmt, cached);
}
}
CommonPreparedStatement _getPreparedStatement(String sql) {
/// Returns a prepared statement for [sql] and reports whether this statement
/// was cached.
(CommonPreparedStatement, bool) _getPreparedStatement(String sql) {
if (cachePreparedStatements) {
final cachedStmt = _preparedStmtsCache.use(sql);
if (cachedStmt != null) {
return cachedStmt;
return (cachedStmt, true);
}
final stmt = database.prepare(sql, checkNoTail: true);
_preparedStmtsCache.addNew(sql, stmt);
if (!stmt.isExplain) {
_preparedStmtsCache.addNew(sql, stmt);
}
return stmt;
return (stmt, !stmt.isExplain);
} else {
final stmt = database.prepare(sql, checkNoTail: true);
return stmt;
return (stmt, false);
}
}
void _returnStatement(CommonPreparedStatement stmt) {
void _returnStatement(CommonPreparedStatement stmt, bool cached) {
// When using a statement cache, prepared statements are disposed as they
// get evicted from the cache, so we don't need to do anything.
if (!cachePreparedStatements) {
if (!cached) {
stmt.dispose();
}
}

View File

@ -15,7 +15,7 @@ dependencies:
js: ^0.6.3
meta: ^1.3.0
stream_channel: ^2.1.0
sqlite3: ^2.2.0
sqlite3: ^2.4.0
path: ^1.8.0
stack_trace: ^1.11.1

View File

@ -185,6 +185,23 @@ void main() {
expect(underlying.activeStatementCount, isZero);
});
test('does not cache explain statements', () async {
final db = NativeDatabase.memory(cachePreparedStatements: true);
addTearDown(db.close);
await db.ensureOpen(_FakeExecutorUser());
await db.runCustom(
'create table test(id integer primary key, description text)');
await db.runCustom('create index i1 on test(description)');
// The schema is locked while an explain is active, so caching this
// statement makes the test fail at the `drop index` statement.
await db.runSelect(
'explain query plan select * from test where description = ?', ['t']);
await db.runCustom('drop index i1');
await db.runSelect(
'explain query plan select * from test where description = ?', ['t']);
});
group('can disable migrations', () {
Future<void> runTest(QueryExecutor executor) async {
final db = TodoDb(executor);