
OpenSEO 本地 Postgres 开发指南Docker 容器、Drizzle 迁移与 Hyperdrive 绑定的完整实操【免费下载链接】open-seoOpen source alternative to Semrush and Ahrefs项目地址: https://gitcode.com/GitHub_Trending/op/open-seoOpenSEO 默认运行在 Cloudflare D1SQLite之上Postgres 是面向数据量突破 D1 存储上限场景的可选后端两者通过DATABASE_PROVIDER标志和 provider-aware 的db层在运行时切换。本文基于仓库内的 LOCAL_POSTGRES.md 展开带你用 Docker 快速搭起一套一次性 Postgres 环境完成迁移、应用指向与数据验证并结合src/db/下的源码讲清这条 Postgres 路径的底层实现。读完你可以独立完成 Postgres 本地开发环境的全部搭建与拆除并理解 OpenSEO 双方言数据库层的运行机制。为什么是 D1 默认、Postgres 可选OpenSEO 的应用代码只写一份直接面向src/db/中 provider-aware 的db句柄运行时唯一的区别就是DATABASE_PROVIDER标志和一条连接串。这一点在 src/db/index.ts 中有直接体现export const db (getDatabaseProvider() postgres ? pgDb : d1Db) as unknown as typeof d1Db;即整库句柄被统一以 D1 客户端的类型导出Postgres schema 与 SQLite schema 结构一致性由专门的 parity 测试保证见下文业务层因此无需感知方言。文档明确指出普通开发不需要这套 Postgres 环境D1 是默认路径也是多数贡献者应该使用的路径。这套指南服务于两类场景本地验证 Postgres 代码路径以及为“D1 用尽后”的规模化部署做预演。DATABASE_PROVIDER的解析逻辑在 src/db/provider.tsexport function getDatabaseProvider(): DatabaseProvider { const provider Reflect.get(env, DATABASE_PROVIDER); if (provider postgres) { return postgres; } if (provider d1 || provider undefined || provider ) { return d1; } throw new Error( Unsupported DATABASE_PROVIDER ${String(provider)}. Expected d1 or postgres., ); }取值只有d1与postgres两种语义未设置或空串时静默回落到 D1出现其他值则直接抛错避免静默走错后端。前置条件Docker Desktop或 Docker Engine常规的本地开发环境即 LOCAL_DEVELOPMENT.md 中描述的 Node.js 20 / Corepack 激活的 pnpm 环境且已完成corepack enable、pnpm install --frozen-lockfile与pnpm run db:migrate:local等基础步骤。第一步启动一次性 Postgres 容器宿主机使用端口5433来避免与本机默认占用5432的系统 Postgres 冲突docker run --name openseo-postgres \ -e POSTGRES_USERopenseo \ -e POSTGRES_PASSWORDopenseo \ -e POSTGRES_DBopenseo \ -p 5433:5432 \ -d postgres:16容器内仍是标准5432通过-p 5433:5432映射到宿主机的5433。等待其接受连接docker exec openseo-postgres pg_isready -U openseo -d openseo此时可用的连接串为postgres://openseo:openseolocalhost:5433/openseo第二步应用 Postgres 迁移Postgres 侧的 schema 是手写的——它是唯一一个db:generate不会重新生成的结构性产物对应的迁移文件位于 drizzle-pg/ 目录与 D1 的 drizzle/ 目录并存。迁移通过pnpm db:migrate:pg应用该命令定义在 package.json 中db:migrate:pg: drizzle-kit migrate --config drizzle-pg.config.ts它要求POSTGRES_DATABASE_URL已设置drizzle-kit会从 shell 环境读取。配置文件 drizzle-pg.config.ts 的关键内容import { defineConfig } from drizzle-kit; import { loadLocalEnv } from ./scripts/cli-utils; // Pull POSTGRES_DATABASE_URL from .env.local (no-op if already in the shell env), // so the migration runbooks single .env.local works for db:migrate:pg too. loadLocalEnv(); export default defineConfig({ dialect: postgresql, schema: ./src/db/pg/schema.ts, out: ./drizzle-pg, dbCredentials: { url: process.env.POSTGRES_DATABASE_URL!, }, });注意loadLocalEnv()它来自 scripts/cli-utils.ts会依次读取.env.local、.env并把未设置的变量补进process.env。这意味着你可以把POSTGRES_DATABASE_URL只写进.env.local而无需在每条命令前内联赋值——文档给出的标准命令则演示了内联方式POSTGRES_DATABASE_URLpostgres://openseo:openseolocalhost:5433/openseo \ pnpm db:migrate:pgPOSTGRES_DATABASE_URL只被 Node 侧工具读取drizzle-kit与 scripts/migrate-d1-to-postgres.ts应用本身会忽略它这一点在排查“迁移成功了但应用连不上”之类的问题时尤其关键。第三步让应用指向 PostgresCloudflare Vite 运行时从.env.local读取 Worker vars所以 provider 标志必须写在那里而不是仅仅 export 到 shell# .env.local DATABASE_PROVIDERpostgres连接串则来自HYPERDRIVE绑定。wrangler.jsonc中的hyperdrive块随仓库以注释形式提供需要先取消注释见 wrangler.jsonc 中的相关注释块// hyperdrive: [ // { // binding: HYPERDRIVE, // id: 9d64ccfb559f44449ce52a143912f898, // localConnectionString: postgres://openseo:openseolocalhost:5433/openseo, // }, // ],本地开发时Miniflare 会把这个绑定解析为localConnectionString——它已经指向第一步的 Docker 容器且 Miniflare 从不接触真实的 Hyperdrive 服务。在已部署的 Worker 中同一个绑定解析到真正的 Hyperdrive应用只有经过这个绑定才会连接 Postgres不存在直连回退。如果本地 Postgres 不在默认位置可以不改配置直接覆盖CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVEpostgres://... pnpm dev然后照常启动开发服务器pnpm dev要切回 D1删除.env.local中那行或改为DATABASE_PROVIDERd1再重启即可。源码视角应用如何真正访问 Postgres这条绑定在运行时的消费入口是 src/db/provider.ts 的getPostgresConnectionString()export function getPostgresConnectionString() { const hyperdrive Reflect.get(env, HYPERDRIVE) as | { connectionString?: string } | undefined; const hyperdriveUrl hyperdrive?.connectionString?.trim(); if (hyperdriveUrl) { return hyperdriveUrl; } throw new Error( DATABASE_PROVIDERpostgres requires a HYPERDRIVE binding (in local dev, its localConnectionString)., ); }即只从HYPERDRIVE绑定取连接串取不到就抛错与文档“Hyperdrive is the ONLY way the app connects to Postgres”的表述一致。拿到连接串之后src/db/pg/client.ts 展示了 Workers 环境下 Postgres 的硬性约束运行时禁止一个请求复用另一个请求创建的 socketCannot perform I/O on behalf of a different request所以每个请求都必须持有独立的 Postgres 客户端。实现上pgDb是一个Proxy它从AsyncLocalStorage中取出当前请求作用域的客户端各入口点fetch、scheduledcron、WorkflowEntrypointrun统一用withPgClient()包裹src/db/pg/client.tsexport async function withPgClientT(fn: () PromiseT): PromiseT { if (getDatabaseProvider() ! postgres) { return fn(); // D1 模式下是空操作 } if (pgClientStore.getStore()) { return fn(); // 可重入嵌套作用域复用环境中的客户端 } const sql withQueryRetries( postgres(getPostgresConnectionString(), { max: 1, // Hyperdrive 在边缘池化源连接客户端再建池只会引入陈旧连接风险 fetch_types: false, connect_timeout: 10, // 限制连接等待让 per-query 重试在请求生命周期内生效 }), ); return pgClientStore.run({ sql, db: createPgDb(sql) }, fn); }几个值得注意的细节单连接max: 1且不调用sql.end()Workers 与 Hyperdrive 之间的 socket 在调用结束时自动拆除而源端连接保持热态供复用不主动断开还允许流式响应在 handler 返回后继续查询瞬时连接错误重试src/db/pg/retry.ts 的withQueryRetries包装了 postgres.js 的sql.unsafe入口对连接类错误ECONNREFUSED、08000等 Postgres class 08 错误码等按250/1000/2500ms 随机抖动的间隔重试。为保证写安全只有“查询到达服务器之前”就失败的错误才允许对写语句重试连接中途断开时仅SELECT会被重放事务则完全不做重试src/db/pg/retry.ts原子批量写入必须走runBatchdb.batch只存在于 D1 驱动Postgres 驱动下调用会抛错。src/db/runBatch.ts 提供了双后端一致的原子批量语义——D1 走db.batch([...])Postgres 走db.transaction内按数组顺序逐条执行并配合DB_BATCH_SIZE 100的executeInBatches分片该批大小受 D1 每条语句约 100 个绑定参数的上限约束。第四步验证用psql直接检查迁移建出的表以及应用写入的数据# 迁移创建的表 docker exec openseo-postgres psql -U openseo -d openseo -c \dt # 查看应用写入的行例如创建项目 / 保存关键词之后 docker exec openseo-postgres psql -U openseo -d openseo -c select count(*) from projects;projects表来自迁移产物 drizzle-pg/ 下的 SQL 文件目录内含0000至0042共 40 余个版本化迁移如果\dt能看到对应表而select count(*)在你操作应用后仍然为 0通常说明应用还在走 D1 路径——回到第三步检查.env.local与wrangler.jsonc的 hyperdrive 块。Schema 变更两个方言都要改修改任何表时需要同时更新两套方言SQLitesrc/db/*.schema.ts配合pnpm db:generate:d1迁移输出到 drizzle/Postgressrc/db/pg/*.schema.ts配合pnpm db:generate:pg迁移输出到 drizzle-pg/。两套命令都定义在 package.json 中pnpm db:generate会顺序执行两者。防漂移的守门员是 src/db/schema-parity.test.ts它会在 CI 中因两套方言漂移而失败。从测试源码看它逐表比对的维度相当完整两套方言定义了相同的表集合每表的列名称、可空性、方言无关的dataType、默认值、enum 取值主键含复合主键唯一约束/唯一索引且索引是否带WHERE谓词partial→full 的变更会被捕获因为它会改变onConflict语义外键含onDelete行为repository 层的级联删除依赖这一不变量CHECK 约束名称better-auth 表因为按方言生成SQLiteinteger时间戳 vs Postgrestimestamptz/jsonb只比较结构而非列类型但额外校验了一批 CLI 不会生成、需要手工补齐的二级索引如session.user_id、organization.slug唯一索引仍然存在。还有一个容易踩的坑文档特别强调parity 测试比较的是schema 定义而不是生成的迁移文件。所以修改 Postgres schema 后务必运行pnpm db:generate:pg并提交新的drizzle-pg/迁移——否则即便 parity 测试是绿的Postgres 部署也会漏掉这次变更。此外同一份测试还静态扫描src/目录禁止任何文件在src/db/runBatch.ts之外直接调用.batch(从 CI 层面固化了上文“原子多语句写入必须走runBatch”的约束。拆除环境docker rm -f openseo-postgres这会删除容器及其全部数据需要干净环境时从第一步重新执行即可。同时记得清理.env.local中的DATABASE_PROVIDERpostgres避免后续开发误连已不存在的容器。小结与适用前提要素取值 / 位置说明本地端口5433避开系统 Postgres 的5432容器openseo-postgrespostgres:16一次性开发库数据随容器销毁连接串postgres://openseo:openseolocalhost:5433/openseo同时用于迁移与localConnectionString迁移命令POSTGRES_DATABASE_URL... pnpm db:migrate:pg只被 Node 侧工具读取应用开关.env.local中DATABASE_PROVIDERpostgres由 Vite/Miniflare 作为 Worker var 注入绑定wrangler.jsonc中注释掉的hyperdrive块取消注释后本地解析为localConnectionString一致性守门src/db/schema-parity.test.ts比较 schema 定义而非迁移文件改 Postgres schema 后必须db:generate:pg适用前提本指南针对 OpenSEO 当前的双方言数据库架构src/db/provider 层 drizzle/drizzle-pg双迁移目录与postgres:16镜像DATABASE_PROVIDER的合法取值仅限d1/postgres见 src/db/provider.ts。对绝大多数贡献者D1 默认路径pnpm run db:migrate:localpnpm dev见 LOCAL_DEVELOPMENT.md仍然是更简单、推荐的选择。【免费下载链接】open-seoOpen source alternative to Semrush and Ahrefs项目地址: https://gitcode.com/GitHub_Trending/op/open-seo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考