feat: Split HLC storage into separate timestamp, counter, and node ID columns (#28)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcelomendoncasoares <6807059+marcelomendoncasoares@users.noreply.github.com>
This commit is contained in:
Copilot 2026-04-05 22:40:32 -03:00 • committed by GitHub
commit 027aec5ed0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 117 additions and 18 deletions

View file

@ -80,8 +80,14 @@ class CrdtNormalizedDataTable extends Table {
/// Unique identifier for the row in the table.
late final rowId = text()();
/// Hybrid Logical Clock timestamp of the data update.
late final hlcTimestamp = text().map(hlcConverter)();
/// Hybrid Logical Clock timestamp component (milliseconds since epoch).
late final hlcTimestamp = integer()();
/// Hybrid Logical Clock counter component.
late final hlcCounter = integer()();
/// Hybrid Logical Clock node ID component.
late final hlcNodeId = text()();
/// Raw value of the data.
late final rawValue = sqliteAny().nullable()();

View file

@ -146,10 +146,66 @@ ${columnInserts.join('\n UNION ALL\n')}
/// same format as the `hlcConverter.toSql` method.
String _getHlcJsonExprSql() {
return "printf('%015d', \"last_timestamp\") || '-' || "
"printf('%05x', \"counter\") || '-' || "
"printf('%04X', \"counter\") || '-' || "
"'${crdtDb.nodeId}'";
}
/// Returns SQL fragment that combines the split HLC columns (hlc_timestamp,
/// hlc_counter, hlc_node_id) back into a single text field.
String _getHlcCombineExprSql() {
return "printf('%015d', \"hlc_timestamp\") || '-' || "
"printf('%04X', \"hlc_counter\") || '-' || "
'"hlc_node_id"';
}
/// Returns SQL expression that converts a 4-character hex string to integer.
/// Used to parse the counter from the HLC text format.
String _hexToIntExpr(String hexString) {
// Convert each hex digit (0-9, A-F) to its decimal value and combine
return '''(
CASE SUBSTR($hexString, 1, 1)
WHEN '0' THEN 0 WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3
WHEN '4' THEN 4 WHEN '5' THEN 5 WHEN '6' THEN 6 WHEN '7' THEN 7
WHEN '8' THEN 8 WHEN '9' THEN 9
WHEN 'A' THEN 10 WHEN 'B' THEN 11 WHEN 'C' THEN 12 WHEN 'D' THEN 13
WHEN 'E' THEN 14 WHEN 'F' THEN 15
WHEN 'a' THEN 10 WHEN 'b' THEN 11 WHEN 'c' THEN 12 WHEN 'd' THEN 13
WHEN 'e' THEN 14 WHEN 'f' THEN 15
ELSE 0
END * 4096 +
CASE SUBSTR($hexString, 2, 1)
WHEN '0' THEN 0 WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3
WHEN '4' THEN 4 WHEN '5' THEN 5 WHEN '6' THEN 6 WHEN '7' THEN 7
WHEN '8' THEN 8 WHEN '9' THEN 9
WHEN 'A' THEN 10 WHEN 'B' THEN 11 WHEN 'C' THEN 12 WHEN 'D' THEN 13
WHEN 'E' THEN 14 WHEN 'F' THEN 15
WHEN 'a' THEN 10 WHEN 'b' THEN 11 WHEN 'c' THEN 12 WHEN 'd' THEN 13
WHEN 'e' THEN 14 WHEN 'f' THEN 15
ELSE 0
END * 256 +
CASE SUBSTR($hexString, 3, 1)
WHEN '0' THEN 0 WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3
WHEN '4' THEN 4 WHEN '5' THEN 5 WHEN '6' THEN 6 WHEN '7' THEN 7
WHEN '8' THEN 8 WHEN '9' THEN 9
WHEN 'A' THEN 10 WHEN 'B' THEN 11 WHEN 'C' THEN 12 WHEN 'D' THEN 13
WHEN 'E' THEN 14 WHEN 'F' THEN 15
WHEN 'a' THEN 10 WHEN 'b' THEN 11 WHEN 'c' THEN 12 WHEN 'd' THEN 13
WHEN 'e' THEN 14 WHEN 'f' THEN 15
ELSE 0
END * 16 +
CASE SUBSTR($hexString, 4, 1)
WHEN '0' THEN 0 WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3
WHEN '4' THEN 4 WHEN '5' THEN 5 WHEN '6' THEN 6 WHEN '7' THEN 7
WHEN '8' THEN 8 WHEN '9' THEN 9
WHEN 'A' THEN 10 WHEN 'B' THEN 11 WHEN 'C' THEN 12 WHEN 'D' THEN 13
WHEN 'E' THEN 14 WHEN 'F' THEN 15
WHEN 'a' THEN 10 WHEN 'b' THEN 11 WHEN 'c' THEN 12 WHEN 'd' THEN 13
WHEN 'e' THEN 14 WHEN 'f' THEN 15
ELSE 0
END
)''';
}
/// Returns SQL statements to create the CRDT data view and its INSTEAD OF
/// triggers, and the HLC canonical view with its INSTEAD OF trigger.
///
@ -180,38 +236,72 @@ ${columnInserts.join('\n UNION ALL\n')}
String get _normalizedTableName => crdtDb.crdtNormalizedDataTable.actualTableName;
/// CREATE VIEW for the CRDT data view: selects all columns from the
/// normalized table so reads against the view hit the real table.
/// CREATE VIEW for the CRDT data view: combines the split HLC columns from
/// the normalized table back into a single hlc_timestamp text field.
String _createCrdtDataViewStatement() =>
'''
CREATE VIEW IF NOT EXISTS "$crdtDataTableName" AS
SELECT ${crdtDataAllColumns.join(', ')} FROM "$_normalizedTableName";''';
SELECT
"user_id",
"table_name",
"column_name",
"row_id",
${_getHlcCombineExprSql()} AS "hlc_timestamp",
"raw_value"
FROM "$_normalizedTableName";''';
/// INSTEAD OF INSERT on the CRDT data view: insert into the normalized table,
/// and on conflict update only when the new row has a greater hlc_timestamp.
/// splitting the hlc_timestamp text into 3 columns, and on conflict update
/// only when the new row has a greater hlc_timestamp.
String _createCrdtDataInsteadOfInsertTriggerStatement() =>
'''
CREATE TRIGGER IF NOT EXISTS "${crdtDataTableName}__instead_of_insert"
INSTEAD OF INSERT ON "$crdtDataTableName"
BEGIN
INSERT INTO "$_normalizedTableName" (${crdtDataAllColumns.join(', ')})
VALUES (${crdtDataAllColumns.map((c) => 'NEW.$c').join(', ')})
ON CONFLICT (${crdtDataPrimaryKeyColumns.join(', ')}) DO UPDATE SET
INSERT INTO "$_normalizedTableName" (
"user_id", "table_name", "column_name", "row_id",
"hlc_timestamp", "hlc_counter", "hlc_node_id", "raw_value"
)
SELECT
NEW."user_id",
NEW."table_name",
NEW."column_name",
NEW."row_id",
CAST(SUBSTR(NEW."hlc_timestamp", 1, 15) AS INTEGER),
${_hexToIntExpr('SUBSTR(NEW."hlc_timestamp", 17, 4)')},
SUBSTR(NEW."hlc_timestamp", 22),
NEW."raw_value"
ON CONFLICT ("user_id", "table_name", "column_name", "row_id") DO UPDATE SET
"hlc_timestamp" = excluded."hlc_timestamp",
"hlc_counter" = excluded."hlc_counter",
"hlc_node_id" = excluded."hlc_node_id",
"raw_value" = excluded."raw_value"
WHERE "hlc_timestamp" < excluded."hlc_timestamp";
WHERE "hlc_timestamp" < excluded."hlc_timestamp"
OR ("hlc_timestamp" = excluded."hlc_timestamp" AND "hlc_counter" < excluded."hlc_counter");
END;''';
/// INSTEAD OF UPDATE on the CRDT data view: apply the new values to the
/// matching row in the normalized table (by primary key).
/// matching row in the normalized table (by primary key), splitting the
/// hlc_timestamp text into 3 columns.
String _createCrdtDataInsteadOfUpdateTriggerStatement() =>
'''
CREATE TRIGGER IF NOT EXISTS "${crdtDataTableName}__instead_of_update"
INSTEAD OF UPDATE ON "$crdtDataTableName"
BEGIN
UPDATE "$_normalizedTableName"
SET ${crdtDataAllColumns.map((c) => '$c = NEW.$c').join(', ')}
WHERE ${crdtDataPrimaryKeyColumns.map((c) => '$c = OLD.$c').join(' AND ')};
SET
"user_id" = NEW."user_id",
"table_name" = NEW."table_name",
"column_name" = NEW."column_name",
"row_id" = NEW."row_id",
"hlc_timestamp" = CAST(SUBSTR(NEW."hlc_timestamp", 1, 15) AS INTEGER),
"hlc_counter" = ${_hexToIntExpr('SUBSTR(NEW."hlc_timestamp", 17, 4)')},
"hlc_node_id" = SUBSTR(NEW."hlc_timestamp", 22),
"raw_value" = NEW."raw_value"
WHERE "user_id" = OLD."user_id"
AND "table_name" = OLD."table_name"
AND "column_name" = OLD."column_name"
AND "row_id" = OLD."row_id";
END;''';
/// INSTEAD OF DELETE on the CRDT data view: delete the matching row from the
@ -222,7 +312,10 @@ CREATE TRIGGER IF NOT EXISTS "${crdtDataTableName}__instead_of_delete"
INSTEAD OF DELETE ON "$crdtDataTableName"
BEGIN
DELETE FROM "$_normalizedTableName"
WHERE ${crdtDataPrimaryKeyColumns.map((c) => '$c = OLD.$c').join(' AND ')};
WHERE "user_id" = OLD."user_id"
AND "table_name" = OLD."table_name"
AND "column_name" = OLD."column_name"
AND "row_id" = OLD."row_id";
END;''';
/// CREATE VIEW for the HLC canonical view: an empty view (WHERE FALSE) used

View file

@ -57,7 +57,7 @@ BEGIN
"column_name",
NEW."id" AS "row_id",
(
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%05x', "counter") || '-' || '${migrator.nodeId}')
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%04X', "counter") || '-' || '${migrator.nodeId}')
FROM "${migrator.crdtDb.sqlBuilder.hlcStateTableName}"
WHERE "user_id" = '${migrator.userId}'
) AS "hlc_timestamp",
@ -124,7 +124,7 @@ BEGIN
"column_name",
OLD."id" AS "row_id",
(
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%05x', "counter") || '-' || '${migrator.nodeId}')
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%04X', "counter") || '-' || '${migrator.nodeId}')
FROM "${migrator.crdtDb.sqlBuilder.hlcStateTableName}"
WHERE "user_id" = '${migrator.userId}'
) AS "hlc_timestamp",
@ -194,7 +194,7 @@ BEGIN
"column_name",
OLD."id" AS "row_id",
(
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%05x', "counter") || '-' || '${migrator.nodeId}')
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%04X', "counter") || '-' || '${migrator.nodeId}')
FROM "${migrator.crdtDb.sqlBuilder.hlcStateTableName}"
WHERE "user_id" = '${migrator.userId}'
) AS "hlc_timestamp",