Database Adapters
Bavard is designed to be purely agnostic regarding the underlying database driver. This ensures the core package remains lightweight and free of unnecessary dependencies.
To connect Bavard to a database, you must implement the DatabaseAdapter interface.
Grammars & Dialects
Since Bavard supports multiple SQL dialects, your adapter must specify which Grammar it uses to generate SQL.
- SQLiteGrammar: For SQLite, PowerSync, Drift, and sqflite.
- PostgresGrammar: For PostgreSQL, Supabase, and other Postgres-compatible drivers.
The DatabaseAdapter interface requires you to implement the grammar getter:
@override
Grammar get grammar => SQLiteGrammar(); // or PostgresGrammar()Implementing DatabaseAdapter
The DatabaseAdapter interface is straightforward. You only need to implement methods for executing SQL queries and managing transactions.
Automatic Reactivity
You do not need to implement any logic for watch() or change notifications. Bavard's core DatabaseManager automatically handles table change tracking and stream updates by wrapping your adapter's execute and insert calls.
Key Methods
getAll(sql, [args]): Returns a list of rows (List<Map<String, dynamic>>).get(sql, [args]): Returns a single row (Map<String, dynamic>).execute(table, sql, [args]): Executes an UPDATE/DELETE command and returns the number of affected rows. Note: Thetableargument is used by the core for change tracking.insert(table, values): Inserts a row and returns the new ID (or primary key).transaction(callback): Executes a callback within a database transaction.
Transactions
The transaction method receives a callback that provides a TransactionContext. This context has the same API as the adapter (get, getAll, execute, insert) but operates inside the transaction.
Your adapter is responsible for:
- Starting the transaction (e.g.,
BEGIN). - Creating a context that executes queries on that transaction.
- Committing or Rolling back based on whether the callback throws an error.
Bavard automatically handles buffering notifications during transactions, so you don't need to worry about dirty reads in your streams.
Below are the reference implementations for the most common use cases. You can copy these files directly into your project.
SQLite (via package:sqlite3)
Best for: Dart Desktop / Mobile (FFI).
Dependencies:
bavardsqlite3
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:sqlite3/sqlite3.dart';
import 'package:bavard/bavard.dart';
import 'package:bavard/src/grammars/sqlite_grammar.dart';
import 'package:bavard/schema.dart';
class SqliteAdapter implements DatabaseAdapter {
final Database _db;
SqliteAdapter(this._db);
@override
Grammar get grammar => SQLiteGrammar();
List<dynamic> _sanitize(List<dynamic> args) {
return args.map((arg) {
if (arg is DateTime) return arg.toIso8601String();
if (arg is bool) return arg ? 1 : 0;
if (arg is Map || arg is List) return jsonEncode(arg);
return arg;
}).toList();
}
@override
Future<List<Map<String, dynamic>>> getAll(String sql, [List<dynamic>? arguments]) async {
final result = _db.select(sql, _sanitize(arguments ?? []));
return result.map((row) => Map<String, dynamic>.from(row)).toList();
}
@override
Future<Map<String, dynamic>> get(String sql, [List<dynamic>? arguments]) async {
final result = await getAll(sql, arguments);
if (result.isEmpty) return {};
return result.first;
}
@override
Future<int> execute(String table, String sql, [List<dynamic>? arguments]) async {
_db.execute(sql, _sanitize(arguments ?? []));
return _db.getUpdatedRows();
}
@override
Future<dynamic> insert(String table, Map<String, dynamic> values) async {
final columns = values.keys.map((k) => '"$k"').join(', ');
final placeholders = List.filled(values.length, '?').join(', ');
final sql = 'INSERT INTO "$table" ($columns) VALUES ($placeholders)';
_db.execute(sql, _sanitize(values.values.toList()));
return _db.lastInsertRowId;
}
@override
bool get supportsTransactions => true;
@override
Future<T> transaction<T>(Future<T> Function(TransactionContext txn) callback) async {
_db.execute('BEGIN TRANSACTION');
try {
final txnContext = _SqliteTransactionContext(_db, _sanitize);
final result = await callback(txnContext);
_db.execute('COMMIT');
return result;
} catch (e) {
_db.execute('ROLLBACK');
rethrow;
}
}
}
class _SqliteTransactionContext implements TransactionContext {
final Database _db;
final List<dynamic> Function(List<dynamic>) _sanitize;
_SqliteTransactionContext(this._db, this._sanitize);
@override
Future<List<Map<String, dynamic>>> getAll(String sql, [List? arguments]) async {
final result = _db.select(sql, _sanitize(arguments ?? []));
return result.map((row) => Map<String, dynamic>.from(row)).toList();
}
@override
Future<Map<String, dynamic>> get(String sql, [List? arguments]) async {
final list = await getAll(sql, arguments);
return list.isNotEmpty ? list.first : {};
}
@override
Future<int> execute(String table, String sql, [List? arguments]) async {
_db.execute(sql, _sanitize(arguments ?? []));
return _db.getUpdatedRows();
}
@override
Future<dynamic> insert(String table, Map<String, dynamic> values) async {
final columns = values.keys.map((k) => '"$k"').join(', ');
final placeholders = List.filled(values.length, '?').join(', ');
final sql = 'INSERT INTO "$table" ($columns) VALUES ($placeholders)';
_db.execute(sql, _sanitize(values.values.toList()));
return _db.lastInsertRowId;
}
}PowerSync
Best for: Offline-First Flutter Apps.
Bavard is the perfect ORM for PowerSync because it handles query generation while letting PowerSync handle the complex synchronization logic.
Dependencies:
bavardpowersyncsqlite_async
import 'dart:async';
import 'package:bavard/bavard.dart';
import 'package:powersync/powersync.dart';
import 'package:sqlite3/common.dart';
import 'package:sqlite_async/sqlite_async.dart';
mixin _PowerSyncExecutor {
Future<ResultSet> executeRaw(String sql, [List<dynamic>? arguments]);
Future<List<Map<String, dynamic>>> getAll(
String sql, [
List<dynamic>? arguments,
]) async {
final result = await executeRaw(sql, arguments ?? []);
return result.map((row) => Map<String, dynamic>.from(row)).toList();
}
Future<Map<String, dynamic>> get(
String sql, [
List<dynamic>? arguments,
]) async {
final rows = await getAll(sql, arguments);
return rows.isNotEmpty ? rows.first : {};
}
Future<int> _executeInternal(String sql, [List<dynamic>? arguments]) async {
await executeRaw(sql, arguments ?? []);
final result = await executeRaw('SELECT changes() as affected');
return result.first['affected'] as int;
}
Future<dynamic> _insertInternal(String table, Map<String, dynamic> values) async {
if (values.keys.any((k) => !RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(k))) {
throw ArgumentError('Invalid column name in insert values');
}
final keys = values.keys.join(', ');
final placeholders = List.filled(values.length, '?').join(', ');
final sql = 'INSERT INTO $table ($keys) VALUES ($placeholders)';
await executeRaw(sql, values.values.toList());
if (values['id'] != null) {
return values['id'];
}
final result = await executeRaw('SELECT last_insert_rowid() as id');
return result.first['id'];
}
}
class PowerSyncDatabaseAdapter with _PowerSyncExecutor implements DatabaseAdapter {
final PowerSyncDatabase db;
PowerSyncDatabaseAdapter(this.db);
@override
Future<ResultSet> executeRaw(String sql, [List<dynamic>? arguments]) =>
db.execute(sql, arguments ?? []);
@override
Grammar get grammar => SQLiteGrammar();
@override
Future<int> execute(String table, String sql, [List<dynamic>? arguments]) async {
return db.writeTransaction((tx) async {
final context = _PowerSyncTransactionContext(tx);
return context.execute(table, sql, arguments);
});
}
@override
Future<dynamic> insert(String table, Map<String, dynamic> values) async {
return db.writeTransaction((tx) async {
final context = _PowerSyncTransactionContext(tx);
return context.insert(table, values);
});
}
@override
bool get supportsTransactions => true;
@override
Future<T> transaction<T>(
Future<T> Function(TransactionContext txn) callback,
) async {
return db.writeTransaction((tx) async {
final context = _PowerSyncTransactionContext(tx);
return await callback(context);
});
}
}
class _PowerSyncTransactionContext with _PowerSyncExecutor implements TransactionContext {
final SqliteWriteContext _txn;
_PowerSyncTransactionContext(this._txn);
@override
Future<ResultSet> executeRaw(String sql, [List<dynamic>? arguments]) =>
_txn.execute(sql, arguments ?? []);
@override
Future<int> execute(String table, String sql, [List<dynamic>? arguments]) =>
_executeInternal(sql, arguments);
@override
Future<dynamic> insert(String table, Map<String, dynamic> values) =>
_insertInternal(table, values);
}