运行时能力

数据库与数据归属

根据数据归属选择 Plugin 数据库或应用数据库,并管理 Drizzle schema 与迁移。

先判断数据是否必须跟随 Plugin 独立安装、替换和迁移,再选择数据库组织方式。不要仅因为代码写在 Plugin class 中,就默认使用 ctx.database

数据与生命周期要求正确组织方式
Plugin 可以独立发布、安装或替换,数据也属于这个 PlugindefineDatabase() + ctx.database.use(),每个 Plugin 使用独立实例
需要 Plugin 独立 lineage 或旧 generation handle 撤销defineDatabase() + ctx.database.use()
fixed catalog、schema 和部署由同一个应用团队控制application-private database package
没有共享数据库,整个 static application 就无法成立host prepare() + root-bound typed accessor
只有部分内置 Plugin 依赖共享数据库,其他 Plugin 应继续运行application-private provider Plugin + constructor dependency
多个内置 Plugin 的表必须 join、使用 foreign key 或共享原子事务application-private database package

Managed Plugin database 统一使用 PostgreSQL dialect 和 Drizzle。Plugin 作者只依赖 drizzle-orm,不选择 driver;部署宿主在 native PostgreSQL 与 PGlite 之间选择,同一份 schema、migration 和 query 不编写 driver 分支。

Application-private database 由应用自行选择 PostgreSQL、SQLite、ORM 和 migration 方案。它不是“多个 Plugin 共用一份 Plugin database”:使用它的 Plugin 是应用内部模块,不再拥有独立的数据可移植性,也不会自动获得 Pluxel 的 per-plugin lineage、隔离、旧 handle 撤销或 owner-scoped invalidation。

发布与否只是常见线索,不是最终判断。私有 Plugin 如果仍要被独立启停、替换并保留自己的数据,应该使用 managed database;公开 Plugin 如果不拥有结构化数据,则不需要数据库。

Managed Plugin database

选择 managed database 后,正式部署使用 native PostgreSQL;PGlite 只用于本机开发和自动化测试。两者统一的是 PostgreSQL 作者 contract,不是性能、并发和 durability 等价。

宿主必须启用 managed database,并选好连接后端;如果现有入口设置了 database: false,先在 宿主配置 中调整。插件包安装 drizzle-orm,driver 由宿主提供。下面的 schema 与插件文件放在同一个插件包内。

定义 Plugin schema

// @filename: database.ts
// 仅服务端
import { , , , ,  } from 'drizzle-orm/pg-core'
import {  } from '@pluxel/runtime/database'

export const  = (
	'notes',
	{
		: ('id').().(),
		: ('title').(),
		: ('created_at', { : true }).().(),
	},
	() => [('notes_created_at_idx').(.)],
)

export const  = ({
	: {  },
})

// @filename: NotesPlugin.ts
import type {  } from '@pluxel/runtime/database'
import { ,  } from '@pluxel/runtime'
import { ,  } from './database.ts'

@({ : 'Notes' })
export class  extends  {
	private !: <typeof >

	protected override async () {
		this. = await this...()
	}

	() {
		return this..(() => .().().(.))
	}

	(: string) {
		return this..(async () => {
			const [] = await .().({  }).()
			return 
		})
	}
}

使用普通 pgTable()。不要使用 pgSchema()、Plugin name prefix、public. 或 physical schema;owner isolation 由 database instance 和 runtime namespace 管理。

schema module 是 server-only。Workbench API/browser bundle 不能导入它。Plugin package 依赖受支持的 drizzle-orm,不依赖 pg、PGlite 或其他 driver。

defineDatabase() 必须作为 module-level const 的直接 initializer,显式 evolution 必须写在直接 object literal 中。不要把它包进 function、branch 或 config factory;compiler 必须静态看到 package 唯一的 definition,才能注入对应 artifact。

use() 在返回前完成 active instance 选择和 migration prepare。handle 绑定当前 Plugin owner/generation,stop/replacement 后旧 handle 失效。

读操作放进 read(),写操作放进完整 transaction() callback。callback 内是标准 Drizzle database/transaction;不要缓存 callback 参数或 row lock,也不要在 transaction 中等待网络、用户交互或 worker task。

一个 Plugin 只能 use() 一个 definition。definition 可以作为 schema 模板由多个 Plugin 复用,但每个 owner 都有独立数据、role 和 operation queue;不能跨 Plugin 共享 handle 或 transaction。

PluginPart 也不拥有第二个 definition;它的表和 migration 都归属根 Plugin 的这一个 definition。Part 只帮助组织代码和资源,不创造新的数据边界。

跨 Plugin 数据关系与扩展

独立发布的 Plugin 不能修改另一个 Plugin 的表模型:不要向其 pgTable() 加列、生成 ALTER TABLE、添加 foreign key/index,也不要把对方 table object 当作自己的 schema import。表结构和 migration 是 owner 可独立替换的存储 contract;允许第三方修改会把安装顺序、卸载、rebase 和 rollback 耦合在一起。

需求正确做法
为另一个 Plugin 的实体保存本 Plugin 专属数据extension Plugin 自己拥有表,以 provider 公开的稳定 entity ID 关联,并通过 typed capability 验证或操作该实体
需要 join、foreign key、跨表原子 transaction,或 provider 必须直接查询/索引扩展字段将整组表放进同一 application-private database,由同一个应用团队迁移
需要可插拔的自定义字段provider 只在存在具体产品需求时发布自己的领域 extension protocol;不要增加通用“改别人表”的能力

第一种方式的两个写入是两个 transaction;需要一致性时,extension 应设计可重试、幂等的领域流程,而不是绕过 owner boundary。若这个代价不能接受,说明这些表本来就不应是独立 Plugin 数据。

跨 Plugin workflow 通过 typed capability/RPC,并接受它是两个 transaction。真正必须满足 foreign key、join 或原子 transaction 的表应该归同一 owner。

不要通过猜测 physical schema、连接字符串或 owner prefix 跨界读另一个 Plugin 的表。Workbench target 也只能调用所属 Plugin 的领域 service,再返回 detached DTO;它不是跨 owner 数据库入口。

Migration evolution

每个 definition 只走一条表演进流程;不要在两者之间混用 artifact 或命令:

evolution改表后的作者动作repository 中的 artifact
migrations(默认)修改 schema → generate --namecheck → 提交 → build提交 drizzle/;history 只能追加
reset-on-schema-change修改 schema → build/测试不创建 drizzle/generatecheckrebase 会明确拒绝

保留权威数据

默认 evolution: 'migrations'。在 definition 所在 package root 运行:

pluxel database generate --name add-notes
pluxel database check
pluxel build

提交整个 drizzle/,包括 SQL、meta/pluxel-migrations.json。已经提交的 SQL 不可重写;schema 变化生成下一条 migration。

production startup 只应用 build 时检查过的 artifact,不执行 schema push,也不根据当前 TypeScript schema 临时猜 DDL。

明确重建 lineage

如果产品明确放弃旧结构和 history,生成新 lineage:

pluxel database rebase --lineage 2026-v2 --name initial
pluxel database check

命令在 staging 生成当前 schema 的全新 baseline,成功后才替换本地 drizzle/。部署时 runtime 创建隔离 candidate instance,全部 migration 成功后原子激活;旧 instance 与数据归档保留。

不要手工修改 lineage,也不要把临时权限、锁、连接中断或 SQL error 当作 rebase 信号。

可丢弃数据自动 reset

缓存、搜索索引和可重新生成的同步副本可以声明:

export const SearchDatabase = defineDatabase({
	schema: { documents },
	evolution: 'reset-on-schema-change',
})

这种 definition 只运行 pluxel build,不创建或提交 drizzle/generatecheckrebase 会明确拒绝它。构建器从当前 schema 生成 baseline:相同 schema 保留数据,table/column/index/constraint 改变时建立空 candidate,准备成功后替换 active instance 并归档旧数据。

这个声明意味着未来任何 schema 变化都允许清空数据。需要保留或转换旧数据时必须使用 migrations。

Document 风格数据

可以在稳定的 idversionjsonb data 表中兼容多个 document version。JSON 字段内部变化不一定需要 SQL migration,但新增物理 index、constraint 或 column 仍是 schema evolution。

如果数据只是少量加密 JSON 且不需要 query/index,先评估 Vault;不要为一个 token 建完整关系表,也不要拿 Vault documents 替代需要查询的数据库。

Application-private database

当 fixed catalog、schema 和部署都由同一团队维护时,把 schema、client、repositories、migration 和 connection lifecycle 放进普通 application-private package,例如 @app/database。这个 package 自己声明 ORM 和 driver dependency;不要只把依赖安装在 workspace root,再让子包隐式使用。

Static application 应在 configure() 返回 database: false,使误用 ctx.database 的内置 Plugin 直接启动失败;production freezer 同时设置 managedDatabaseDrivers: [],避免把未使用的 PGlite 与 pg package 复制进发行物。两处配置分别约束运行时 capability 与构建闭包,必须保持一致。

内置 Plugin 优先消费 repository 或 application service。只有确实需要构造查询时才暴露 ORM client;不要让每个 Plugin 各自读取 DSN、创建 pool 或运行 migration。

整个应用依赖数据库

如果没有数据库,整个 static application 就没有可运行的核心功能,数据库是 host-owned root resource:static prepare() 必须在 Plugin graph 启动前完成连接、migration 和必要 preflight;失败直接终止本次 host startup。成功实例按 root Context 绑定,关闭登记到 root effects,不能归属任一 consumer Plugin。

Application package 导出接收 Context 的 typed accessor,不把数据库投影成 ctx.database,也不使用进程级 module singleton:

// @app/database — application-private server module
import type { Context } from '@pluxel/runtime'
import { openDatabase, migrate, type AppDatabase, type DatabaseOptions } from './internal.js'

const active = new WeakMap<object, AppDatabase>()

export async function prepareAppDatabase(ctx: Context, options: DatabaseOptions): Promise<void> {
	const root = ctx.root
	if (active.has(root)) return
	const database = await openDatabase(options)

	try {
		await migrate(database)
		active.set(root, database)
		root.effects.defer(
			async () => {
				if (active.get(root) === database) active.delete(root)
				await database.close()
			},
			{ tag: 'AppDatabase', phase: 'shutdown' },
		)
	} catch (error) {
		if (active.get(root) === database) active.delete(root)
		await database.close()
		throw error
	}
}

export function appDatabaseFor(ctx: Context): AppDatabase {
	const database = active.get(ctx.root)
	if (!database) throw new Error('Application database has not been prepared')
	return database
}

appDatabaseFor(ctx) 的参数既保留 root 隔离和完整返回类型,也在调用点诚实表达 application-private dependency。不要用 declaration merging 增加 ctx.appDatabase;static application 的泛型不能反向改变独立编译 Plugin 的 Context shape。

Static entry 对部署路径保持唯一 authority,同时供 Runtime persistence 和 application database 使用:

import { resolve } from 'node:path'
import { defineStaticRuntime } from '@pluxel/runtime-static'
import { prepareAppDatabase } from '@app/database'

function storagePaths({ env, deployment }) {
	const root = resolve(env.APP_DATA_ROOT ?? `${deployment?.root ?? '.'}/data`)
	return {
		runtimePersistence: resolve(root, 'runtime'),
		applicationDatabase: resolve(root, 'application.sqlite'),
	}
}

export default defineStaticRuntime({
	name: 'application',
	plugins: [BillingPlugin, AuditPlugin],
	configure(startup) {
		return {
			persistence: storagePaths(startup).runtimePersistence,
			database: false,
		}
	},
	async prepare({ host, startup }) {
		await prepareAppDatabase(host.ctx, {
			filename: storagePaths(startup).applicationDatabase,
		})
	},
})

Plugin 在 init() 或之后同步取得已准备实例:

protected override init() {
	this.database = appDatabaseFor(this.ctx)
}

不要读取已移除的 ctx.config.persistence,也不要从 ctx.root.persistence 猜 filesystem path。前者会把 host config 泄露给 Plugin;后者是 namespace/get/put 操作抽象,backend 可能是 memory、readonly 或 custom,并不保证存在 SQLite 可以打开的目录。SQLite path、DSN、TLS 和 pool options 都从 static startup 的 env、bindings 或 deployment facts 解析。

prepare() 不是通用 service lifecycle:这里只表达“数据库是整个应用的硬 readiness 前提”。数据库 package 负责领域初始化,root effects 负责 acquisition rollback、正常 stop 和 shutdown;consumer replacement 不能关闭共享实例。

只有部分 Plugin 依赖数据库

如果数据库失败时无关 Plugin 仍应运行,把数据库建模为 application-private provider Plugin,并让 consumer 通过 constructor 声明 required dependency。provider 在 init() 打开和迁移数据库,并立即把关闭登记到自己的 effects;provider failure 只阻塞 dependents。不要同时保留 root prepare() 和 provider Plugin 两套所有权。

这条路径可以使用 SQLite 或其他数据库,但应用必须自行负责 migration 并发、连接恢复、备份、durability 和 shutdown。不要把 application client 包装成 ctx.database,否则会让调用者误以为它具备 managed Plugin database 的 owner isolation 与 replacement 语义。

在 Workbench 中读取数据库状态

Workbench 不提供数据库专用查询协议。Plugin 在自己的 Direct View API 中返回 bounded browser-safe snapshot, 需要更新时公开普通 watch(invalidate) capability;查询、分页、DTO 投影和 mutation 都留在 Plugin service/target:

interface NotesApi extends RpcTarget {
	list(input: { cursor: string | null; limit: number }): Promise<NotesPage>
	update(input: UpdateNoteInput): Promise<UpdateNoteResult>
	watch(invalidate: () => void): RpcTarget
}

Target 内部可以使用 owner-bound database handle,但不能把 handle、Drizzle row 或 transaction 暴露给 browser。DTO 显式转换 DateBigInt、Buffer 等 server values,并限制 rows/bytes。Mutation commit 后由 Plugin 自己触发 invalidation; Workbench 不解析 table identity,也不成为 database lifecycle owner。完整用法见 插件管理界面

选择 native PostgreSQL 或 PGlite

部署或验证目标Backend
正式部署、持续用户数据或多个 Plugin 频繁访问数据库native PostgreSQL
本机开发、自动化测试PGlite
row lock、deadlock、pool exhaustion、连接中断必须验证 native PG
多进程并发 migration、advisory lock 和故障恢复必须验证 native PG
throughput、latency、容量规划或生产硬件性能验收必须使用目标 native PG

PGlite 是执行真实 PostgreSQL 语义的本地 backend,不是 query mock;它适合快速验证 schema、migration、CRUD、owner isolation 和 Plugin lifecycle。但是 Pluxel 会把共享 PGlite 上的所有 database operation 串行调度,因此一个 host 中的 Plugin 会共同受到单连接吞吐上限影响。不要用 PGlite benchmark 推断 native PostgreSQL 性能。

PGlite 的 data directory 只为本机工作流提供正常关闭后的便利重启,不是部署存储承诺。Pluxel 不以补充 filesystem flush 或 fault-injection 测试的方式把它升级为 production baseline;需要正式部署时使用 native PostgreSQL。资源受限时,应在目标设备上调低 PostgreSQL connection/pool budget 并实测,而不是把 PGlite 带入部署。

连接字符串、TLS、pool 与 PGlite data directory 是 host startup policy,不是 Plugin config。Plugin schema/query 不根据 backend 分支。

测试与发布检查

@pluxel/test/vitest 会对 database declaration 运行与开发/生产相同的 artifact transform:migration strategy 校验已提交 history,reset strategy 生成临时 baseline。

日常测试使用 memory:// PGlite;部署门禁增加真实 PostgreSQL integration suite。PGlite 覆盖语义与生命周期,native PostgreSQL suite 覆盖并发、锁、连接池和故障行为。

至少验证:

  • first startup 与 restart;
  • read/transaction callback;
  • migration failure 阻止 Plugin running;
  • required dependent 被 blocked、无关 Plugin 继续;
  • stop/replacement 后旧 handle 失效;
  • package artifact 包含 dist/database/migrations/ 的 checked SQL/manifest。

不要在测试里调用 internal helper 或手工构造 migration artifact,否则测试没有覆盖作者真正发布的 schema。

最后更新于

本页目录