Skip to content

fix: restore persisted metadata from sys_metadata on ObjectQLPlugin cold start#1076

Merged
hotlong merged 3 commits intomainfrom
copilot/fix-object-metadata-recovery
Apr 7, 2026
Merged

fix: restore persisted metadata from sys_metadata on ObjectQLPlugin cold start#1076
hotlong merged 3 commits intomainfrom
copilot/fix-object-metadata-recovery

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 7, 2026

protocol.loadMetaFromDb() exists and works, but was never called during kernel boot. Custom objects/views/apps saved to sys_metadata via protocol.saveMetaItem() were silently lost on every cold start or redeployment.

Changes

  • ObjectQLPlugin.start() lifecycle — Added restoreMetadataFromDb() call between driver.init() and syncRegisteredSchemas(), closing the persist→hydrate loop:
driver.init()              // DB connection ready
→ restoreMetadataFromDb()  // hydrate SchemaRegistry from sys_metadata  ← NEW
→ syncRegisteredSchemas()  // create/update tables for ALL registered objects (including restored ones)
  • Graceful degradation — Non-fatal when protocol service is missing, loadMetaFromDb isn't implemented, or sys_metadata table doesn't exist yet (first-run / in-memory driver). Logs at debug level and continues.

  • Integration tests — Three new tests: successful restoration from mock DB records, graceful handling of DB errors, and verification that restore runs before schema sync.

  • CHANGELOG — Entry under [Unreleased] > Fixed.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Apr 7, 2026 5:30am
spec Ready Ready Preview, Comment Apr 7, 2026 5:30am

Request Review

…old start

ObjectQLPlugin.start() now calls protocol.loadMetaFromDb() after driver
init and before syncRegisteredSchemas, restoring user-created objects,
views, and apps from the database on every kernel boot. Gracefully
degrades when sys_metadata table doesn't exist (first-run/in-memory).

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/8106ca9a-9f6e-4dfb-b4e6-c4e1ff984289

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix objectQL plugin automatic metadata recovery on startup fix: restore persisted metadata from sys_metadata on ObjectQLPlugin cold start Apr 7, 2026
Copilot AI requested a review from hotlong April 7, 2026 04:15
@hotlong hotlong marked this pull request as ready for review April 7, 2026 05:04
Copilot AI review requested due to automatic review settings April 7, 2026 05:04
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 7, 2026

@copilot 评估是否已经完成了任务中要求的所有开发,并仔细测试确认修正bug

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 7, 2026

@copilot 评估是否已经完成了任务中要求的所有开发,并仔细测试确认修正bug

已完成所有任务要求的验证,确认如下:

Issue 需求对照 (全部完成 ✅):

  1. ObjectQLPlugin.start()driver.init() 之后、syncRegisteredSchemas() 之前调用 protocol.loadMetaFromDb() — 通过 restoreMetadataFromDb(ctx) 实现 (commit 5d4f207)
  2. ✅ 恢复后自动触发 schema sync — 调用顺序: driver.init()restoreMetadataFromDb()syncRegisteredSchemas(),恢复的对象会被 sync
  3. ✅ 兼容内存模式 — 当 protocol 不可用、loadMetaFromDb 未实现或 sys_metadata 表不存在时,以 debug 级别日志优雅降级,不抛异常
  4. ✅ CHANGELOG 已更新 — [Unreleased] > Fixed 下新增条目

测试结果: 188/188 全部通过

  • 185 个原有测试 ✅
  • 3 个新增冷启动测试 ✅:
    • 从 sys_metadata 成功恢复元数据
    • DB 故障时优雅降级不抛异常
    • 恢复在 syncRegisteredSchemas 之前执行(顺序验证)
  • 7 个已有 loadMetaFromDb 单元测试 ✅

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a cold-start persistence gap in ObjectQLPlugin by restoring previously saved metadata from the sys_metadata table during kernel boot, so user-created objects/views/apps survive redeployments.

Changes:

  • Added a startup hydration step in ObjectQLPlugin.start() that calls protocol.loadMetaFromDb() after driver init and before schema sync.
  • Introduced integration tests covering successful restore, graceful failure, and restore-before-schema-sync ordering.
  • Added a changelog entry under [Unreleased] > Fixed.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
packages/objectql/src/plugin.ts Adds restoreMetadataFromDb() and invokes it during start() to hydrate the in-memory registry from DB before schema sync.
packages/objectql/src/plugin.integration.test.ts Adds integration tests validating metadata restoration behavior and ordering.
CHANGELOG.md Documents the cold-start metadata restoration fix.

Comment on lines +354 to +363
private async restoreMetadataFromDb(ctx: PluginContext): Promise<void> {
try {
const protocol = ctx.getService('protocol') as any;
if (!protocol || typeof protocol.loadMetaFromDb !== 'function') {
ctx.logger.debug('Protocol service does not support loadMetaFromDb, skipping DB restore');
return;
}

const { loaded, errors } = await protocol.loadMetaFromDb();

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restoreMetadataFromDb relies on ctx.getService('protocol') as any to access loadMetaFromDb, but loadMetaFromDb is not part of the typed protocol contract (and the as any defeats type-safety). Consider introducing a local ProtocolWithDbRestore type + type guard (or extending the protocol contract with an optional loadMetaFromDb?) so this call is type-checked and discoverable.

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +374
private async restoreMetadataFromDb(ctx: PluginContext): Promise<void> {
try {
const protocol = ctx.getService('protocol') as any;
if (!protocol || typeof protocol.loadMetaFromDb !== 'function') {
ctx.logger.debug('Protocol service does not support loadMetaFromDb, skipping DB restore');
return;
}

const { loaded, errors } = await protocol.loadMetaFromDb();

if (loaded > 0 || errors > 0) {
ctx.logger.info('Metadata restored from database', { loaded, errors });
} else {
ctx.logger.debug('No persisted metadata found in database');
}
} catch (e: unknown) {
// Non-fatal: first-run or in-memory driver may not have sys_metadata yet
ctx.logger.debug('DB metadata restore skipped (non-fatal)', {
error: e instanceof Error ? e.message : String(e),
});
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch-all log message DB metadata restore skipped (non-fatal) can be misleading because this try also covers non-DB failures (e.g., getService('protocol') throwing, permission enforcement, async-service error). Consider narrowing the try/catch (separate service resolution from DB hydration) or adjusting the message/fields (e.g., include a structured error object/stack) so operators can distinguish missing protocol vs DB/table issues.

Copilot uses AI. Check for mistakes.
Comment on lines +841 to +845
type: 'app',
name: 'custom_crm',
state: 'active',
metadata: JSON.stringify({ name: 'custom_crm', label: 'Custom CRM' }),
},
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test persists an app record with type: 'app', but the registry’s canonical app helpers/validation are keyed under 'apps' (e.g., SchemaRegistry.registerApp() stores type 'apps'). Using 'apps' here would better reflect how apps are typically registered/persisted and would catch type-normalization issues earlier.

Copilot uses AI. Check for mistakes.

// Assert — items should be restored into the registry
const registry = (kernel.getService('objectql') as any).registry;
expect(registry.getItem('app', 'custom_crm')).toEqual({
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion checks registry.getItem('app', ...), which may not match how the rest of the code consumes apps (often via getAllApps() / 'apps' type). Consider asserting against the canonical app storage ('apps' / getAllApps()) so the test validates end-to-end behavior rather than a type alias that might not be used by consumers.

Suggested change
expect(registry.getItem('app', 'custom_crm')).toEqual({
expect(registry.getAllApps()).toContainEqual({

Copilot uses AI. Check for mistakes.
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 7, 2026

@copilot fix all

@github-actions github-actions bot added documentation Improvements or additions to documentation tests size/m labels Apr 7, 2026
…ndling, canonical app type in tests

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/0c3ad9ed-acb5-48f4-a9c5-c81bde62ed87

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 7, 2026

@copilot fix all

All 4 review comments addressed in commit e8330d4:

  1. Type safety — Replaced as any cast with ProtocolWithDbRestore interface + hasLoadMetaFromDb() type guard
  2. Narrower error handling — Separated service resolution (Phase 1) from DB hydration (Phase 2) into distinct try/catch blocks with specific log messages
  3. Test type — Changed mock DB record from type: 'app' to type: 'apps' (canonical registry type)
  4. Test assertion — Changed getItem('app', ...) to getAllApps() with toContainEqual

All 188 tests pass.

@hotlong hotlong merged commit 563c087 into main Apr 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ObjectQLPlugin未在启动时自动从数据库(sys_metadata)恢复对象元数据,导致冷启动后自定义对象丢失

3 participants