更新数据库

This commit is contained in:
Happi (哈比)
2026-03-06 16:54:51 +08:00
parent bf9e099747
commit 113ecb633a
17 changed files with 604 additions and 117 deletions

View File

@@ -11,14 +11,22 @@ import '../../data/local/drift/app_database.dart';
/// 用法:
/// ```dart
/// // 登录后开库
/// await ref.read(storageSdkProvider).openDatabase(user.id);
/// await ref.read(storageSdkLifecycleProvider).openDatabase(user.id);
///
/// // CRUD 示例
/// final db = ref.read(storageSdkProvider);
/// await db.insertOrReplace(appDb.users, companion);
/// await db.insertOrReplace<UsersCompanion>(companion);
/// final users = await db.selectAll<User>();
/// ```
final storageSdkProvider = Provider<StorageSdkApi>((ref) {
return StorageSdkApi(
databaseFactory: (executor) => AppDatabase(executor),
tableRegistry: (db) => AppDatabase.getTableRegistry(db),
);
});
/// 生命周期管理,仅供登录/登出使用。
final storageSdkLifecycleProvider = Provider<StorageSdkLifecycle>((ref) {
return ref.read(storageSdkProvider) as StorageSdkLifecycle;
});

View File

@@ -64,6 +64,7 @@ enum AppRouteName {
chatDetail('/chat/detail'),
// 路径参数形式:导航用 AppRouteName.chatDetailByIdPath(id),不直接用 .path
chatDetailById('/chat/:id'),
chatDBTest('/chat/dbTest'),
// ── Settings 子路由 ───────────────────────────────────────────────────────
settingsTheme('/settings/theme'),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:im_app/features/chat/view/chat_db_test_page.dart';
import '../../features/app_tab/view/app_tab.dart';
import '../../features/chat/view/chat_detail_page.dart';
@@ -115,6 +116,13 @@ final routerProvider = Provider<GoRouter>((ref) {
// parentNavigatorKey: _rootKey 确保路由覆盖 ShellTabBar 消失
//
// extra 传参:接收 ({String conversationId, String title})
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDBTest.path,
builder: (context, state) {
return const ChatDbTestPage();
},
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDetail.path,

View File

@@ -34,13 +34,13 @@ String? authGuard(AuthNotifier authNotifier, GoRouterState state) {
case AppRouteName.login:
// 已登录还在登录页 → 跳聊天页
return isLoggedIn ? AppRouteName.chat.path : null;
case AppRouteName.chat:
case AppRouteName.chatDetail:
case AppRouteName.chatDetailById:
case AppRouteName.contact:
case AppRouteName.settings:
case AppRouteName.settingsTheme:
case AppRouteName.chatDBTest:
// 受保护路由 → 未登录跳登录页
return isLoggedIn ? null : AppRouteName.login.path;
}

View File

@@ -1,10 +1,23 @@
import 'package:drift/drift.dart';
import 'package:im_app/data/local/drift/tables/users.dart';
import 'package:im_app/data/local/drift/tables/test_tables.dart';
part 'app_database.g.dart';
@DriftDatabase(tables: [Users])
@DriftDatabase(tables: [Users,TestTables]) //update mapping here
class AppDatabase extends _$AppDatabase {
static Map<Type, TableInfo> getTableRegistry(GeneratedDatabase database) {
if (database is! AppDatabase) {
return {
};
}
return {
User: database.users,
TestTable: database.testTables,
};
}
AppDatabase(super.e);
@override
@@ -37,4 +50,6 @@ class AppDatabase extends _$AppDatabase {
},
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:drift/drift.dart';
@DataClassName('TestTable')
class TestTables extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get uid => integer().nullable()();
TextColumn get uuid => text().nullable()();
IntColumn get lastOnline => integer().nullable()();
TextColumn get profilePic => text().nullable()();
TextColumn get profilePicGaussian => text().withDefault(const Constant(''))();
TextColumn get nickname => text().nullable()();
TextColumn get depositName => text().nullable()();
IntColumn get hasSetDepositName => integer().withDefault(const Constant(0))();
TextColumn get contact => text().nullable()();
TextColumn get countryCode => text().nullable()();
TextColumn get username => text().nullable()();
IntColumn get role => integer().nullable()();
IntColumn get relationship => integer().nullable()();
IntColumn get friendStatus => integer().nullable()();
TextColumn get bio => text().nullable()();
TextColumn get userAlias => text().nullable()();
IntColumn get requestAt => integer().nullable()();
IntColumn get deletedAt => integer().nullable()();
TextColumn get email => text().nullable()();
TextColumn get recoveryEmail => text().nullable()();
TextColumn get remark => text().nullable()();
TextColumn get source => text().nullable()();
IntColumn get addIndex => integer().nullable()();
IntColumn get incomingSoundId => integer().withDefault(const Constant(0))();
IntColumn get outgoingSoundId => integer().withDefault(const Constant(0))();
IntColumn get notificationSoundId => integer().withDefault(const Constant(0))();
IntColumn get sendMessageSoundId => integer().withDefault(const Constant(0))();
IntColumn get groupNotificationSoundId => integer().withDefault(const Constant(0))();
TextColumn get groupTags => text().withDefault(const Constant('[]'))();
TextColumn get friendTags => text().withDefault(const Constant('[]'))();
TextColumn get publicKey => text().nullable()();
IntColumn get configBits => integer().withDefault(const Constant(0))();
TextColumn get hint => text().nullable()();
@override
String get tableName => 'test_tables';
}

View File

@@ -0,0 +1,119 @@
import 'dart:math';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:im_app/app/di/db_provider.dart';
import 'package:im_app/data/local/drift/app_database.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'chat_db_test_view_model.g.dart';
class TestResult {
final String title;
final String subtitle;
final String duration;
TestResult({
required this.title,
required this.subtitle,
required this.duration,
});
}
class ChatDbTestState {
final bool testStarted;
final List<TestResult> testResults;
final String currentState;
const ChatDbTestState({
this.testStarted = false,
this.testResults = const [],
this.currentState = '',
});
ChatDbTestState copyWith({
bool? testStarted,
List<TestResult>? testResults,
String? currentState,
}) => ChatDbTestState(
testStarted: testStarted ?? this.testStarted,
testResults: testResults ?? this.testResults,
currentState: currentState ?? this.currentState,
);
}
@riverpod
class ChatDbTestViewModel extends _$ChatDbTestViewModel {
@override
ChatDbTestState build() {
// 这里就是 onInit
final List<TestResult> testResults = List.generate(
1000,
(i) => TestResult(
title: '用户 ${Random().nextInt(9999)}',
subtitle: 'uid: ${Random().nextInt(999999)}',
duration: '${Random().nextInt(500)}ms',
),
);
return ChatDbTestState(testResults: testResults);
}
// ── 导航Demo 按钮,正式开发后随 UI 一并替换) ──────────────────────────
/// 开始测试
void startDBTest(BuildContext context) {
state = state.copyWith(testStarted: true, currentState: '开始测试');
_testDBInsert();
}
/// 结束测试
void stopDBTest(BuildContext context) {
state = state.copyWith(testStarted: false, currentState: '结束测试');
}
Future<void> _testDBInsert() async {
final db = ref.read(storageSdkProvider);
const count = 10000;
const chunkSize = 50;
final stopwatch = Stopwatch()..start();
debugPrint('开始测试: $count 条,每批 $chunkSize');
int completed = 0;
for (var i = 0; i < count; i += chunkSize) {
final chunk = List.generate(
chunkSize.clamp(0, count - i),
(j) => UsersCompanion.insert(
uid: Value(i + j),
nickname: Value('User ${i + j}'),
),
);
await db.batchInsertOrReplace<User>(chunk);
completed += chunk.length;
// 让出主线程
await Future.delayed(Duration.zero);
debugPrint('已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)');
// 更新 UI 状态
if (ref.mounted) {
state = state.copyWith(
currentState: '已插入 $completed / $count',
);
}
}
debugPrint('全部完成: ${stopwatch.elapsedMilliseconds}ms');
if (ref.mounted) {
state = state.copyWith(
testStarted: false,
currentState: '完成!共 $count 条,耗时 ${stopwatch.elapsedMilliseconds}ms',
);
}
}
}

View File

@@ -55,6 +55,11 @@ class ChatViewModel extends _$ChatViewModel {
context.go(AppRouteName.settings.path);
}
/// 测试数据库性能
void goToDatabaseTest(BuildContext context) {
context.push(AppRouteName.chatDBTest.path);
}
// ── 业务 ─────────────────────────────────────────────────────────────────
/// 退出登录

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:im_app/features/chat/presentation/chat_db_test_view_model.dart';
import '../../../core/ui/components/app_button.dart';
import '../presentation/chat_view_model.dart';
/// 聊天页Demo 按钮)
///
/// 包含五个演示按钮,覆盖 go_router 的常见导航场景:
/// - 「切换 Tab」 — go替换历史不可返回
/// - 「有参 pushextra」 — push + extraDart Record可返回
/// - 「有参 push路径参数」— push + URL 内嵌 id可返回
/// - 「无参 push」 — push可返回
/// - 「退出登录」 — 守卫自动重定向到 /login
///
/// 所有操作通过 [ChatViewModel] 处理View 不直接调用路由。
/// 正式开发后替换为会话列表,按钮相关代码一并清除。
class ChatDbTestPage extends ConsumerWidget {
const ChatDbTestPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(chatDbTestViewModelProvider.notifier);
final state = ref.watch(chatDbTestViewModelProvider);
return Scaffold(
appBar: AppBar(title: const Text('测试数据库'), ),
body: Column(
mainAxisSize: MainAxisSize.max,
spacing: 16,
children: [
SizedBox(height: 4),
Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppButton.inverse(
label: state.testStarted ? '结束' : '开始',
onPressed: () => state.testStarted ? vm.stopDBTest(context) : vm.startDBTest(context),
),
SizedBox(width: 8),
Expanded(
child: Text(
state.currentState,
textAlign: TextAlign.end,
),
)
],
)
),
Expanded(
child: ListView.builder(
itemCount: state.testResults.length,
itemBuilder: (context, index) {
final result = state.testResults[index];
return ListTile(
titleAlignment: ListTileTitleAlignment.center,
title: Text(result.title),
subtitle: Text(result.subtitle),
// trailing: Text(result.duration),
);
},
),
)
],
),
);
}
}

View File

@@ -54,6 +54,10 @@ class ChatPage extends ConsumerWidget {
label: '无参 go',
onPressed: () => vm.goToSettings(context),
),
AppButton.inverse(
label: '测试数据库性能',
onPressed: () => vm.goToDatabaseTest(context),
),
AppButton.secondary(
label: '退出登录',
fullWidth: false,

View File

View File

@@ -45,7 +45,7 @@ class DatabaseDataSource {
}
final file = File('${dbDir.path}/$uid.sqlite');
_db = _databaseFactory(NativeDatabase(file));
_db = _databaseFactory(NativeDatabase.createInBackground(file));
return _db!;
}

View File

@@ -4,11 +4,14 @@ import '../wiring/storage_sdk_wiring.dart';
/// 本地数据库的统一公开接口。
///
/// 通过 [StorageSdkApi] 工厂方法获取实例,传入 App 侧的数据库工厂即可:
/// 通过 [StorageSdkApi] 工厂方法获取实例,传入 App 侧的数据库工厂和表注册表即可:
///
/// ```dart
/// final api = StorageSdkApi(
/// databaseFactory: (executor) => AppDatabase(executor),
/// tableRegistry: (db) => {
/// User: (db as AppDatabase).users,
/// },
/// );
/// ```
///
@@ -19,98 +22,124 @@ import '../wiring/storage_sdk_wiring.dart';
///
/// 内部接口,仅供 App 层生命周期管理使用
abstract class StorageSdkLifecycle {
/// 打开指定用户的数据库(按 uid 隔离文件)。
Future<void> openDatabase(int uid);
/// 关闭当前数据库连接。
Future<void> closeDatabase();
/// 是否已开库。
bool get isDatabaseOpen;
}
///
///
/// ## CRUD
/// 所有 CRUD 方法均为泛型,接受 Drift 的 [TableInfo],与具体业务表解耦。
/// App 层传入自己定义的 Table 即可复用全部操作
/// 所有 CRUD 方法均通过泛型数据类型查找对应表,与具体业务表解耦。
/// App 层通过 [tableRegistry] 注入表映射,无需在调用时传入 [TableInfo]
abstract class StorageSdkApi {
/// 创建 SDK 实例。
///
/// [databaseFactory] 由 App 层提供:接受 [QueryExecutor]
/// 返回自定义的 [GeneratedDatabase] 子类(含表定义和迁移策略)。
///
/// [tableRegistry] 由 App 层提供:将数据类映射到对应的表信息,
/// storage_sdk 不感知具体表结构。
///
/// 示例:
/// ```dart
/// final api = StorageSdkApi(
/// databaseFactory: (executor) => AppDatabase(executor),
/// tableRegistry: (db) => {
/// User: (db as AppDatabase).users,
/// },
/// );
/// ```
factory StorageSdkApi({
required GeneratedDatabase Function(QueryExecutor) databaseFactory,
required Map<Type, TableInfo> Function(GeneratedDatabase) tableRegistry,
}) =>
StorageSdkWiring.build(databaseFactory: databaseFactory);
StorageSdkWiring.build(
databaseFactory: databaseFactory,
tableRegistry: tableRegistry,
);
// ── 插入 ─────────────────────────────────────────────────────────────────
/// 插入或替换(主键冲突时覆盖)。
Future<void> insertOrReplace<T extends Table, D>(
TableInfo<T, D> table,
Insertable<D> companion,
);
///
/// 示例:
/// ```dart
/// await sdk.insertOrReplace<UsersCompanion>(
/// UsersCompanion.insert(nickname: Value('Edmund')),
/// );
/// ```
Future<void> insertOrReplace<D>(Insertable<D> companion);
/// 插入或忽略(主键冲突时跳过)。
Future<void> insert<T extends Table, D>(
TableInfo<T, D> table,
Insertable<D> companion,
);
Future<void> insert<D>(Insertable<D> companion);
/// 批量插入或替换。
Future<void> batchInsertOrReplace<T extends Table, D>(
TableInfo<T, D> table,
List<Insertable<D>> companions,
);
Future<void> batchInsertOrReplace<D>(List<Insertable<D>> companions);
// ── 更新 ─────────────────────────────────────────────────────────────────
/// 按条件更新。
Future<void> updateWhere<T extends Table, D>(
TableInfo<T, D> table,
Insertable<D> companion,
Expression<bool> Function(T) filter,
);
///
/// 示例:
/// ```dart
/// await sdk.updateWhere<User, $UsersTable>(
/// UsersCompanion(nickname: Value('NewName')),
/// (t) => t.uid.equals(123),
/// );
/// ```
Future<void> updateWhere<D, T extends Table>(
Insertable<D> companion,
Expression<bool> Function(T) filter,
);
// ── 删除 ─────────────────────────────────────────────────────────────────
/// 按条件删除。
Future<void> deleteWhere<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
);
Future<void> deleteWhere<D, T extends Table>(
Expression<bool> Function(T) filter,
);
/// 清空整张表。
Future<void> deleteAll<T extends Table, D>(TableInfo<T, D> table);
Future<void> deleteAll<D>();
// ── 查询 ─────────────────────────────────────────────────────────────────
/// 查询全部记录。
Future<List<D>> selectAll<T extends Table, D>(TableInfo<T, D> table);
///
/// 示例:
/// ```dart
/// final users = await sdk.selectAll<User>();
/// ```
Future<List<D>> selectAll<D>();
/// 按条件查询。
Future<List<D>> selectWhere<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
);
Future<List<D>> selectWhere<D, T extends Table>(
Expression<bool> Function(T) filter,
);
/// 查询第一条匹配记录。
Future<D?> selectFirst<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
);
Future<D?> selectFirst<D, T extends Table>(
Expression<bool> Function(T) filter,
);
// ── 监听 ─────────────────────────────────────────────────────────────────
/// 监听全部记录(实时流)。
Stream<List<D>> watchAll<T extends Table, D>(TableInfo<T, D> table);
Stream<List<D>> watchAll<D>();
/// 按条件监听(实时流)。
Stream<List<D>> watchWhere<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
);
Stream<List<D>> watchWhere<D, T extends Table>(
Expression<bool> Function(T) filter,
);
/// 监听第一条匹配记录(实时流)。
Stream<D?> watchFirst<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
);
Stream<D?> watchFirst<D, T extends Table>(
Expression<bool> Function(T) filter,
);
// ── 原始 SQL ─────────────────────────────────────────────────────────────
@@ -123,8 +152,7 @@ abstract class StorageSdkApi {
// ── 统计 ─────────────────────────────────────────────────────────────────
/// 统计记录数。
Future<int> count<T extends Table, D>(
TableInfo<T, D> table, {
Future<int> count<D, T extends Table>({
Expression<bool> Function(T)? filter,
});
}
}

View File

@@ -6,14 +6,29 @@ import 'storage_sdk_core.dart';
/// [StorageSdkApi] 的实现,委托给 [StorageSdkCore]。
class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
final StorageSdkCore _core;
final Map<Type, TableInfo> Function(GeneratedDatabase) _tableRegistry;
StorageSdkApiImpl({required StorageSdkCore core}) : _core = core;
StorageSdkApiImpl({
required StorageSdkCore core,
required Map<Type, TableInfo> Function(GeneratedDatabase) tableRegistry,
}) : _core = core,
_tableRegistry = tableRegistry;
// ── 表查找 ───────────────────────────────────────────────────────────────
/// 根据泛型数据类型从注册表中查找对应的 [TableInfo]。
TableInfo<T, D> _tableFor<T extends Table, D>() {
final db = _core.dataSource.current;
if (db == null) throw StateError('数据库未开启,请先调用 openDatabase()');
final table = _tableRegistry(db)[D];
if (table == null) throw StateError('未注册类型 $D 对应的表,请检查 tableRegistry');
return table as TableInfo<T, D>;
}
// ── 生命周期 ─────────────────────────────────────────────────────────────
@override
Future<void> openDatabase(int uid) =>
_core.dataSource.openDatabase(uid);
Future<void> openDatabase(int uid) => _core.dataSource.openDatabase(uid);
@override
Future<void> closeDatabase() => _core.dataSource.closeDatabase();
@@ -24,88 +39,73 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
// ── 插入 ─────────────────────────────────────────────────────────────────
@override
Future<void> insertOrReplace<T extends Table, D>(
TableInfo<T, D> table,
Insertable<D> companion,
) =>
_core.repo.insertOrReplace(table, companion);
Future<void> insertOrReplace<D>(Insertable<D> companion) =>
_core.repo.insertOrReplace(_tableFor<Table, D>(), companion);
@override
Future<void> insert<T extends Table, D>(
TableInfo<T, D> table,
Insertable<D> companion,
) =>
_core.repo.insert(table, companion);
Future<void> insert<D>(Insertable<D> companion) =>
_core.repo.insert(_tableFor<Table, D>(), companion);
@override
Future<void> batchInsertOrReplace<T extends Table, D>(
TableInfo<T, D> table,
List<Insertable<D>> companions,
) =>
_core.repo.batchInsertOrReplace(table, companions);
Future<void> batchInsertOrReplace<D>(List<Insertable<D>> companions) =>
_core.repo.batchInsertOrReplace(_tableFor<Table, D>(), companions);
// ── 更新 ─────────────────────────────────────────────────────────────────
@override
Future<void> updateWhere<T extends Table, D>(
TableInfo<T, D> table,
Insertable<D> companion,
Expression<bool> Function(T) filter,
) =>
_core.repo.updateWhere(table, companion, filter);
Future<void> updateWhere<D, T extends Table>(
Insertable<D> companion,
Expression<bool> Function(T) filter,
) =>
_core.repo.updateWhere(_tableFor<T, D>(), companion, filter);
// ── 删除 ─────────────────────────────────────────────────────────────────
@override
Future<void> deleteWhere<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
) =>
_core.repo.deleteWhere(table, filter);
Future<void> deleteWhere<D, T extends Table>(
Expression<bool> Function(T) filter,
) =>
_core.repo.deleteWhere(_tableFor<T, D>(), filter);
@override
Future<void> deleteAll<T extends Table, D>(TableInfo<T, D> table) =>
_core.repo.deleteAll(table);
Future<void> deleteAll<D>() =>
_core.repo.deleteAll(_tableFor<Table, D>());
// ── 查询 ─────────────────────────────────────────────────────────────────
@override
Future<List<D>> selectAll<T extends Table, D>(TableInfo<T, D> table) =>
_core.repo.selectAll(table);
Future<List<D>> selectAll<D>() =>
_core.repo.selectAll(_tableFor<Table, D>());
@override
Future<List<D>> selectWhere<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
) =>
_core.repo.selectWhere(table, filter);
Future<List<D>> selectWhere<D, T extends Table>(
Expression<bool> Function(T) filter,
) =>
_core.repo.selectWhere(_tableFor<T, D>(), filter);
@override
Future<D?> selectFirst<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
) =>
_core.repo.selectFirst(table, filter);
Future<D?> selectFirst<D, T extends Table>(
Expression<bool> Function(T) filter,
) =>
_core.repo.selectFirst(_tableFor<T, D>(), filter);
// ── 监听 ─────────────────────────────────────────────────────────────────
@override
Stream<List<D>> watchAll<T extends Table, D>(TableInfo<T, D> table) =>
_core.repo.watchAll(table);
Stream<List<D>> watchAll<D>() =>
_core.repo.watchAll(_tableFor<Table, D>());
@override
Stream<List<D>> watchWhere<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
) =>
_core.repo.watchWhere(table, filter);
Stream<List<D>> watchWhere<D, T extends Table>(
Expression<bool> Function(T) filter,
) =>
_core.repo.watchWhere(_tableFor<T, D>(), filter);
@override
Stream<D?> watchFirst<T extends Table, D>(
TableInfo<T, D> table,
Expression<bool> Function(T) filter,
) =>
_core.repo.watchFirst(table, filter);
Stream<D?> watchFirst<D, T extends Table>(
Expression<bool> Function(T) filter,
) =>
_core.repo.watchFirst(_tableFor<T, D>(), filter);
// ── 原始 SQL ─────────────────────────────────────────────────────────────
@@ -120,9 +120,8 @@ class StorageSdkApiImpl implements StorageSdkApi, StorageSdkLifecycle {
// ── 统计 ─────────────────────────────────────────────────────────────────
@override
Future<int> count<T extends Table, D>(
TableInfo<T, D> table, {
Future<int> count<D, T extends Table>({
Expression<bool> Function(T)? filter,
}) =>
_core.repo.count(table, filter: filter);
}
_core.repo.count(_tableFor<T, D>(), filter: filter);
}

View File

@@ -8,12 +8,15 @@ import 'storage_sdk_api_impl.dart';
/// SDK 依赖装配入口。
///
/// 调用方传入数据库工厂SDK 负责连接生命周期和 CRUD 机制。
/// 调用方传入数据库工厂和表注册表SDK 负责连接生命周期和 CRUD 机制。
///
/// 示例im_app 的 DI 层):
/// ```dart
/// StorageSdkWiring.build(
/// databaseFactory: (executor) => AppDatabase(executor),
/// tableRegistry: (db) => {
/// User: (db as AppDatabase).users,
/// },
/// );
/// ```
class StorageSdkWiring {
@@ -21,10 +24,11 @@ class StorageSdkWiring {
static StorageSdkApi build({
required GeneratedDatabase Function(QueryExecutor) databaseFactory,
required Map<Type, TableInfo> Function(GeneratedDatabase) tableRegistry,
}) {
final dataSource = DatabaseDataSource(databaseFactory: databaseFactory);
final repo = DatabaseRepositoryImpl(dataSource);
final core = StorageSdkCore(dataSource: dataSource, repo: repo);
return StorageSdkApiImpl(core: core);
return StorageSdkApiImpl(core: core, tableRegistry: tableRegistry);
}
}
}

View File

@@ -47,7 +47,7 @@ melos:
gen:
description: "Run build_runner build in all packages that use it"
run: melos exec --depends-on="build_runner" -- dart run build_runner build --delete-conflicting-outputs
run: bash scripts/table_gen.sh && melos exec --depends-on="build_runner" -- dart run build_runner build --delete-conflicting-outputs
gen:watch:
description: "Watch mode code generation in all packages that use build_runner"

184
scripts/table_gen.sh Normal file
View File

@@ -0,0 +1,184 @@
#!/bin/bash
# 只更新 @DriftDatabase(tables: [...]), imports 和 getTableRegistry
# 其余内容保持不变
APP_DATABASE="apps/im_app/lib/data/local/drift/app_database.dart"
# ── 扫描表类 ────────────────────────────────────────────────────────────────
TABLE_CLASSES=()
DATA_CLASSES=()
GETTERS=()
IMPORTS=()
# 项目根目录(脚本所在目录的上一层)
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
# app_database.dart 所在目录(用于计算相对 import 路径)
APP_DATABASE_DIR="$(dirname "$ROOT_DIR/$APP_DATABASE")"
echo "🔍 扫描所有包中的 drift 表文件..."
echo " 根目录: $ROOT_DIR"
echo " app_database 目录: $APP_DATABASE_DIR"
# 搜索所有包中的 lib/data/local/drift/tables 目录下的 dart 文件
while IFS= read -r FILE; do
echo " 📄 $FILE"
# 提取继承 Table 或 View 的类名
while IFS= read -r LINE; do
CLASS=$(echo "$LINE" | sed -E "s/.*class ([A-Za-z0-9]+) extends (Table|View).*/\1/")
if [ -z "$CLASS" ]; then
continue
fi
TABLE_CLASSES+=("$CLASS")
# 提取 @DataClassName 注解
DATA_CLASS=$(grep -E "@DataClassName\('([^']+)'\)" "$FILE" | sed -E "s/.*@DataClassName\('([^']+)'\).*/\1/" | head -1)
if [ -z "$DATA_CLASS" ]; then
# 无注解时去掉末尾 's' 推断数据类名
DATA_CLASS="${CLASS%s}"
fi
DATA_CLASSES+=("$DATA_CLASS")
# getter 名 = 表类名首字母小写(跟 drift 生成一致)
GETTER=$(echo "$CLASS" | awk '{print tolower(substr($0,1,1)) substr($0,2)}')
GETTERS+=("$GETTER")
# 计算 package: import 路径(找到 pubspec.yaml 确定 package 名)
PKG_IMPORT=$(python3 -c "
import os, re
file_path = '$FILE'
search = os.path.dirname(file_path)
pkg_name = None
pkg_root = None
while search != '/':
pubspec = os.path.join(search, 'pubspec.yaml')
if os.path.exists(pubspec):
with open(pubspec) as f:
for line in f:
m = re.match(r'^name:\s*(\S+)', line)
if m:
pkg_name = m.group(1)
pkg_root = search
break
break
search = os.path.dirname(search)
if pkg_name and pkg_root:
lib_path = os.path.join(pkg_root, 'lib')
rel = os.path.relpath(file_path, lib_path)
print(f'package:{pkg_name}/{rel}')
else:
db_dir = '$APP_DATABASE_DIR'
print(os.path.relpath(file_path, db_dir))
")
IMPORTS+=("$PKG_IMPORT")
echo " ✅ 表类: $CLASS → 数据类: $DATA_CLASS → getter: $GETTER"
echo " 📦 import: $PKG_IMPORT"
done < <(grep -E "^class [A-Za-z0-9]+ extends (Table|View)" "$FILE")
done < <(find "$ROOT_DIR" \
-type f \
-name "*.dart" \
-path "*/lib/data/local/drift/tables/*" \
! -path "*/build/*" \
! -path "*/.dart_tool/*" \
! -path "*/.*")
if [ ${#TABLE_CLASSES[@]} -eq 0 ]; then
echo "❌ 未找到任何 drift 表类,请检查搜索路径。"
exit 1
fi
echo ""
echo "✅ 共找到 ${#TABLE_CLASSES[@]} 个表类:"
for i in "${!TABLE_CLASSES[@]}"; do
echo " [${TABLE_CLASSES[$i]}] → [${DATA_CLASSES[$i]}] → getter: ${GETTERS[$i]}${IMPORTS[$i]}"
done
TABLES_LIST=$(IFS=', '; echo "${TABLE_CLASSES[*]}")
echo ""
echo "📋 @DriftDatabase(tables: [$TABLES_LIST])"
# ── 用 sed 替换 @DriftDatabase(tables: [...]) ──────────────────────────────
echo ""
echo "✍️ 更新 @DriftDatabase..."
sed -i '' "s/@DriftDatabase(tables: \[.*\])/@DriftDatabase(tables: [$TABLES_LIST])/" "$ROOT_DIR/$APP_DATABASE"
echo "✅ @DriftDatabase 已更新"
# ── 生成 import 块和 registry ─────────────────────────────────────────────
IMPORT_BLOCK=""
for IMPORT_PATH in "${IMPORTS[@]}"; do
IMPORT_BLOCK+="import '$IMPORT_PATH';\n"
done
REGISTRY=""
for i in "${!TABLE_CLASSES[@]}"; do
DATA="${DATA_CLASSES[$i]}"
GETTER="${GETTERS[$i]}"
REGISTRY+=" $DATA: database.$GETTER,\n"
echo " 注册: $DATA → database.$GETTER"
done
echo ""
echo "✍️ 更新 imports + getTableRegistry..."
python3 - "$ROOT_DIR/$APP_DATABASE" "$REGISTRY" "$IMPORT_BLOCK" << 'PYEOF'
import sys, re
file_path = sys.argv[1]
registry = sys.argv[2].replace('\\n', '\n')
imports = sys.argv[3].replace('\\n', '\n')
with open(file_path, 'r') as f:
content = f.read()
# 替换 import 块(保留 drift import替换其余 table imports
new_content = re.sub(
r"(import 'package:drift/drift\.dart';\n)(?:import '[^']+\';\n)*",
"import 'package:drift/drift.dart';\n" + imports,
content
)
# 替换 getTableRegistry 里的第二个 return { 块(不依赖注释)
parts = new_content.split('getTableRegistry', 1)
if len(parts) == 2:
after = parts[1]
# 跳过第一个 return {}is! AppDatabase 那个),替换第二个
count = [0]
def replace_second(m):
count[0] += 1
if count[0] == 2:
return m.group(1) + '\n' + registry + ' ' + m.group(2)
return m.group(0)
new_after = re.sub(
r'(return \{)(.*?)(\};)',
lambda m: replace_second(re.match(r'(return \{)(.*?)(\};)', m.group(0), re.DOTALL)),
after,
flags=re.DOTALL
)
# 用更简单直接的方式:找到所有 return { 块,替换第二个
matches = list(re.finditer(r'return \{.*?\};', after, re.DOTALL))
if len(matches) >= 2:
m = matches[1]
new_after = after[:m.start()] + 'return {\n' + registry + ' };' + after[m.end():]
new_content = parts[0] + 'getTableRegistry' + new_after
with open(file_path, 'w') as f:
f.write(new_content)
print("✅ imports + getTableRegistry 已更新")
PYEOF
echo ""
echo "🎉 完成!下一步运行:"
echo " melos run gen"