update
Deploy / deploy (push) Canceled after 0s

This commit is contained in:
xuyue committed 2026-09-27 19:36:28 +08:00
1 parent 23844ae301
commit d25b5dbe6d
268 files changed
+3684 -8861

No files matched your search

+1 -3
View File
@@ -15,9 +15,7 @@ export const db = drizzle(client, { schema })
* 别再写 `tx as unknown as typeof db` —— 那个断言把两者的差异(事务上没有
* `$client`、`batch` 等)一并抹掉了。
*/
export type DbOrTx =
| typeof db
| Parameters<Parameters<typeof db.transaction>[0]>[0]
export type DbOrTx = typeof db | Parameters<Parameters<typeof db.transaction>[0]>[0]
/**
* 读出来的时刻统一成 ISO 8601 UTC,和写侧的 `new Date().toISOString()` 同形状。
+6 -20
View File
@@ -57,9 +57,7 @@ export async function runMigrations() {
process.exit(2)
}
if (files.length === 0) {
console.error(
`${migrationsDir} 下没找到任何迁移。镜像里的迁移目录是不是漏拷了?`,
)
console.error(`${migrationsDir} 下没找到任何迁移。镜像里的迁移目录是不是漏拷了?`)
process.exit(2)
}
@@ -138,16 +136,10 @@ export async function runMigrations() {
// 自举时不拦:空库上没有数据可丢,0002 那串 DROP ... IF EXISTS 全是空转。
// 拦下来只会逼着每个新环境都带一次 OJ2_ALLOW_DESTRUCTIVE,把这道闸训练成习惯动作 ——
// 那正是它想避免的事。
if (
blocked.length > 0 &&
!bootstrapping &&
process.env.OJ2_ALLOW_DESTRUCTIVE !== "1"
) {
if (blocked.length > 0 && !bootstrapping && process.env.OJ2_ALLOW_DESTRUCTIVE !== "1") {
console.error(
"待执行的迁移里有破坏性语句,已停下:\n" +
blocked
.map(({ tag, reasons }) => ` · ${tag}:${reasons.join(" / ")}`)
.join("\n") +
blocked.map(({ tag, reasons }) => ` · ${tag}:${reasons.join(" / ")}`).join("\n") +
"\n\n这类改动不可逆,不该在一次日常部署里顺手执行。" +
"\n确认已经做过备份之后,用这个显式放行:\n\n" +
" OJ2_ALLOW_DESTRUCTIVE=1 docker/deploy.sh\n",
@@ -188,9 +180,7 @@ export async function runMigrations() {
function destructiveReasons(sql: string) {
const bare = stripComments(sql)
return DESTRUCTIVE_PATTERNS.filter(([re]) => re.test(bare)).map(
([, label]) => label,
)
return DESTRUCTIVE_PATTERNS.filter(([re]) => re.test(bare)).map(([, label]) => label)
}
/**
@@ -202,9 +192,7 @@ function destructiveReasons(sql: string) {
*/
function readMigrationTags(): Map<number, string> {
try {
const journal = JSON.parse(
readFileSync(`${migrationsDir}/meta/_journal.json`, "utf8"),
) as {
const journal = JSON.parse(readFileSync(`${migrationsDir}/meta/_journal.json`, "utf8")) as {
entries?: Array<{ when: number; tag: string }>
}
return new Map((journal.entries ?? []).map((e) => [e.when, e.tag]))
@@ -246,9 +234,7 @@ async function applyMigration(
) {
// 只留有可执行内容的段。`readMigrationFiles` 按 `--> statement-breakpoint` 切开后
// 保留原文,所以纯注释段(比如 0002 开头那一大段说明)会自成一段。
const statements = migration.sql.filter(
(stmt) => stripComments(stmt).trim() !== "",
)
const statements = migration.sql.filter((stmt) => stripComments(stmt).trim() !== "")
if (statements.length === 0) {
// 上游已经拦过一次(那条兜底检查),走到这里说明拦漏了,宁可响一声也别静默跳过
throw new Error(`${tag} 没有任何可执行语句`)
+12 -47
View File
@@ -250,17 +250,11 @@ export const flowchartSubmission = pgTable(
table.problemId.asc().nullsLast().op("int4_ops"),
table.createTime.asc().nullsLast().op("int4_ops"),
),
index("flowchart_status_idx").using(
"btree",
table.status.asc().nullsLast().op("int4_ops"),
),
index("flowchart_status_idx").using("btree", table.status.asc().nullsLast().op("int4_ops")),
// 流程图列表分页。原来是 hash join 全表再 top-N 排序(4.5ms / 551 buffers),
// 走这条之后 0.19ms / 47。绝对值不大,但索引只要 64kB,而这张表每行带一大坨
// jsonb,行数涨上去是线性恶化的。ASC 反向扫,理由同 submission 那几条。
index("flowchart_create_time_idx").using(
"btree",
table.createTime.asc().nullsLast(),
),
index("flowchart_create_time_idx").using("btree", table.createTime.asc().nullsLast()),
index("flowchart_user_time_idx").using(
"btree",
table.userId.asc().nullsLast().op("int4_ops"),
@@ -520,10 +514,7 @@ export const problemsetProgress = pgTable(
foreignColumns: [user.id],
name: "problemset_progress_user_id_c8041a80_fk_user_id",
}),
unique("unique_problemset_progress_user").on(
table.problemsetId,
table.userId,
),
unique("unique_problemset_progress_user").on(table.problemsetId, table.userId),
],
)
@@ -662,13 +653,9 @@ export const problem = pgTable(
difficulty: text().notNull().$type<ProblemDifficulty>(),
source: text(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
submissionNumber: bigint("submission_number", { mode: "number" })
.default(0)
.notNull(),
submissionNumber: bigint("submission_number", { mode: "number" }).default(0).notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
acceptedNumber: bigint("accepted_number", { mode: "number" })
.default(0)
.notNull(),
acceptedNumber: bigint("accepted_number", { mode: "number" }).default(0).notNull(),
createdById: integer("created_by_id").notNull(),
displayId: text("_id").notNull(),
statisticInfo: jsonb("statistic_info").default({}).notNull(),
@@ -702,10 +689,7 @@ export const problem = pgTable(
"btree",
table.createdById.asc().nullsLast().op("int4_ops"),
),
index("problem_visible_idx").using(
"btree",
table.visible.asc().nullsLast().op("bool_ops"),
),
index("problem_visible_idx").using("btree", table.visible.asc().nullsLast().op("bool_ops")),
foreignKey({
columns: [table.contestId],
foreignColumns: [contest.id],
@@ -755,9 +739,7 @@ export const problemTag = pgTable(
id: serial().primaryKey().notNull(),
name: text().notNull(),
},
() => [
uniqueIndex("problem_tag_name_ci_unique").using("btree", sql`lower(name)`),
],
() => [uniqueIndex("problem_tag_name_ci_unique").using("btree", sql`lower(name)`)],
)
export const submission = pgTable(
@@ -819,11 +801,7 @@ export const submission = pgTable(
// 两列同为 ASC 时整条索引反着扫就是精确的反序,所以反而是能用的那一种。
// 这两列都 NOT NULL,nulls 位置在语义上无所谓,纯粹是规划器的匹配规则。
index("submission_public_create_time_id_idx")
.using(
"btree",
table.createTime.asc().nullsLast(),
table.id.asc().nullsLast(),
)
.using("btree", table.createTime.asc().nullsLast(), table.id.asc().nullsLast())
.where(sql`${table.contestId} is null`),
/**
* Django 给每个外键都自动建了一个单列索引,`db_index=True` 的还会多一个
@@ -870,18 +848,10 @@ export const submission = pgTable(
* 写成 DESC NULLS LAST 规划器直接不认这条索引,回落到分页索引带 Filter。
*/
index("submission_language_time_idx")
.using(
"btree",
table.language.asc().nullsLast(),
table.createTime.asc().nullsLast(),
)
.using("btree", table.language.asc().nullsLast(), table.createTime.asc().nullsLast())
.where(sql`${table.contestId} is null`),
index("submission_result_time_idx")
.using(
"btree",
table.result.asc().nullsLast(),
table.createTime.asc().nullsLast(),
)
.using("btree", table.result.asc().nullsLast(), table.createTime.asc().nullsLast())
.where(sql`${table.contestId} is null`),
/**
* 提交列表的「题号」筛选。路由先把题号解析成 problem.id(见 routes/submission.ts 的
@@ -1318,9 +1288,7 @@ export const user = pgTable(
// 里的角色名也受类型检查。运行时的兜底仍在 auth/session.ts 的 toAdminType。
adminType: text("admin_type").notNull().$type<AdminType>(),
isDisabled: boolean("is_disabled").default(false).notNull(),
problemPermission: text("problem_permission")
.notNull()
.$type<ProblemPermission>(),
problemPermission: text("problem_permission").notNull().$type<ProblemPermission>(),
rawPassword: varchar("raw_password", { length: 20 }),
className: text("class_name"),
},
@@ -1335,10 +1303,7 @@ export const user = pgTable(
),
// 按班级 / 按年级(class_name like '241%')取学生:班级榜、班级对比、AI 学情的
// 排名 scope 都走它,见 routes/classroom.ts 的 loadClassUsers。
index("user_class_name_idx").using(
"btree",
table.className.asc().nullsLast(),
),
index("user_class_name_idx").using("btree", table.className.asc().nullsLast()),
],
)