From d2c2dcf4659dffe197cea644ee80a4e7d82e31ed Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 16 Dec 2021 10:56:39 +0100 Subject: [PATCH] Add prepareMultiple to prepare multiple stmts --- sqlite3/CHANGELOG.md | 4 + sqlite3/lib/src/api/database.dart | 12 +++ sqlite3/lib/src/api/statement.dart | 3 + sqlite3/lib/src/impl/database.dart | 136 ++++++++++++++++++---------- sqlite3/lib/src/impl/statement.dart | 11 ++- sqlite3/test/database_test.dart | 42 +++++++++ 6 files changed, 157 insertions(+), 51 deletions(-) diff --git a/sqlite3/CHANGELOG.md b/sqlite3/CHANGELOG.md index 21d3cb35..a0018ef0 100644 --- a/sqlite3/CHANGELOG.md +++ b/sqlite3/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.5.0-dev + +- Add `prepareMultiple` method to prepare multiple statements from one SQL string. + ## 1.4.0 - Report writes on the database through the `Database.updates` stream diff --git a/sqlite3/lib/src/api/database.dart b/sqlite3/lib/src/api/database.dart index 95476bdf..017170a9 100644 --- a/sqlite3/lib/src/api/database.dart +++ b/sqlite3/lib/src/api/database.dart @@ -68,6 +68,18 @@ abstract class Database { PreparedStatement prepare(String sql, {bool persistent = false, bool vtab = true, bool checkNoTail = false}); + /// Compiles multiple statements from [sql] to be executed later. + /// + /// Unlike [prepare], which can only compile a single statement, + /// [prepareMultiple] will return multiple statements if the source [sql] + /// string contains more than one statement. + /// For example, calling [prepareMultiple] with `SELECT 1; SELECT 2;` will + /// return `2` prepared statements. + /// + /// For the [persistent] and [vtab] parameters, see [prepare]. + List prepareMultiple(String sql, + {bool persistent = false, bool vtab = true}); + /// Creates a scalar function that can be called from sql queries sent against /// this database. /// diff --git a/sqlite3/lib/src/api/statement.dart b/sqlite3/lib/src/api/statement.dart index a666bfcb..8923ea06 100644 --- a/sqlite3/lib/src/api/statement.dart +++ b/sqlite3/lib/src/api/statement.dart @@ -5,6 +5,9 @@ import 'result_set.dart'; /// A prepared statement. abstract class PreparedStatement { + /// The SQL statement backing this prepared statement. + String get sql; + /// Returns the amount of parameters in this prepared statement. int get parameterCount; diff --git a/sqlite3/lib/src/impl/database.dart b/sqlite3/lib/src/impl/database.dart index 982e28f4..11d245ac 100644 --- a/sqlite3/lib/src/impl/database.dart +++ b/sqlite3/lib/src/impl/database.dart @@ -149,12 +149,29 @@ class DatabaseImpl implements Database { @override PreparedStatement prepare(String sql, {bool persistent = false, bool vtab = true, bool checkNoTail = false}) { + return _prepareInternal(sql, + persistent: persistent, + vtab: vtab, + maxStatements: 1, + checkNoTail: checkNoTail) + .single; + } + + @override + List prepareMultiple(String sql, + {bool persistent = false, bool vtab = true}) { + return _prepareInternal(sql, persistent: persistent, vtab: vtab); + } + + List _prepareInternal(String sql, + {bool persistent = false, + bool vtab = true, + int? maxStatements, + bool checkNoTail = false}) { _ensureOpen(); final stmtOut = allocate>(); - final pzTail = checkNoTail - ? allocate>() - : nullPtr>(); + final pzTail = allocate>(); final bytes = utf8.encode(sql); final sqlPtr = allocateBytes(bytes); @@ -167,64 +184,91 @@ class DatabaseImpl implements Database { prepFlags |= SqlPrepareFlag.SQLITE_PREPARE_NO_VTAB; } - int resultCode; - // Use prepare_v3 if supported, fall-back to prepare_v2 otherwise - if (_library.supportsOpenV3) { - final function = _library.appropriateOpenFunction - .cast>() - .asFunction(); - - resultCode = function( - _handle, - sqlPtr.cast(), - bytes.length, - prepFlags, - stmtOut, - pzTail, - ); - } else { - assert( - prepFlags == 0, - 'Used custom preparation flags, but the loaded sqlite library does not ' - 'support prepare_v3', - ); - - final function = _library.appropriateOpenFunction - .cast>() - .asFunction(); - - resultCode = function( - _handle, - sqlPtr.cast(), - bytes.length, - stmtOut, - pzTail, - ); + final createdStatements = []; + var offset = 0; + + void freeIntermediateResults() { + stmtOut.free(); + sqlPtr.free(); + pzTail.free(); + + for (final stmt in createdStatements) { + _bindings.sqlite3_finalize(stmt.handle.cast()); + } } - final stmtPtr = stmtOut.value; - stmtOut.free(); - sqlPtr.free(); + while (offset < bytes.length) { + int resultCode; + // Use prepare_v3 if supported, fall-back to prepare_v2 otherwise + if (_library.supportsOpenV3) { + final function = _library.appropriateOpenFunction + .cast>() + .asFunction(); + + resultCode = function( + _handle, + sqlPtr.elementAt(offset).cast(), + bytes.length, + prepFlags, + stmtOut, + pzTail, + ); + } else { + assert( + prepFlags == 0, + 'Used custom preparation flags, but the loaded sqlite library does ' + 'not support prepare_v3', + ); + + final function = _library.appropriateOpenFunction + .cast>() + .asFunction(); + + resultCode = function( + _handle, + sqlPtr.elementAt(offset).cast(), + bytes.length, + stmtOut, + pzTail, + ); + } + + if (resultCode != SqlError.SQLITE_OK) { + freeIntermediateResults(); + throwException(this, resultCode, sql); + } + + final stmtPtr = stmtOut.value; + final endOffset = pzTail.value.address - sqlPtr.address; + + final sqlForStatement = utf8.decoder.convert(bytes, offset, endOffset); + final stmt = PreparedStatementImpl(sqlForStatement, stmtPtr, this); + + createdStatements.add(stmt); - if (resultCode != SqlError.SQLITE_OK) { - throwException(this, resultCode, sql); + if (createdStatements.length == maxStatements) { + break; + } + + offset = endOffset; } if (checkNoTail) { final usedBytes = pzTail.value.address - sqlPtr.address; - pzTail.free(); if (usedBytes < bytes.length) { - _bindings.sqlite3_finalize(stmtPtr); + freeIntermediateResults(); throw ArgumentError.value( sql, 'sql', 'Has trailing data after the first sql statement;'); } } - final stmt = PreparedStatementImpl(sql, stmtPtr, this); + stmtOut.free(); + sqlPtr.free(); + pzTail.free(); - _statements.add(stmt); - return stmt; + _statements.addAll(createdStatements); + return createdStatements; } int _eTextRep(bool deterministic, bool directOnly) { diff --git a/sqlite3/lib/src/impl/statement.dart b/sqlite3/lib/src/impl/statement.dart index 0cf86666..d815ccbe 100644 --- a/sqlite3/lib/src/impl/statement.dart +++ b/sqlite3/lib/src/impl/statement.dart @@ -1,7 +1,8 @@ part of 'implementation.dart'; class PreparedStatementImpl implements PreparedStatement { - final String originalSql; + @override + final String sql; final Pointer _stmt; final DatabaseImpl _db; @@ -13,7 +14,7 @@ class PreparedStatementImpl implements PreparedStatement { Bindings get _bindings => _db._bindings; - PreparedStatementImpl(this.originalSql, this._stmt, this._db); + PreparedStatementImpl(this.sql, this._stmt, this._db); @override int get parameterCount { @@ -40,7 +41,7 @@ class PreparedStatementImpl implements PreparedStatement { } while (result == SqlError.SQLITE_ROW); if (result != SqlError.SQLITE_OK && result != SqlError.SQLITE_DONE) { - throwException(_db, result, originalSql); + throwException(_db, result, sql); } } @@ -88,7 +89,7 @@ class PreparedStatementImpl implements PreparedStatement { if (resultCode != SqlError.SQLITE_OK && resultCode != SqlError.SQLITE_DONE) { - throwException(_db, resultCode, originalSql); + throwException(_db, resultCode, sql); } return ResultSet(names, tableNames, rows); @@ -255,7 +256,7 @@ class _ActiveCursorIterator extends IteratingCursor { statement._currentCursor = null; if (result != SqlError.SQLITE_OK && result != SqlError.SQLITE_DONE) { - throwException(statement._db, result, statement.originalSql); + throwException(statement._db, result, statement.sql); } return false; diff --git a/sqlite3/test/database_test.dart b/sqlite3/test/database_test.dart index 36116a61..3fe972e2 100644 --- a/sqlite3/test/database_test.dart +++ b/sqlite3/test/database_test.dart @@ -406,6 +406,44 @@ void main() { database.dispose(); }); }); + + test('prepare does not throw for multiple statements by default', () { + final db = sqlite3.openInMemory(); + addTearDown(db.dispose); + + final stmt = db.prepare('SELECT 1; SELECT 2'); + expect(stmt.sql, 'SELECT 1;'); + }); + + test('prepare throws with checkNoTail', () { + final db = sqlite3.openInMemory(); + addTearDown(db.dispose); + + expect(() => db.prepare('SELECT 1; SELECT 2', checkNoTail: true), + throwsArgumentError); + }); + + group('prepareMultiple', () { + late Database db; + + setUp(() => db = sqlite3.openInMemory()); + tearDown(() => db.dispose()); + + test('can prepare multiple statements', () { + final statements = db.prepareMultiple('SELECT 1; SELECT 2;'); + expect(statements, [_statement('SELECT 1;'), _statement(' SELECT 2;')]); + }); + + test('fails for trailing syntax error', () { + expect(() => db.prepareMultiple('SELECT 1; error here '), + throwsA(isA())); + }); + + test('fails for syntax error in the middle', () { + expect(() => db.prepareMultiple('SELECT 1; error here; SELECT 2;'), + throwsA(isA())); + }); + }); } Matcher _update(SqliteUpdate update) { @@ -415,6 +453,10 @@ Matcher _update(SqliteUpdate update) { .having((e) => e.rowId, 'rowId', update.rowId); } +Matcher _statement(String sql) { + return isA().having((e) => e.sql, 'sql', sql); +} + /// Aggregate function that counts the length of all string parameters it /// receives. class _SummedStringLength implements AggregateFunction {