fix: Return the userId to the CRDT data table
This commit is contained in:
parent
cef8ef7df5
commit
0a3fb46163
6 changed files with 63 additions and 26 deletions
|
|
@ -3,15 +3,31 @@ import 'package:crdt/crdt.dart';
|
||||||
/// A [Hlc] instance that is used to generate unique timestamps for CRDT operations.
|
/// A [Hlc] instance that is used to generate unique timestamps for CRDT operations.
|
||||||
/// Will keep track of the HLC during the lifetime of the application.
|
/// Will keep track of the HLC during the lifetime of the application.
|
||||||
class StatefulHlc {
|
class StatefulHlc {
|
||||||
StatefulHlc._(this.nodeId) : lastHlc = Hlc.now(nodeId);
|
StatefulHlc._(this.userId, this.nodeId) {
|
||||||
|
lastHlc = _lastUserIdHlc[userId]?.apply(nodeId: nodeId) ?? Hlc.zero(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final Map<String, Hlc> _lastUserIdHlc = {};
|
||||||
static final Map<String, StatefulHlc> _instances = {};
|
static final Map<String, StatefulHlc> _instances = {};
|
||||||
|
|
||||||
/// Returns a cached instance of [StatefulHlc] for the provided [nodeId].
|
/// Initialize the HLC for the provided [userId].
|
||||||
static StatefulHlc cached(String nodeId) {
|
///
|
||||||
return _instances.putIfAbsent(nodeId, () => StatefulHlc._(nodeId));
|
/// This method allows a pre-initialization of the HLC for the node from the
|
||||||
|
/// last stored HLC for the user - regardless of the node ID. If no [initialHlc]
|
||||||
|
/// is provided, a zero HLC will be used.
|
||||||
|
static void initialize(String userId, Hlc initialHlc) {
|
||||||
|
if (_lastUserIdHlc.containsKey(userId)) return;
|
||||||
|
_lastUserIdHlc[userId] = initialHlc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a cached instance of [StatefulHlc] for the provided [nodeId].
|
||||||
|
static StatefulHlc cached(String userId, String nodeId) {
|
||||||
|
return _instances.putIfAbsent(nodeId, () => StatefulHlc._(userId, nodeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The user ID for the CRDT system.
|
||||||
|
final String userId;
|
||||||
|
|
||||||
/// The node ID for the CRDT system.
|
/// The node ID for the CRDT system.
|
||||||
final String nodeId;
|
final String nodeId;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,8 @@ class OfflineSyncMigrator extends Migrator {
|
||||||
final crdtTable = crdtDb.crdtDataTable;
|
final crdtTable = crdtDb.crdtDataTable;
|
||||||
await database.customStatement(
|
await database.customStatement(
|
||||||
'ALTER TABLE ${crdtTable.actualTableName} '
|
'ALTER TABLE ${crdtTable.actualTableName} '
|
||||||
'PARTITION BY RANGE (${crdtTable.tblName.name});',
|
// TODO: Fix this partitioning, since RANGE can not be used for discrete values.
|
||||||
// 'PARTITION BY RANGE (${crdtTable.userId.name}, ${crdtTable.tblName.name});',
|
'PARTITION BY RANGE (${crdtTable.userId.name}, ${crdtTable.tblName.name});',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:drift/native.dart';
|
||||||
|
|
||||||
import '/src/hlc/stateful.dart';
|
import '/src/hlc/stateful.dart';
|
||||||
import '/src/triggers.dart';
|
import '/src/triggers.dart';
|
||||||
|
import 'tables/data.dart';
|
||||||
|
|
||||||
/// The database setup function to register the HLC function.
|
/// The database setup function to register the HLC function.
|
||||||
///
|
///
|
||||||
|
|
@ -32,6 +33,18 @@ DatabaseSetup get registerHlcFunction {
|
||||||
deterministic: false,
|
deterministic: false,
|
||||||
directOnly: false,
|
directOnly: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Load the last HLC for the user from the control table.
|
||||||
|
try {
|
||||||
|
database.select(CrdtDataTable.getLastHlcTimestampQuery()).forEach((row) {
|
||||||
|
final userId = row.values.first! as String;
|
||||||
|
final lastHlcTimestamp = hlcConverter.fromSql(row.values.last! as String);
|
||||||
|
StatefulHlc.initialize(userId, lastHlcTimestamp);
|
||||||
|
});
|
||||||
|
} on Exception catch (_) {
|
||||||
|
// Ignore errors, as the HLC will be initialized to zero.
|
||||||
|
// This is expected to happen on a new database.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,14 @@ export '../hlc/converter.dart';
|
||||||
/// the client will have the `user_id` column with a single value, while the server
|
/// the client will have the `user_id` column with a single value, while the server
|
||||||
/// will have all users' data in the same table. To avoid performance issues, on a
|
/// will have all users' data in the same table. To avoid performance issues, on a
|
||||||
/// Postgres server, the `user_id` and `table_name` columns are partition keys.
|
/// Postgres server, the `user_id` and `table_name` columns are partition keys.
|
||||||
///
|
|
||||||
@TableIndex(
|
@TableIndex(
|
||||||
name: 'idx_crdt_data_tbl_name_column_name_row_id',
|
name: 'idx_crdt_data_tbl_name_column_name_row_id',
|
||||||
columns: {#tblName, #columnName, #rowId},
|
columns: {#userId, #tblName, #columnName, #rowId},
|
||||||
unique: true,
|
unique: true,
|
||||||
)
|
)
|
||||||
@TableIndex(
|
@TableIndex(
|
||||||
name: 'idx_crdt_data_hlc_timestamp',
|
name: 'idx_crdt_data_hlc_timestamp',
|
||||||
columns: {#hlcTimestamp},
|
columns: {#userId, #hlcTimestamp},
|
||||||
)
|
)
|
||||||
@DataClassName('CrdtDataEntry')
|
@DataClassName('CrdtDataEntry')
|
||||||
class CrdtDataTable extends Table {
|
class CrdtDataTable extends Table {
|
||||||
|
|
@ -35,14 +34,8 @@ class CrdtDataTable extends Table {
|
||||||
@override
|
@override
|
||||||
bool get isStrict => true;
|
bool get isStrict => true;
|
||||||
|
|
||||||
// MAYBE: It seems it is not possible to have the userId column here without
|
/// Identifier for the user or client that owns the data.
|
||||||
// creating hard requirements on how each table should relate to the userId.
|
late final userId = text()();
|
||||||
// Either all tables would have to have this column, or users would have to
|
|
||||||
// supply a query to join the userId with the changed table. Either way, it
|
|
||||||
// makes the module a lot less plug-and-play than it should, so it might be
|
|
||||||
// better to not have it here and accept the query performance penalty.
|
|
||||||
// /// Identifier for the user or client that owns the data.
|
|
||||||
// late final userId = text()();
|
|
||||||
|
|
||||||
/// Name of the table that data belongs to.
|
/// Name of the table that data belongs to.
|
||||||
late final tblName = text().named('table_name')();
|
late final tblName = text().named('table_name')();
|
||||||
|
|
@ -60,5 +53,17 @@ class CrdtDataTable extends Table {
|
||||||
late final rawValue = sqliteAny().nullable()();
|
late final rawValue = sqliteAny().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {/*userId,*/ tblName, columnName, rowId};
|
Set<Column> get primaryKey => {userId, tblName, columnName, rowId};
|
||||||
|
|
||||||
|
/// Query to get the last HLC timestamp for each user.
|
||||||
|
///
|
||||||
|
/// Declared here to ensure the query is synchronized with the table.
|
||||||
|
static String getLastHlcTimestampQuery() {
|
||||||
|
return '''
|
||||||
|
SELECT user_id,
|
||||||
|
max(hlc_timestamp) as last_hlc_timestamp
|
||||||
|
FROM __crdt_data
|
||||||
|
GROUP BY 1
|
||||||
|
''';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,14 @@ abstract class OfflineSyncTriggers {
|
||||||
${_crdtDataAllColumns.join(', ')}
|
${_crdtDataAllColumns.join(', ')}
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
'${migrator.userId}' AS "user_id",
|
||||||
'$tableName' AS "table_name",
|
'$tableName' AS "table_name",
|
||||||
column_name,
|
column_name,
|
||||||
$uniqueRowId AS "row_id",
|
$uniqueRowId AS "row_id",
|
||||||
"hlc_timestamp",
|
"hlc_timestamp",
|
||||||
raw_value
|
raw_value
|
||||||
FROM (
|
FROM (
|
||||||
SELECT $nextHlcFunction('${migrator.nodeId}') AS "hlc_timestamp"
|
SELECT $nextHlcFunction('${migrator.userId}', '${migrator.nodeId}') AS "hlc_timestamp"
|
||||||
), (
|
), (
|
||||||
${columnInserts.join('\n UNION ALL\n')}
|
${columnInserts.join('\n UNION ALL\n')}
|
||||||
)$whereClause;''';
|
)$whereClause;''';
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,17 @@ CREATE TRIGGER IF NOT EXISTS __crdt__todos__insert
|
||||||
AFTER INSERT ON "todos"
|
AFTER INSERT ON "todos"
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT OR REPLACE INTO __crdt_data (
|
INSERT OR REPLACE INTO __crdt_data (
|
||||||
"table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
"user_id", "table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
'${migrator.userId}' AS "user_id",
|
||||||
'todos' AS "table_name",
|
'todos' AS "table_name",
|
||||||
column_name,
|
column_name,
|
||||||
NEW."id" AS "row_id",
|
NEW."id" AS "row_id",
|
||||||
"hlc_timestamp",
|
"hlc_timestamp",
|
||||||
raw_value
|
raw_value
|
||||||
FROM (
|
FROM (
|
||||||
SELECT $nextHlcFunction('${migrator.nodeId}') AS "hlc_timestamp"
|
SELECT $nextHlcFunction('${migrator.userId}', '${migrator.nodeId}') AS "hlc_timestamp"
|
||||||
), (
|
), (
|
||||||
SELECT
|
SELECT
|
||||||
'id' AS "column_name",
|
'id' AS "column_name",
|
||||||
|
|
@ -88,17 +89,17 @@ CREATE TRIGGER IF NOT EXISTS __crdt__todos__update
|
||||||
AFTER UPDATE ON "todos"
|
AFTER UPDATE ON "todos"
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT OR REPLACE INTO __crdt_data (
|
INSERT OR REPLACE INTO __crdt_data (
|
||||||
"table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
"user_id", "table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
'${migrator.userId}' AS "user_id",
|
||||||
'todos' AS "table_name",
|
'todos' AS "table_name",
|
||||||
column_name,
|
column_name,
|
||||||
OLD."id" AS "row_id",
|
OLD."id" AS "row_id",
|
||||||
"hlc_timestamp",
|
"hlc_timestamp",
|
||||||
raw_value,
|
raw_value
|
||||||
value_changed
|
|
||||||
FROM (
|
FROM (
|
||||||
SELECT $nextHlcFunction('${migrator.nodeId}') AS "hlc_timestamp"
|
SELECT $nextHlcFunction('${migrator.userId}', '${migrator.nodeId}') AS "hlc_timestamp"
|
||||||
), (
|
), (
|
||||||
SELECT
|
SELECT
|
||||||
'id' AS "column_name",
|
'id' AS "column_name",
|
||||||
|
|
@ -146,16 +147,17 @@ CREATE TRIGGER IF NOT EXISTS __crdt__todos__delete
|
||||||
AFTER DELETE ON "todos"
|
AFTER DELETE ON "todos"
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT OR REPLACE INTO __crdt_data (
|
INSERT OR REPLACE INTO __crdt_data (
|
||||||
"table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
"user_id", "table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
'${migrator.userId}' AS "user_id",
|
||||||
'todos' AS "table_name",
|
'todos' AS "table_name",
|
||||||
column_name,
|
column_name,
|
||||||
OLD."id" AS "row_id",
|
OLD."id" AS "row_id",
|
||||||
"hlc_timestamp",
|
"hlc_timestamp",
|
||||||
raw_value
|
raw_value
|
||||||
FROM (
|
FROM (
|
||||||
SELECT $nextHlcFunction('${migrator.nodeId}') AS "hlc_timestamp"
|
SELECT $nextHlcFunction('${migrator.userId}', '${migrator.nodeId}') AS "hlc_timestamp"
|
||||||
), (
|
), (
|
||||||
SELECT
|
SELECT
|
||||||
'__crdt_is_deleted' AS "column_name",
|
'__crdt_is_deleted' AS "column_name",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue