feat(db): 0001 来源域——connection/snapshot/job/record + 快照不可变触发器

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-20 19:27:31 +08:00
parent 5da883cd40
commit 32ceba58f0
5 changed files with 1002 additions and 2 deletions
+71 -2
View File
@@ -1,5 +1,5 @@
// 聚合导出:Task 6 起按域追加 pgTable 定义(站点域 = 迁移文件 0000_pale_sumo)
import { pgTable, uuid, text, integer, jsonb, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
// 聚合导出:Task 6 起按域追加 pgTable 定义(站点域 = 迁移文件 0000_pale_sumo;来源域 = 0001_*)
import { pgTable, pgEnum, uuid, text, integer, bigint, jsonb, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
export const sites = pgTable('sites', {
id: uuid('id').primaryKey().defaultRandom(),
@@ -19,3 +19,72 @@ export const siteConfigRevisions = pgTable('site_config_revisions', {
checksum: text('checksum').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (t) => [uniqueIndex('site_config_rev_uq').on(t.siteId, t.version)])
// ---- 来源域(spec §5.2;迁移文件 0001_*,末尾手写追加快照不可变触发器)----
export const sourceConnectionKind = pgEnum('source_connection_kind', ['wxr'])
export const sourceSnapshotKind = pgEnum('source_snapshot_kind', ['wxr', 'sitemap', 'media_bundle', 'manual_url_list'])
export const importJobStatus = pgEnum('import_job_status', [
'queued', 'running', 'succeeded', 'succeeded_with_issues', 'failed', 'cancelled', 'stale',
])
// Connection 只描述来源,不保存某次文件路径和 Hash
export const sourceConnections = pgTable('source_connections', {
id: uuid('id').primaryKey().defaultRandom(),
siteId: uuid('site_id').notNull().references(() => sites.id),
kind: sourceConnectionKind('kind').notNull(),
name: text('name').notNull(),
configuration: jsonb('configuration').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// Snapshot 创建后不可变(库内触发器强制,见 0001 迁移手写 SQL);替换文件必须创建新 Snapshot
export const sourceSnapshots = pgTable('source_snapshots', {
id: uuid('id').primaryKey().defaultRandom(),
siteId: uuid('site_id').notNull().references(() => sites.id),
sourceConnectionId: uuid('source_connection_id').notNull().references(() => sourceConnections.id),
kind: sourceSnapshotKind('kind').notNull(),
storagePath: text('storage_path').notNull(),
sha256: text('sha256').notNull(),
byteSize: bigint('byte_size', { mode: 'number' }).notNull(),
adapterVersion: text('adapter_version').notNull(),
capturedAt: timestamp('captured_at', { withTimezone: true }).notNull(),
metadata: jsonb('metadata').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (t) => [uniqueIndex('source_snapshots_uq').on(t.siteId, t.kind, t.sha256)])
export const importJobs = pgTable('import_jobs', {
id: uuid('id').primaryKey().defaultRandom(),
siteId: uuid('site_id').notNull().references(() => sites.id),
sourceSnapshotId: uuid('source_snapshot_id').notNull().references(() => sourceSnapshots.id),
status: importJobStatus('status').notNull(),
mappingVersion: text('mapping_version').notNull(),
idempotencyKey: text('idempotency_key').notNull().unique(),
attempt: integer('attempt').notNull().default(0),
startedAt: timestamp('started_at', { withTimezone: true }),
finishedAt: timestamp('finished_at', { withTimezone: true }),
errorCode: text('error_code'),
errorSummary: text('error_summary'),
reportPath: text('report_path'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const sourceRecords = pgTable('source_records', {
id: uuid('id').primaryKey().defaultRandom(),
siteId: uuid('site_id').notNull().references(() => sites.id),
sourceConnectionId: uuid('source_connection_id').notNull().references(() => sourceConnections.id),
sourceSnapshotId: uuid('source_snapshot_id').notNull().references(() => sourceSnapshots.id),
importJobId: uuid('import_job_id').notNull().references(() => importJobs.id),
sourceType: text('source_type').notNull(),
sourceId: text('source_id').notNull(),
locale: text('locale').notNull(),
sourceUrl: text('source_url'),
rawLocation: text('raw_location').notNull(),
sourceChecksum: text('source_checksum'),
normalizedChecksum: text('normalized_checksum'),
parseStatus: text('parse_status').notNull(),
// 暂无 FK:content_entries 于 Task 8(内容域)落地后再补引用
contentEntryId: uuid('content_entry_id'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
// 跨 Snapshot 稳定映射键 = site_id + source_connection_id + source_type + source_id + locale(snapshot_id 进键以允许跨快照再导入)
}, (t) => [uniqueIndex('source_records_identity_uq').on(t.siteId, t.sourceConnectionId, t.sourceType, t.sourceId, t.locale, t.sourceSnapshotId)])