feat(db): 0005 路由注册表——exact/collision 双键 + NULLS NOT DISTINCT 唯一
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
CREATE TYPE "public"."route_kind" AS ENUM('html', 'asset', 'redirect', 'gone', 'dynamic', 'unresolved');--> statement-breakpoint
|
||||
CREATE TYPE "public"."route_query_policy" AS ENUM('ignore', 'exact', 'preserve', 'dynamic');--> statement-breakpoint
|
||||
CREATE TABLE "route_registry" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"site_id" uuid NOT NULL,
|
||||
"host" text NOT NULL,
|
||||
"source_path_raw" text NOT NULL,
|
||||
"route_path" text NOT NULL,
|
||||
"exact_route_key" text NOT NULL,
|
||||
"collision_key" text NOT NULL,
|
||||
"query_policy" "route_query_policy" NOT NULL,
|
||||
"query_key" text,
|
||||
"source_status" text NOT NULL,
|
||||
"target_status" text NOT NULL,
|
||||
"route_kind" "route_kind" NOT NULL,
|
||||
"target_content_id" uuid,
|
||||
"target_file" text,
|
||||
"redirect_status" integer,
|
||||
"redirect_location" text,
|
||||
"indexable" boolean NOT NULL,
|
||||
"priority" integer NOT NULL,
|
||||
"source_snapshot_id" uuid,
|
||||
"verification_status" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "route_registry" ADD CONSTRAINT "route_registry_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "route_registry" ADD CONSTRAINT "route_registry_source_snapshot_id_source_snapshots_id_fk" FOREIGN KEY ("source_snapshot_id") REFERENCES "public"."source_snapshots"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
-- ↓ 手写追加(drizzle-kit 0.31 不生成 NULLS NOT DISTINCT 子句,schema 层故意的未声明该索引):
|
||||
-- 纯路径唯一——query_key 为 NULL 的路由也参与唯一(spec §5.6 要求数据库级强制,不用哨兵值;
|
||||
-- 默认 NULLS DISTINCT 下两行 NULL 不冲突,挡不住纯路径重复)。
|
||||
CREATE UNIQUE INDEX route_exact_uq ON route_registry (site_id, host, exact_route_key, query_key) NULLS NOT DISTINCT;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
||||
"when": 1792497336007,
|
||||
"tag": "0004_last_victor_mancha",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1792498347954,
|
||||
"tag": "0005_fine_dark_phoenix",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// 聚合导出:Task 6 起按域追加 pgTable 定义(站点域 = 迁移文件 0000_pale_sumo;来源域 = 0001_*;内容域 = 0002_*;分类域 = 0003_*;媒体域 = 0004_*)
|
||||
// 聚合导出:Task 6 起按域追加 pgTable 定义(站点域 = 迁移文件 0000_pale_sumo;来源域 = 0001_*;内容域 = 0002_*;分类域 = 0003_*;媒体域 = 0004_*;路由域 = 0005_*)
|
||||
import { pgTable, pgEnum, uuid, text, integer, bigint, jsonb, timestamp, uniqueIndex, index, boolean, primaryKey, type AnyPgColumn } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const sites = pgTable('sites', {
|
||||
@@ -213,3 +213,44 @@ export const mediaUsages = pgTable('media_usages', {
|
||||
fieldPath: text('field_path').notNull(),
|
||||
sourceUrl: text('source_url').notNull(),
|
||||
}, (t) => [primaryKey({ columns: [t.mediaAssetId, t.contentRevisionId, t.fieldPath] })])
|
||||
|
||||
// ---- 路由域(spec §5.6;迁移文件 0005_*,末尾手写追加 NULLS NOT DISTINCT 唯一索引)----
|
||||
|
||||
export const routeKind = pgEnum('route_kind', ['html', 'asset', 'redirect', 'gone', 'dynamic', 'unresolved'])
|
||||
export const routeQueryPolicy = pgEnum('route_query_policy', ['ignore', 'exact', 'preserve', 'dynamic'])
|
||||
|
||||
// ADR-007 双键:exact_route_key 保留大小写/尾斜杠语义用于请求匹配(真实路由不折叠——
|
||||
// /About 与 /about 是两条路由);collision_key 只折叠给风险检测用(Windows 大小写不敏感、
|
||||
// 尾斜杠、Unicode NFC),永不参与唯一约束。查询路由单独建模(query_key),不与 path 静默合并。
|
||||
export const routeRegistry = pgTable('route_registry', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
siteId: uuid('site_id').notNull().references(() => sites.id),
|
||||
host: text('host').notNull(),
|
||||
// source_path_raw 永远保存来源原文;route_path 是经安全校验的实际公开路径(spec §5.6 规则)
|
||||
sourcePathRaw: text('source_path_raw').notNull(),
|
||||
routePath: text('route_path').notNull(),
|
||||
exactRouteKey: text('exact_route_key').notNull(),
|
||||
collisionKey: text('collision_key').notNull(),
|
||||
queryPolicy: routeQueryPolicy('query_policy').notNull(),
|
||||
// 纯路径路由 query_key 为 NULL——其唯一性靠 0005 迁移手写索引的 NULLS NOT DISTINCT 兜底
|
||||
queryKey: text('query_key'),
|
||||
sourceStatus: text('source_status').notNull(),
|
||||
targetStatus: text('target_status').notNull(),
|
||||
routeKind: routeKind('route_kind').notNull(),
|
||||
// target_content_id 指向 content_entries,但照 spec §5.6 与 Task 11 清单暂不设 FK
|
||||
// (同 source_records.content_entry_id 先例:需要时由后续迁移手写回填)
|
||||
targetContentId: uuid('target_content_id'),
|
||||
targetFile: text('target_file'),
|
||||
redirectStatus: integer('redirect_status'),
|
||||
redirectLocation: text('redirect_location'),
|
||||
indexable: boolean('indexable').notNull(),
|
||||
priority: integer('priority').notNull(),
|
||||
sourceSnapshotId: uuid('source_snapshot_id').references(() => sourceSnapshots.id),
|
||||
verificationStatus: text('verification_status').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
// 注意:唯一索引 route_exact_uq (site_id, host, exact_route_key, query_key) NULLS NOT DISTINCT
|
||||
// 故意的在此不定义——drizzle-kit 0.31 无法生成 NULLS NOT DISTINCT 子句,声明普通唯一索引
|
||||
// 会产出语义错误(默认 NULLS DISTINCT,纯路径路由两行 NULL 不冲突)的迁移。该索引由
|
||||
// 0005 迁移文件末尾手写 SQL 追加,集成测试 db.routes.test.ts 守住其存在与语义。
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user