Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 73 additions & 10 deletions lib/model/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ class Accounts extends Table {
/// It never changes for a given account.
Column<int> get userId => integer()();

/// The ID of this client device as logged into this account.
///
/// This comes from [registerClientDevice] and corresponds to
/// a device ID in [InitialSnapshot.devices].
///
/// Once this is no longer null, it never again changes for
/// a given account record.
///
/// This is null if the server is old (TODO(server-12))
/// and lacks the concept of device ID,
/// as well as when the client has only recently upgraded from a
/// version that lacked this column and has not yet populated it.
Column<int> get deviceId => integer().nullable()();

Column<String> get email => text()();
Column<String> get apiKey => text()();

Expand All @@ -165,26 +179,47 @@ class Accounts extends Table {
];
}

/// The table of [PushKey] records, with keys for E2EE push notifications.
class PushKeys extends Table {
Column<int> get pushKeyId => integer()();
Column<Uint8List> get pushKey => blob()();

Column<int> get accountId => integer()
.references(Accounts, #id, onDelete: .cascade)();

Column<int> get createdTimestamp => integer()();
Column<int> get supersededTimestamp => integer().nullable()();

@override
List<Set<Column<Object>>> get uniqueKeys => [
{pushKeyId},
];
}

class UriConverter extends TypeConverter<Uri, String> {
const UriConverter();
@override String toSql(Uri value) => value.toString();
@override Uri fromSql(String fromDb) => Uri.parse(fromDb);
}

@DriftDatabase(tables: [GlobalSettings, BoolGlobalSettings, IntGlobalSettings, Accounts])
const _allTables = [
GlobalSettings, BoolGlobalSettings, IntGlobalSettings,
Accounts, PushKeys,
];

@DriftDatabase(tables: _allTables)
class AppDatabase extends _$AppDatabase {
AppDatabase(super.e);

// When updating the schema:
// * Make the change in the table classes, and bump latestSchemaVersion.
// * Export the new schema and generate test migrations with drift:
// $ tools/check --fix drift
// and generate database code with build_runner.
// See ../../README.md#generated-files for more
// information on using the build_runner.
// * Write a migration in `_migrationSteps` below.
// * Make the change in the table classes.
// * Bump latestSchemaVersion.
// * Updated generated code for the new schema:
// $ tools/check --fix build_runner drift
// * Fix resulting analyzer errors; in particular,
// write a migration in `_migrationSteps` below.
// * Write tests.
static const int latestSchemaVersion = 12; // See note.
static const int latestSchemaVersion = 14; // See note.

@override
int get schemaVersion => latestSchemaVersion;
Expand Down Expand Up @@ -283,6 +318,12 @@ class AppDatabase extends _$AppDatabase {
await m.addColumn(schema.accounts, schema.accounts.realmName);
await m.addColumn(schema.accounts, schema.accounts.realmIcon);
},
from12To13: (m, schema) async {
await m.addColumn(schema.accounts, schema.accounts.deviceId);
},
from13To14: (m, schema) async {
await m.createTable(schema.pushKeys);
},
);

Future<void> _createLatestSchema(Migrator m) async {
Expand Down Expand Up @@ -319,7 +360,11 @@ class AppDatabase extends _$AppDatabase {

assert(debugLog('Upgrading DB schema from v$from to v$to.'));
await m.runMigrationSteps(from: from, to: to, steps: _migrationSteps);
});
},
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
);
}

Future<GlobalSettingsData> getGlobalSettings() async {
Expand Down Expand Up @@ -360,6 +405,24 @@ class AppDatabase extends _$AppDatabase {
rethrow;
}
}

Future<void> createPushKey(PushKeysCompanion values) async {
try {
await into(pushKeys).insert(values);
} catch (e) {
// Unwrap cause if it's a remote Drift call. On the app, it's running
// via a remote, but on local tests, it's running natively so
// unwrapping is not required.
final cause = (e is DriftRemoteException) ? e.remoteCause : e;
if (cause case SqliteException(
extendedResultCode: SqlExtendedError.SQLITE_CONSTRAINT_UNIQUE)) {
throw PushKeyAlreadyExistsException();
}
rethrow;
}
}
}

class AccountAlreadyExistsException implements Exception {}

class PushKeyAlreadyExistsException implements Exception {}
Loading