From 18ee0b4e0e8f85260d040f4d2b11be26052df98b Mon Sep 17 00:00:00 2001 From: Steven Landow Date: Fri, 5 Dec 2025 18:41:26 -0800 Subject: [PATCH 1/3] add executeMultiple for multi-statement strings --- .../connection/sync_sqlite_connection.dart | 16 ++++++++++++++++ packages/sqlite_async/lib/src/impl/context.dart | 7 +++++++ .../database/native_sqlite_connection_impl.dart | 16 ++++++++++++++++ .../sqlite_async/lib/src/sqlite_connection.dart | 3 +++ .../sqlite_async/lib/src/sqlite_queries.dart | 7 +++++++ packages/sqlite_async/lib/src/web/database.dart | 13 +++++++++++++ packages/sqlite_async/pubspec.yaml | 9 +++++++++ packages/sqlite_async/test/basic_test.dart | 17 +++++++++++++++++ 8 files changed, 88 insertions(+) diff --git a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart index f63e0df..959dbf7 100644 --- a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart @@ -164,4 +164,20 @@ final class _UnsafeSyncContext extends UnscopedContext { } }, sql: sql); } + + @override + Future executeMultiple(String sql) async { + task.timeSync('executeBatch', () { + final statements = db.prepareMultiple(sql); + try { + for (var statement in statements) { + task.timeSync('iteration', () => statement.execute()); + } + } finally { + for (var statement in statements) { + statement.dispose(); + } + } + }, sql: sql); + } } diff --git a/packages/sqlite_async/lib/src/impl/context.dart b/packages/sqlite_async/lib/src/impl/context.dart index 0e3eef6..fb88263 100644 --- a/packages/sqlite_async/lib/src/impl/context.dart +++ b/packages/sqlite_async/lib/src/impl/context.dart @@ -12,6 +12,7 @@ import '../sqlite_connection.dart'; abstract base class UnscopedContext implements SqliteReadContext { Future execute(String sql, List parameters); Future executeBatch(String sql, List> parameterSets); + Future executeMultiple(String sql); /// Returns an [UnscopedContext] useful as the outermost transaction. /// @@ -143,6 +144,12 @@ final class ScopedWriteContext extends ScopedReadContext return await _context.executeBatch(sql, parameterSets); } + @override + Future executeMultiple(String sql) { + _checkNotLocked(); + return _context.executeMultiple(sql); + } + @override Future writeTransaction( Future Function(SqliteWriteContext tx) callback) async { diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart index e90739e..13c2eed 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart @@ -275,6 +275,22 @@ final class _UnsafeContext extends UnscopedContext { } }); } + + @override + Future executeMultiple(String sql) async { + return computeWithDatabase((db) async { + final statements = db.prepareMultiple(sql); + try { + for (var statement in statements) { + statement.execute(); + } + } finally { + for (var statement in statements) { + statement.dispose(); + } + } + }); + } } void _sqliteConnectionIsolate(_SqliteConnectionParams params) async { diff --git a/packages/sqlite_async/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart index 0e360fc..b124c57 100644 --- a/packages/sqlite_async/lib/src/sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/sqlite_connection.dart @@ -76,6 +76,9 @@ abstract interface class SqliteWriteContext extends SqliteReadContext { /// parameter set. Future executeBatch(String sql, List> parameterSets); + // Execute multiple SQL statements from a single string, sequentially. + Future executeMultiple(String sql); + /// Open a read-write transaction on this write context. /// /// When called on a [SqliteConnection], this takes a global lock - only one diff --git a/packages/sqlite_async/lib/src/sqlite_queries.dart b/packages/sqlite_async/lib/src/sqlite_queries.dart index 367d23f..c5e4e54 100644 --- a/packages/sqlite_async/lib/src/sqlite_queries.dart +++ b/packages/sqlite_async/lib/src/sqlite_queries.dart @@ -136,6 +136,13 @@ mixin SqliteQueries implements SqliteWriteContext, SqliteConnection { }); } + @override + Future executeMultiple(String sql) { + return writeTransaction((tx) async { + return tx.executeMultiple(sql); + }); + } + @override Future refreshSchema() { return getAll("PRAGMA table_info('sqlite_master')"); diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 23424fc..1f7e410 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -293,6 +293,19 @@ final class _UnscopedContext extends UnscopedContext { }); } + @override + Future executeMultiple(String sql) { + return _task.timeAsync('executeMultiple', sql: sql, () { + return wrapSqliteException(() async { + await _database._database.executeMultiple( + sql, + token: _lock, + checkInTransaction: _checkInTransaction, + ); + }); + }); + } + @override UnscopedContext interceptOutermostTransaction() { // All execute calls done in the callback will be checked for the diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 1ddd431..5e4f5b0 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -5,6 +5,15 @@ repository: https://github.com/powersync-ja/sqlite_async.dart environment: sdk: ">=3.5.0 <4.0.0" +# TODO : Remove this override when the changes are merged and published. +dependency_overrides: + sqlite3_web: + git: + url: https://github.com/stevenctl/sqlite3.dart + ref: stevenctl/execute-multi-support + path: sqlite3_web + + topics: - sqlite - async diff --git a/packages/sqlite_async/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart index e2914b3..00154ce 100644 --- a/packages/sqlite_async/test/basic_test.dart +++ b/packages/sqlite_async/test/basic_test.dart @@ -303,6 +303,23 @@ void main() { ) }); + test('executeMultiple inserts multiple rows', () async { + final db = await testUtils.setupDatabase(path: path); + await createTables(db); + + await db.executeMultiple(''' + INSERT INTO test_data(description) VALUES('row1'); + INSERT INTO test_data(description) VALUES('row2'); + '''); + + final results = await db.getAll('SELECT description FROM test_data ORDER BY id'); + expect(results.length, equals(2)); + expect(results.rows[0], equals(['row1'])); + expect(results.rows[1], equals(['row2'])); + + await db.close(); + }, skip: _isWeb ? 'executeMultiple is not supported on web' : null); + test('with all connections', () async { final maxReaders = _isWeb ? 0 : 3; From 4586d872fe403336e0ebb02abedc0d49c2b94ff5 Mon Sep 17 00:00:00 2001 From: Steven Landow Date: Sat, 6 Dec 2025 07:18:48 -0800 Subject: [PATCH 2/3] update tests --- packages/sqlite_async/test/basic_test.dart | 37 +++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/sqlite_async/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart index 00154ce..42d3103 100644 --- a/packages/sqlite_async/test/basic_test.dart +++ b/packages/sqlite_async/test/basic_test.dart @@ -303,22 +303,51 @@ void main() { ) }); - test('executeMultiple inserts multiple rows', () async { + test('execute single statement with RETURNING populates ResultSet', + () async { + final db = await testUtils.setupDatabase(path: path); + await createTables(db); + final result = await db.execute( + 'INSERT INTO test_data(description) VALUES(?) RETURNING id, description', + ['test returning with params']); + + expect(result.columnNames, equals(['id', 'description'])); + expect(result.rows.length, equals(1)); + expect(result.rows[0][0], isA()); + expect(result.rows[0][1], equals('test returning with params')); + }); + + test( + 'execute single statment with RETURNING populates ResultSet without params', + () async { final db = await testUtils.setupDatabase(path: path); await createTables(db); + final result = await db.execute( + 'INSERT INTO test_data(description) VALUES("test returning without params") RETURNING id, description'); - await db.executeMultiple(''' + expect(result.columnNames, equals(['id', 'description'])); + expect(result.rows.length, equals(1)); + expect(result.rows[0][0], isA()); + expect(result.rows[0][1], equals('test returning without params')); + }); + + test('execute handles multiple statements', () async { + final db = await testUtils.setupDatabase(path: path); + await createTables(db); + + await db.execute(''' INSERT INTO test_data(description) VALUES('row1'); INSERT INTO test_data(description) VALUES('row2'); '''); - final results = await db.getAll('SELECT description FROM test_data ORDER BY id'); + final results = + await db.getAll('SELECT description FROM test_data ORDER BY id'); expect(results.length, equals(2)); expect(results.rows[0], equals(['row1'])); expect(results.rows[1], equals(['row2'])); await db.close(); - }, skip: _isWeb ? 'executeMultiple is not supported on web' : null); + }); test('with all connections', () async { final maxReaders = _isWeb ? 0 : 3; From de3f744f52596d667cd9fe95a12e811a8d40ce17 Mon Sep 17 00:00:00 2001 From: Steven Landow Date: Sat, 6 Dec 2025 07:26:03 -0800 Subject: [PATCH 3/3] use execute instead of prepareMultiple --- .../connection/sync_sqlite_connection.dart | 16 ++++------------ packages/sqlite_async/lib/src/impl/context.dart | 7 ++++--- .../database/native_sqlite_connection_impl.dart | 15 ++++----------- .../sqlite_async/lib/src/sqlite_connection.dart | 5 +++-- .../sqlite_async/lib/src/sqlite_queries.dart | 5 +++-- packages/sqlite_async/lib/src/web/database.dart | 6 ++++-- packages/sqlite_async/pubspec.yaml | 9 --------- packages/sqlite_async/test/basic_test.dart | 4 ++-- 8 files changed, 24 insertions(+), 43 deletions(-) diff --git a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart index 959dbf7..b0e2158 100644 --- a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart @@ -166,18 +166,10 @@ final class _UnsafeSyncContext extends UnscopedContext { } @override - Future executeMultiple(String sql) async { - task.timeSync('executeBatch', () { - final statements = db.prepareMultiple(sql); - try { - for (var statement in statements) { - task.timeSync('iteration', () => statement.execute()); - } - } finally { - for (var statement in statements) { - statement.dispose(); - } - } + Future executeMultiple(String sql, + [List parameters = const []]) async { + task.timeSync('executeMultiple', () { + db.execute(sql, parameters); }, sql: sql); } } diff --git a/packages/sqlite_async/lib/src/impl/context.dart b/packages/sqlite_async/lib/src/impl/context.dart index fb88263..a8a754a 100644 --- a/packages/sqlite_async/lib/src/impl/context.dart +++ b/packages/sqlite_async/lib/src/impl/context.dart @@ -12,7 +12,7 @@ import '../sqlite_connection.dart'; abstract base class UnscopedContext implements SqliteReadContext { Future execute(String sql, List parameters); Future executeBatch(String sql, List> parameterSets); - Future executeMultiple(String sql); + Future executeMultiple(String sql, List parameters); /// Returns an [UnscopedContext] useful as the outermost transaction. /// @@ -145,9 +145,10 @@ final class ScopedWriteContext extends ScopedReadContext } @override - Future executeMultiple(String sql) { + Future executeMultiple(String sql, + [List parameters = const []]) async { _checkNotLocked(); - return _context.executeMultiple(sql); + return await _context.executeMultiple(sql, parameters); } @override diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart index 13c2eed..8181e28 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart @@ -277,18 +277,11 @@ final class _UnsafeContext extends UnscopedContext { } @override - Future executeMultiple(String sql) async { + Future executeMultiple(String sql, + [List parameters = const []]) async { return computeWithDatabase((db) async { - final statements = db.prepareMultiple(sql); - try { - for (var statement in statements) { - statement.execute(); - } - } finally { - for (var statement in statements) { - statement.dispose(); - } - } + // execute allows multiple statements, but does not return results. + db.execute(sql, parameters); }); } } diff --git a/packages/sqlite_async/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart index b124c57..2369585 100644 --- a/packages/sqlite_async/lib/src/sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/sqlite_connection.dart @@ -76,8 +76,9 @@ abstract interface class SqliteWriteContext extends SqliteReadContext { /// parameter set. Future executeBatch(String sql, List> parameterSets); - // Execute multiple SQL statements from a single string, sequentially. - Future executeMultiple(String sql); + // Execute a query that potentially contains multiple statements. + Future executeMultiple(String sql, + [List parameters = const []]); /// Open a read-write transaction on this write context. /// diff --git a/packages/sqlite_async/lib/src/sqlite_queries.dart b/packages/sqlite_async/lib/src/sqlite_queries.dart index c5e4e54..073bde2 100644 --- a/packages/sqlite_async/lib/src/sqlite_queries.dart +++ b/packages/sqlite_async/lib/src/sqlite_queries.dart @@ -137,9 +137,10 @@ mixin SqliteQueries implements SqliteWriteContext, SqliteConnection { } @override - Future executeMultiple(String sql) { + Future executeMultiple(String sql, + [List parameters = const []]) { return writeTransaction((tx) async { - return tx.executeMultiple(sql); + return tx.executeMultiple(sql, parameters); }); } diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 1f7e410..7567eef 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -294,11 +294,13 @@ final class _UnscopedContext extends UnscopedContext { } @override - Future executeMultiple(String sql) { + Future executeMultiple(String sql, + [List parameters = const []]) { return _task.timeAsync('executeMultiple', sql: sql, () { return wrapSqliteException(() async { - await _database._database.executeMultiple( + await _database._database.execute( sql, + parameters: parameters, token: _lock, checkInTransaction: _checkInTransaction, ); diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 5e4f5b0..1ddd431 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -5,15 +5,6 @@ repository: https://github.com/powersync-ja/sqlite_async.dart environment: sdk: ">=3.5.0 <4.0.0" -# TODO : Remove this override when the changes are merged and published. -dependency_overrides: - sqlite3_web: - git: - url: https://github.com/stevenctl/sqlite3.dart - ref: stevenctl/execute-multi-support - path: sqlite3_web - - topics: - sqlite - async diff --git a/packages/sqlite_async/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart index 42d3103..38f5775 100644 --- a/packages/sqlite_async/test/basic_test.dart +++ b/packages/sqlite_async/test/basic_test.dart @@ -331,11 +331,11 @@ void main() { expect(result.rows[0][1], equals('test returning without params')); }); - test('execute handles multiple statements', () async { + test('executeMultiple handles multiple statements', () async { final db = await testUtils.setupDatabase(path: path); await createTables(db); - await db.execute(''' + await db.executeMultiple(''' INSERT INTO test_data(description) VALUES('row1'); INSERT INTO test_data(description) VALUES('row2'); ''');