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()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 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
Stream<List<Map<String, dynamic>>> watch(String sql, {List<dynamic>? parameters}) {
return Stream.fromFuture(getAll(sql, parameters));
}
@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 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 sql, [List<dynamic>? arguments]) async {
return db.writeTransaction((tx) async {
final context = _PowerSyncTransactionContext(tx);
return context.execute(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
Stream<List<Map<String, dynamic>>> watch(
String sql, {
List<dynamic>? parameters,
}) {
return db.watch(sql, parameters: parameters ?? []).map(
(rows) => rows.map((r) => Map<String, dynamic>.from(r)).toList(),
);
}
@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 sql, [List<dynamic>? arguments]) =>
_executeInternal(sql, arguments);
@override
Future<dynamic> insert(String table, Map<String, dynamic> values) =>
_insertInternal(table, values);
}