Skip to content

Commit 6e151a6

Browse files
committed
Add docs and examples.
1 parent 8677a6e commit 6e151a6

16 files changed

+281
-42
lines changed

example/basic_example.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'package:sqlite_async/sqlite_async.dart';
2+
3+
final migrations = SqliteMigrations()
4+
..add(SqliteMigration(1, (tx) async {
5+
await tx.execute(
6+
'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT)');
7+
}));
8+
9+
void main() async {
10+
final db = SqliteDatabase(path: 'test.db');
11+
await migrations.migrate(db);
12+
13+
// Use execute() or executeBatch() for INSERT/UPDATE/DELETE statements
14+
await db.executeBatch('INSERT INTO test_data(data) values(?)', [
15+
['Test1'],
16+
['Test2']
17+
]);
18+
19+
// Use getAll(), get() or getOptional() for SELECT statements
20+
var results = await db.getAll('SELECT * FROM test_data');
21+
print('Results: $results');
22+
23+
// Combine multiple statements into a single write transaction for:
24+
// 1. Atomic persistence (all updates are either applied or rolled back).
25+
// 2. Improved throughput.
26+
await db.writeTransaction((tx) async {
27+
await db.execute('INSERT INTO test_data(data) values(?)', ['Test3']);
28+
await db.execute('INSERT INTO test_data(data) values(?)', ['Test4']);
29+
});
30+
31+
await db.close();
32+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'dart:ffi';
2+
import 'dart:io';
3+
import 'dart:isolate';
4+
5+
import 'package:sqlite_async/sqlite_async.dart';
6+
import 'package:sqlite3/open.dart' as sqlite_open;
7+
import 'package:sqlite3/sqlite3.dart' as sqlite;
8+
9+
class TestOpenFactory extends DefaultSqliteOpenFactory {
10+
TestOpenFactory({required super.path, super.sqliteOptions});
11+
12+
@override
13+
sqlite.Database open(SqliteOpenOptions options) {
14+
final db = super.open(options);
15+
16+
db.createFunction(
17+
functionName: 'sleep',
18+
argumentCount: const sqlite.AllowedArgumentCount(1),
19+
function: (args) {
20+
final millis = args[0] as int;
21+
sleep(Duration(milliseconds: millis));
22+
return millis;
23+
},
24+
);
25+
26+
db.createFunction(
27+
functionName: 'isolate_name',
28+
argumentCount: const sqlite.AllowedArgumentCount(0),
29+
function: (args) {
30+
return Isolate.current.debugName;
31+
},
32+
);
33+
34+
return db;
35+
}
36+
}
37+
38+
void main() async {
39+
final db = SqliteDatabase.withFactory(TestOpenFactory(path: 'test.db'));
40+
await db.get('SELECT sleep(5)');
41+
print(await db.get('SELECT isolate_name()'));
42+
print(await db.execute('SELECT isolate_name()'));
43+
await db.close();
44+
}

example/linux_cli_example.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'dart:ffi';
2+
3+
import 'package:sqlite_async/sqlite_async.dart';
4+
import 'package:sqlite3/open.dart' as sqlite_open;
5+
import 'package:sqlite3/sqlite3.dart' as sqlite;
6+
7+
const defaultSqlitePath = 'libsqlite3.so.0';
8+
9+
class TestOpenFactory extends DefaultSqliteOpenFactory {
10+
String sqlitePath;
11+
12+
TestOpenFactory(
13+
{required super.path,
14+
super.sqliteOptions,
15+
this.sqlitePath = defaultSqlitePath});
16+
17+
@override
18+
sqlite.Database open(SqliteOpenOptions options) {
19+
sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.linux, () {
20+
return DynamicLibrary.open(sqlitePath);
21+
});
22+
final db = super.open(options);
23+
24+
return db;
25+
}
26+
}
27+
28+
void main() async {
29+
final db = SqliteDatabase.withFactory(TestOpenFactory(path: 'test.db'));
30+
final version = await db.get('SELECT sqlite_version() as version');
31+
print("Version: ${version['version']}");
32+
await db.close();
33+
}

example/migration_example.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:sqlite_async/sqlite_async.dart';
2+
3+
final migrations = SqliteMigrations()
4+
..add(SqliteMigration(1, (tx) async {
5+
await tx.execute(
6+
'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT)');
7+
}))
8+
..add(SqliteMigration(2, (tx) async {
9+
await tx.execute('ALTER TABLE test_data ADD COLUMN comment TEXT');
10+
},
11+
// Optional: Add a down migration that will execute when users install an older application version.
12+
// This is typically not required for Android or iOS where users cannot install older versions,
13+
// but may be useful on other platforms or during development.
14+
downMigration: SqliteDownMigration(toVersion: 1)
15+
..add('ALTER TABLE test_data DROP COLUMN comment')))
16+
// Optional: Provide a function to initialize the database from scratch,
17+
// avoiding the need to run through incremental migrations for new databases.
18+
..createDatabase = SqliteMigration(
19+
2,
20+
(tx) async {
21+
await tx.execute(
22+
'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT, comment TEXT)');
23+
},
24+
);
25+
26+
void main() async {
27+
final db = SqliteDatabase(path: 'test.db');
28+
// Make sure to run migrations before any other functions access the database.
29+
await migrations.migrate(db);
30+
await db.close();
31+
}

example/watch_example.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'package:sqlite_async/sqlite_async.dart';
2+
3+
final migrations = SqliteMigrations()
4+
..add(SqliteMigration(1, (tx) async {
5+
await tx.execute(
6+
'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT)');
7+
}));
8+
9+
void main() async {
10+
final db = SqliteDatabase(path: 'test.db');
11+
await migrations.migrate(db);
12+
13+
// This query is re-executed every time test_data changes.
14+
var stream1 = db.watch('SELECT data FROM test_data');
15+
var subscription1 = stream1.listen((results) {
16+
print(results);
17+
});
18+
19+
// Use this to get notifications of changes on one or more tables.
20+
var stream2 = db.onChange(['test_data']);
21+
var subscription2 = stream2.listen((changes) {
22+
print(changes);
23+
});
24+
25+
// This achieves the same as db.watch(), but:
26+
// 1. Explicitly specifies the tables to watch for changes.
27+
// 2. May run any number of queries when a change is triggered.
28+
var stream3 = db.onChange(['test_data']).asyncMap((event) async {
29+
return db.getAll('SELECT count() as count FROM test_data');
30+
});
31+
var subscription3 = stream3.listen((results) {
32+
print(results);
33+
});
34+
35+
for (var i = 0; i < 5; i++) {
36+
await db.execute('INSERT INTO test_data(data) values(?)', ['Test $i']);
37+
await Future.delayed(Duration(milliseconds: 100));
38+
}
39+
40+
subscription1.cancel();
41+
subscription2.cancel();
42+
subscription3.cancel();
43+
await db.close();
44+
}

lib/sqlite_async.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export 'src/sqlite_database.dart';
99
export 'src/sqlite_options.dart';
1010
export 'src/sqlite_open_factory.dart';
1111
export 'src/sqlite_migrations.dart';
12+
export 'src/isolate_connection_factory.dart';

lib/src/isolate_connection_factory.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'sqlite_connection_impl.dart';
99
import 'sqlite_open_factory.dart';
1010
import 'update_notification.dart';
1111

12+
/// A connection factory that can be passed to different isolates.
1213
class IsolateConnectionFactory {
1314
SqliteOpenFactory openFactory;
1415
SerializedMutex mutex;
@@ -19,6 +20,9 @@ class IsolateConnectionFactory {
1920
required this.mutex,
2021
required this.upstreamPort});
2122

23+
/// Open a new SqliteConnection.
24+
///
25+
/// This opens a single connection in a background execution isolate.
2226
SqliteConnection open({String? debugName, bool readOnly = false}) {
2327
final updates = _IsolateUpdateListener(upstreamPort);
2428

lib/src/log.dart

Lines changed: 0 additions & 3 deletions
This file was deleted.

lib/src/sqlite_database.dart

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ import 'sqlite_options.dart';
1313
import 'sqlite_queries.dart';
1414
import 'update_notification.dart';
1515

16-
/// A managed database.
16+
/// A SQLite database instance.
1717
///
18-
/// Use one instance per database file.
19-
///
20-
/// Use [SqliteDatabase.connect] to connect to the PowerSync service,
21-
/// to keep the local database in sync with the remote database.
18+
/// Use one instance per database file. If multiple instances are used, update
19+
/// notifications may not trigger, and calls may fail with "SQLITE_BUSY" errors.
2220
class SqliteDatabase with SqliteQueries implements SqliteConnection {
2321
/// Maximum number of concurrent read transactions.
2422
final int maxReaders;
@@ -56,22 +54,25 @@ class SqliteDatabase with SqliteQueries implements SqliteConnection {
5654
/// from the last committed write transaction.
5755
///
5856
/// A maximum of [maxReaders] concurrent read transactions are allowed.
59-
///
60-
/// Advanced: Use [sqliteSetup] to execute custom initialization logic in
61-
/// each database isolate.
6257
factory SqliteDatabase(
6358
{required path,
6459
maxReaders = 5,
6560
options = const SqliteOptions.defaults()}) {
6661
final factory =
6762
DefaultSqliteOpenFactory(path: path, sqliteOptions: options);
68-
return SqliteDatabase.withFactory(openFactory: factory);
63+
return SqliteDatabase.withFactory(factory);
6964
}
7065

7166
/// Advanced: Open a database with a specified factory.
7267
///
73-
/// Use when control is required over the opening process.
74-
SqliteDatabase.withFactory({required this.openFactory, this.maxReaders = 5}) {
68+
/// The factory is used to open each database connection in background isolates.
69+
///
70+
/// Use when control is required over the opening process. Examples include:
71+
/// 1. Specifying the path to `libsqlite.so` on Linux.
72+
/// 2. Running additional per-connection PRAGMA statements on each connection.
73+
/// 3. Creating custom SQLite functions.
74+
/// 4. Creating temporary views or triggers.
75+
SqliteDatabase.withFactory(this.openFactory, {this.maxReaders = 5}) {
7576
updates = _updatesController.stream;
7677

7778
_listenForEvents();
@@ -137,6 +138,9 @@ class SqliteDatabase with SqliteQueries implements SqliteConnection {
137138
});
138139
}
139140

141+
/// A connection factory that can be passed to different isolates.
142+
///
143+
/// Use this to access the database in background isolates.s
140144
IsolateConnectionFactory isolateConnectionFactory() {
141145
return IsolateConnectionFactory(
142146
openFactory: openFactory,

lib/src/sqlite_migrations.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@ import 'package:sqlite3/sqlite3.dart';
55

66
import 'sqlite_connection.dart';
77

8+
/// Migrations to initialize and update a database.
89
class SqliteMigrations {
10+
/// Name of table used to store migrations.
11+
///
12+
/// Defaults to "_migrations".
913
String migrationTable;
14+
15+
/// List of migrations to execute, in order. Use [add] to add new migrations.
1016
List<SqliteMigration> migrations = [];
17+
18+
/// Optional: Migration to create database from scratch.
19+
///
20+
/// Use this to speed up initialization for a fresh database.
21+
///
22+
/// This must be supplied _in addition to_ migrations for the same version,
23+
/// and should produce the same state for the database.
1124
SqliteMigration? createDatabase;
1225

1326
SqliteMigrations({this.migrationTable = "_migrations"});
@@ -34,10 +47,12 @@ class SqliteMigrations {
3447
migrations.add(migration);
3548
}
3649

50+
/// The current version as specified by the migrations.
3751
get version {
3852
return migrations.isEmpty ? 0 : migrations.last.toVersion;
3953
}
4054

55+
/// Get the last applied migration version in the database.
4156
Future<int> getCurrentVersion(SqliteWriteContext db) async {
4257
try {
4358
final currentVersionRow = await db.getOptional(
@@ -69,6 +84,9 @@ class SqliteMigrations {
6984
}
7085
}
7186

87+
/// Initialize or update the database.
88+
///
89+
/// Throws MigrationError if the database cannot be migrated.
7290
Future<void> migrate(SqliteConnection db) async {
7391
_validateCreateDatabase();
7492

@@ -167,12 +185,22 @@ class MigrationError extends Error {
167185
typedef SqliteMigrationFunction = FutureOr<void> Function(
168186
SqliteWriteContext tx);
169187

188+
/// A migration for a single database version.
170189
class SqliteMigration {
190+
/// Function to execute for the migration.
171191
final SqliteMigrationFunction fn;
192+
193+
/// Database version that this migration upgrades to.
172194
final int toVersion;
173195

174196
/// Optional: Add a down migration to allow this migration to be reverted
175197
/// if the user installs an older version.
198+
///
199+
/// If the user will never downgrade the application/database version, this is
200+
/// not required.
201+
///
202+
/// Limited downgrade support can be added by only providing downMigrations
203+
/// for the last migration(s).
176204
SqliteDownMigration? downMigration;
177205

178206
SqliteMigration(this.toVersion, this.fn, {this.downMigration});
@@ -189,12 +217,18 @@ class _SqliteMigrationStatement {
189217
}
190218
}
191219

220+
/// Set of down migration statements, persisted in the database.
221+
///
222+
/// Since this will execute in an older application version, only static
223+
/// SQL statements are supported.
192224
class SqliteDownMigration {
225+
/// The database version after this downgrade.
193226
final int toVersion;
194227
final List<_SqliteMigrationStatement> _statements = [];
195228

196229
SqliteDownMigration({required this.toVersion});
197230

231+
/// Add an statement to execute to downgrade the database version.
198232
add(String sql, [List<Object?>? params]) {
199233
_statements.add(_SqliteMigrationStatement(sql, params ?? []));
200234
}

0 commit comments

Comments
 (0)