Skip to content

Commit

Permalink
Add prepareMultiple to prepare multiple stmts
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed Dec 16, 2021
1 parent a83b4a1 commit d2c2dcf
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 51 deletions.
4 changes: 4 additions & 0 deletions sqlite3/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 12 additions & 0 deletions sqlite3/lib/src/api/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<PreparedStatement> prepareMultiple(String sql,
{bool persistent = false, bool vtab = true});

/// Creates a scalar function that can be called from sql queries sent against
/// this database.
///
Expand Down
3 changes: 3 additions & 0 deletions sqlite3/lib/src/api/statement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
136 changes: 90 additions & 46 deletions sqlite3/lib/src/impl/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<PreparedStatement> prepareMultiple(String sql,
{bool persistent = false, bool vtab = true}) {
return _prepareInternal(sql, persistent: persistent, vtab: vtab);
}

List<PreparedStatement> _prepareInternal(String sql,
{bool persistent = false,
bool vtab = true,
int? maxStatements,
bool checkNoTail = false}) {
_ensureOpen();

final stmtOut = allocate<Pointer<sqlite3_stmt>>();
final pzTail = checkNoTail
? allocate<Pointer<sqlite3_char>>()
: nullPtr<Pointer<sqlite3_char>>();
final pzTail = allocate<Pointer<sqlite3_char>>();

final bytes = utf8.encode(sql);
final sqlPtr = allocateBytes(bytes);
Expand All @@ -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<NativeFunction<sqlite3_prepare_v3_native>>()
.asFunction<sqlite3_prepare_v3_dart>();

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<NativeFunction<sqlite3_prepare_v2_native>>()
.asFunction<sqlite3_prepare_v2_dart>();

resultCode = function(
_handle,
sqlPtr.cast(),
bytes.length,
stmtOut,
pzTail,
);
final createdStatements = <PreparedStatementImpl>[];
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<NativeFunction<sqlite3_prepare_v3_native>>()
.asFunction<sqlite3_prepare_v3_dart>();

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<NativeFunction<sqlite3_prepare_v2_native>>()
.asFunction<sqlite3_prepare_v2_dart>();

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) {
Expand Down
11 changes: 6 additions & 5 deletions sqlite3/lib/src/impl/statement.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
part of 'implementation.dart';

class PreparedStatementImpl implements PreparedStatement {
final String originalSql;
@override
final String sql;
final Pointer<sqlite3_stmt> _stmt;
final DatabaseImpl _db;

Expand All @@ -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 {
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
42 changes: 42 additions & 0 deletions sqlite3/test/database_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<SqliteException>()));
});

test('fails for syntax error in the middle', () {
expect(() => db.prepareMultiple('SELECT 1; error here; SELECT 2;'),
throwsA(isA<SqliteException>()));
});
});
}

Matcher _update(SqliteUpdate update) {
Expand All @@ -415,6 +453,10 @@ Matcher _update(SqliteUpdate update) {
.having((e) => e.rowId, 'rowId', update.rowId);
}

Matcher _statement(String sql) {
return isA<PreparedStatement>().having((e) => e.sql, 'sql', sql);
}

/// Aggregate function that counts the length of all string parameters it
/// receives.
class _SummedStringLength implements AggregateFunction<int> {
Expand Down

1 comment on commit d2c2dcf

@tofutim
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try it out. Have you considered using gitflow and a develop branch?

Please sign in to comment.