chore: Move the drift_offline_first to legacy in favor of Serverpod implementation (#32)

This commit is contained in:
Marcelo Mendonça Soares 2026-04-05 23:20:21 -03:00 • committed by Marcelo Soares
commit cbc472523b
No known key found for this signature in database
GPG key ID: CB2F09CE66E99EAD
10 changed files with 13575 additions and 284 deletions

View file

@ -0,0 +1,13 @@
import 'package:drift/drift.dart';
import 'package:drift_offline_sync/drift_offline_sync.dart';
/// Extensions for [GeneratedDatabase] to get the CRDT context for testing.
extension CrdtContextExtensions on GeneratedDatabase {
(OfflineSyncCrdt crdt, CrdtDatabase crdtDb, String nodeId) get crdtContext {
final migrator = createMigrator();
if (migrator is! OfflineSyncMigrator) {
throw StateError('Migrator is not an OfflineSyncMigrator');
}
return (migrator.crdt, migrator.crdtDb, migrator.nodeId);
}
}

View file

@ -0,0 +1,114 @@
import 'package:drift/drift.dart';
import 'package:drift_offline_sync/drift_offline_sync.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
import 'executor.dart';
import 'tables.dart';
import 'user.dart';
import 'views.dart';
part 'database.g.dart';
/// The test database for the SQLite3 database.
///
/// This is a memory-based SQLite3 database that is used for testing. It follows
/// the same pattern as the `test_descriptor` package to ensure one database is
/// created for each test. The database is closed once test ends.
TodoDb get database {
if (_database != null) return _database!;
final database = TodoDb(testExecutor);
_database = database;
addTearDown(() async {
final database = _database!;
_database = null;
await database.close();
});
return database;
}
TodoDb? _database;
@DriftDatabase(
tables: [
TodosTable,
Categories,
Users,
SharedTodos,
TableWithoutPK,
PureDefaults,
WithCustomType,
TableWithEveryColumnType,
Department,
Product,
Listing,
Store,
],
views: [
CategoryTodoCountView,
TodoWithCategoryView,
],
daos: [SomeDao],
queries: {
'allTodosWithCategory':
'SELECT t.*, c.id as catId, c."desc" as catDesc '
'FROM todos t INNER JOIN categories c ON c.id = t.category',
'deleteTodoById': 'DELETE FROM todos WHERE id = ?',
'withIn': 'SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1',
'search': 'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
'findCustom': 'SELECT custom FROM table_without_p_k WHERE some_float < 10',
},
)
class TodoDb extends _$TodoDb {
TodoDb(super.e) {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
}
@override
int schemaVersion = 1;
// This is what needs to be added to the user database class to make it CRDT aware.
@override
OfflineSyncMigrator createMigrator() => OfflineSyncMigrator(
this,
userId: testUserId,
nodeId: testNodeId,
synchronizedTables: [
todosTable,
categories,
users,
sharedTodos,
// tableWithoutPK,
pureDefaults,
// withCustomType,
tableWithEveryColumnType,
department,
// product,
],
excludeTables: [
listing,
product,
store,
tableWithoutPK,
withCustomType,
],
);
}
@DriftAccessor(
tables: [Users, SharedTodos, TodosTable],
views: [TodoWithCategoryView],
queries: {
'todosForUser':
'SELECT t.* FROM todos t '
'INNER JOIN shared_todos st ON st.todo = t.id '
'INNER JOIN users u ON u.id = st.user '
'WHERE u.id = :user',
},
)
class SomeDao extends DatabaseAccessor<TodoDb> with _$SomeDaoMixin {
SomeDao(super.attachedDatabase);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
import 'package:drift/native.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart';
/// Sets the test executor for the SQLite3 database.
///
/// This is a file-based SQLite3 executor that is used for testing. It follows
/// the same pattern as the `test_descriptor` package to ensure one executor is
/// created for each test. The executor is closed once test ends.
///
/// After setting the test executor, the database can be accessed using the
/// [testExecutor] getter.
void setTestFileExecutor([String fileName = 'test.db']) {
final databaseFile = file(fileName);
final sqliteDatabase = sqlite3.open(databaseFile.io.path);
final executor = NativeDatabase.opened(sqliteDatabase);
_setTestExecutor(executor);
}
/// The test executor for the SQLite3 database.
///
/// This is a memory-based SQLite3 executor that is used for testing. It follows
/// the same pattern as the `test_descriptor` package to ensure one executor is
/// created for each test. The executor is closed once test ends.
NativeDatabase get testExecutor {
if (_testExecutor != null) return _testExecutor!;
final executor = NativeDatabase.memory();
_setTestExecutor(executor);
return executor;
}
NativeDatabase? _testExecutor;
void _setTestExecutor(NativeDatabase executor) {
_testExecutor = executor;
addTearDown(() async {
final executor = _testExecutor;
if (executor == null) return;
_testExecutor = null;
await executor.close();
});
}

View file

@ -0,0 +1,82 @@
import 'package:drift/drift.dart';
import 'package:drift_offline_sync/drift_offline_sync.dart';
import 'tables.dart';
import 'user.dart';
part 'migration.g.dart';
@DriftDatabase(tables: [TodosTable])
class FirstDb extends _$FirstDb {
FirstDb(super.e) {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
}
bool created = false;
bool didUpgrade = false;
bool onBeforeOpen = false;
@override
int schemaVersion = 1;
@override
OfflineSyncMigrator createMigrator() => OfflineSyncMigrator(
this,
userId: testUserId,
nodeId: testNodeId,
);
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) async {
created = true;
await m.createAll();
},
onUpgrade: (m, from, to) async {
if (from == 1 && to == 2) {
didUpgrade = true;
}
},
beforeOpen: (m) async {
onBeforeOpen = true;
},
);
}
@DriftDatabase(tables: [Users])
class SecondDb extends _$SecondDb {
SecondDb(super.e) {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
}
bool created = false;
bool didUpgrade = false;
bool onBeforeOpen = false;
@override
int schemaVersion = 2;
@override
OfflineSyncMigrator createMigrator() => OfflineSyncMigrator(
this,
userId: testUserId,
nodeId: testNodeId,
);
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) async {
created = true;
await m.createAll();
},
onUpgrade: (m, from, to) async {
if (from == 1 && to == 2) {
await m.createTable(users);
didUpgrade = true;
}
},
beforeOpen: (m) async {
onBeforeOpen = true;
},
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,140 @@
// TODO: Add a note with Drift LICENSE in this file, since it is mostly a copy
// of the Drift example tables.
import 'package:drift/drift.dart';
import 'package:uuid/uuid.dart';
import 'types.dart';
export 'types.dart';
extension type RowId._(int id) {
const RowId(this.id);
}
mixin AutoIncrement on Table {
late final id = integer().autoIncrement().map(
TypeConverter.extensionType<RowId, int>(),
)();
}
@DataClassName('TodoEntry')
class TodosTable extends Table with AutoIncrement {
@override
String get tableName => 'todos';
late final title = text().withLength(min: 4, max: 16).nullable()();
late final content = text()();
@JsonKey('target_date')
late final targetDate = dateTime().nullable().unique()();
@ReferenceName('todos')
late final category = integer()
.references(Categories, #id, initiallyDeferred: true)
.map(TypeConverter.extensionType<RowId, int>())
.nullable()();
late final status = textEnum<TodoStatus>().nullable()();
@override
List<Set<Column>>? get uniqueKeys => [
{title, category},
{title, targetDate},
];
}
enum TodoStatus { open, workInProgress, done }
class Users extends Table with AutoIncrement {
late final name = text().withLength(min: 6, max: 32).unique()();
late final isAwesome = boolean().withDefault(const Constant(true))();
late final profilePicture = blob()();
late final DateTimeColumn creationTime = dateTime()
.check(creationTime.isBiggerThan(Constant(DateTime.utc(1950))))
.withDefault(currentDateAndTime)();
}
@DataClassName('Category')
class Categories extends Table with AutoIncrement {
late final description = text().named('desc').customConstraint('NOT NULL UNIQUE')();
late final priority = intEnum<CategoryPriority>().withDefault(const Constant(0))();
late final descriptionInUpperCase = text().generatedAs(description.upper())();
}
enum CategoryPriority { low, medium, high }
class SharedTodos extends Table {
late final todo = integer()();
late final user = integer()();
@override
Set<Column> get primaryKey => {todo, user};
@override
List<String> get customConstraints => [
'FOREIGN KEY (todo) REFERENCES todos(id)',
'FOREIGN KEY (user) REFERENCES users(id)',
];
}
@UseRowClass(CustomRowClass, constructor: 'map', generateInsertable: true)
class TableWithoutPK extends Table {
IntColumn get notReallyAnId => integer()();
RealColumn get someFloat => real()();
Int64Column get webSafeInt => int64().nullable()();
TextColumn get custom =>
text().map(const CustomConverter()).clientDefault(const Uuid().v4)();
}
class TableWithEveryColumnType extends Table with AutoIncrement {
BoolColumn get aBool => boolean().nullable()();
DateTimeColumn get aDateTime => dateTime().nullable()();
TextColumn get aText => text().nullable()();
IntColumn get anInt => integer().nullable()();
Int64Column get anInt64 => int64().nullable()();
RealColumn get aReal => real().nullable()();
BlobColumn get aBlob => blob().nullable()();
IntColumn get anIntEnum => intEnum<TodoStatus>().nullable()();
TextColumn get aTextWithConverter =>
text().named('insert').map(const CustomJsonConverter()).nullable()();
Column<UuidValue> get aUuid => customType(uuidType).nullable()();
}
class Department extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().nullable()();
}
class Product extends Table {
TextColumn get sku => text()();
TextColumn get name => text().nullable()();
IntColumn get department => integer().references(Department, #id).nullable()();
}
class Listing extends Table {
IntColumn get id => integer().autoIncrement()();
@ReferenceName('listings')
TextColumn get product => text().references(Product, #sku)();
@ReferenceName('listings')
IntColumn get store => integer().references(Store, #id).nullable()();
RealColumn get price => real().nullable()();
}
class Store extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().nullable()();
}
class PureDefaults extends Table {
// name after keyword to ensure it's escaped properly
TextColumn get txt =>
text().named('insert').map(const CustomJsonConverter()).nullable()();
@override
Set<Column> get primaryKey => {txt};
}
class WithCustomType extends Table {
Column<UuidValue> get id => customType(uuidType)();
}

View file

@ -0,0 +1,124 @@
import 'package:drift/drift.dart';
import 'package:uuid/uuid.dart';
export 'package:uuid/uuid.dart' show UuidValue;
class CustomRowClass {
CustomRowClass._(
this.notReallyAnId,
this.anotherName,
this.webSafeInt,
this.custom,
this.notFromDb,
);
factory CustomRowClass.map(
int notReallyAnId,
double someFloat, {
required MyCustomObject custom,
BigInt? webSafeInt,
String? notFromDb,
}) {
return CustomRowClass._(notReallyAnId, someFloat, webSafeInt, custom, notFromDb);
}
final int notReallyAnId;
final double anotherName;
final BigInt? webSafeInt;
final MyCustomObject custom;
final String? notFromDb;
double get someFloat => anotherName;
}
// example object used for custom mapping
class MyCustomObject {
MyCustomObject(this.data);
final String data;
@override
int get hashCode => data.hashCode;
@override
bool operator ==(Object other) {
return other is MyCustomObject && other.data == data;
}
}
class CustomConverter extends TypeConverter<MyCustomObject, String> {
const CustomConverter();
@override
MyCustomObject fromSql(String fromDb) {
return MyCustomObject(fromDb);
}
@override
String toSql(MyCustomObject value) {
return value.data;
}
}
class CustomJsonConverter extends CustomConverter
with JsonTypeConverter2<MyCustomObject, String, Map> {
const CustomJsonConverter();
@override
MyCustomObject fromJson(Map json) {
return MyCustomObject(json['data'] as String);
}
@override
Map toJson(MyCustomObject value) {
return {'data': value.data};
}
}
class NativeUuidType implements CustomSqlType<UuidValue> {
const NativeUuidType();
@override
String mapToSqlLiteral(UuidValue dartValue) {
return "'$dartValue'";
}
@override
Object mapToSqlParameter(UuidValue dartValue) {
return dartValue;
}
@override
UuidValue read(Object fromSql) {
return fromSql as UuidValue;
}
@override
String sqlTypeName(GenerationContext context) => 'uuid';
}
class _UuidAsTextType implements CustomSqlType<UuidValue> {
const _UuidAsTextType();
@override
String mapToSqlLiteral(UuidValue dartValue) {
return "'$dartValue'";
}
@override
Object mapToSqlParameter(UuidValue dartValue) {
return dartValue.toString();
}
@override
UuidValue read(Object fromSql) {
return UuidValue.fromString(fromSql as String);
}
@override
String sqlTypeName(GenerationContext context) => 'text';
}
const uuidType = DialectAwareSqlType<UuidValue>.via(
fallback: _UuidAsTextType(),
overrides: {SqlDialect.postgres: NativeUuidType()},
);

View file

@ -0,0 +1,23 @@
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
// NOTE: Generated once per test so that the tests are deterministic, but also
// isolated between each other.
String get testNodeId {
_testNodeId ??= const Uuid().v7();
addTearDown(() {
_testNodeId = null;
});
return _testNodeId!;
}
String get testUserId {
_testUserId ??= const Uuid().v7();
addTearDown(() {
_testUserId = null;
});
return _testUserId!;
}
String? _testNodeId;
String? _testUserId;

View file

@ -0,0 +1,34 @@
import 'package:drift/drift.dart';
import 'tables.dart';
abstract class CategoryTodoCountView extends View {
TodosTable get todos;
Categories get categories;
Expression<int> get categoryId => categories.id;
Expression<String> get description => categories.description + const Variable('!');
Expression<int> get itemCount => todos.id.count();
@override
Query as() =>
select(
[categoryId, description, itemCount],
).from(categories).join([
innerJoin(todos, todos.category.equalsExp(categories.id)),
])
..groupBy([categories.id]);
}
abstract class TodoWithCategoryView extends View {
TodosTable get todos;
Categories get categories;
@override
Query as() =>
select(
[todos.title, categories.description],
).from(todos).join([
innerJoin(categories, categories.id.equalsExp(todos.category)),
]);
}