fix(knowledge-fs): allow zero-revision logical document deletion

This commit is contained in:
Stephen Zhou 2026-08-06 18:48:55 +08:00
parent fd8e4a80cd
commit b88a8933ed
No known key found for this signature in database
8 changed files with 195 additions and 6 deletions

View File

@ -0,0 +1,37 @@
# Logical Document Zero-Revision Deletion
## What Changed
- Relaxed the durable deletion job and tombstone constraints so only `logical_document` targets may use revision `0`.
- Added PostgreSQL and TiDB migration `0037_logical_document_zero_revision_deletion` and refreshed the checked-in migration artifacts.
- Updated database, adapter migration-runner, and API migration-command tests to include migration `0037`.
## Why
A logical document begins at row version `0` and may fail before its first revision becomes active. The deletion repository already accepts that compare-and-swap value, but the persisted deletion job and tombstone constraints previously required every target revision to be at least `1`.
## TDD Notes
- RED: The new artifact exposed stale adapter and API migration-runner expectations, producing six adapter test failures and one API app test failure.
- GREEN: Added `0037` to both runner test baselines and updated the expected migration count and SQL sequence.
- REFACTOR: Kept the shared migration-id fixture structure and changed no runtime runner behavior.
## Performance Notes
- The change only replaces two existing check constraints and adds no queries, round trips, indexes, or unbounded data paths.
## Verification
- `pnpm --filter @knowledge/database test`: passed (110 tests).
- `pnpm --filter @knowledge/adapters test -- src/migration-runner.test.ts`: passed (105 tests).
- `pnpm --filter @knowledge/api-app test -- src/migrate.test.ts`: passed (210 tests).
- `pnpm typecheck`: passed.
- `pnpm build`: passed.
- `pnpm exec biome check apps/api/src/migrate.test.ts packages/adapters/src/migration-runner.test.ts packages/database/src/schema.ts packages/database/src/migration-file.test.ts packages/database/src/migration-artifacts.generated.ts`: passed.
- `pnpm db:migrations:check`: passed.
- `git diff --check`: passed.
## Known Risks / Follow-Up
- The migration was verified through deterministic artifact and runner tests, not against live PostgreSQL or TiDB instances.
- Full-repository `pnpm lint` remains blocked by ten pre-existing formatting/size findings in unrelated admin, OpenAPI, contract, and test-support files; all files changed in this slice pass targeted Biome checks.

View File

@ -76,8 +76,9 @@ describe("runApiDatabaseMigrations", () => {
"0034_knowledge_space_emoji_icons",
"0035_research_task_answer_streaming",
"0036_page_index_findability",
"0037_logical_document_zero_revision_deletion",
],
pendingBeforeRun: 36,
pendingBeforeRun: 37,
});
expect(operations).toEqual([
"schema",
@ -155,8 +156,10 @@ describe("runApiDatabaseMigrations", () => {
"insert",
"schema",
"insert",
"schema",
"insert",
]);
expect(migrationSql).toHaveLength(36);
expect(migrationSql).toHaveLength(37);
expect(migrationSql[2]).toContain("-- Migration id: 0003_projection_set_publications\n");
expect(migrationSql[2]).toContain("-- Dialect: postgres\n");
expect(migrationSql[2]).toContain('CREATE TABLE IF NOT EXISTS "projection_set_publications"');
@ -204,6 +207,9 @@ describe("runApiDatabaseMigrations", () => {
expect(migrationSql[33]).toContain("-- Migration id: 0034_knowledge_space_emoji_icons\n");
expect(migrationSql[34]).toContain("-- Migration id: 0035_research_task_answer_streaming\n");
expect(migrationSql[35]).toContain("-- Migration id: 0036_page_index_findability\n");
expect(migrationSql[36]).toContain(
"-- Migration id: 0037_logical_document_zero_revision_deletion\n",
);
expect(closed).toBe(true);
});

View File

@ -43,6 +43,8 @@ const researchTaskFinalAnswersMigrationId = "0033_research_task_final_answers";
const knowledgeSpaceEmojiIconsMigrationId = "0034_knowledge_space_emoji_icons";
const researchTaskAnswerStreamingMigrationId = "0035_research_task_answer_streaming";
const pageIndexFindabilityMigrationId = "0036_page_index_findability";
const logicalDocumentZeroRevisionDeletionMigrationId =
"0037_logical_document_zero_revision_deletion";
const migrationsAfterDurableDeletion = [
versionedSpaceProfilesMigrationId,
profilePublicationBindingsMigrationId,
@ -63,6 +65,7 @@ const migrationsAfterDurableDeletion = [
knowledgeSpaceEmojiIconsMigrationId,
researchTaskAnswerStreamingMigrationId,
pageIndexFindabilityMigrationId,
logicalDocumentZeroRevisionDeletionMigrationId,
] as const;
const migrationsAfterTidbBaselineRepair = [
spaceAccessControlMigrationId,

View File

@ -0,0 +1,32 @@
-- Knowledge Platform schema migration
-- Migration id: 0037_logical_document_zero_revision_deletion
-- Dialect: postgres
-- A logical document starts at row_version 0 and can fail before its first revision is activated.
-- Durable deletion already accepts that CAS value; keep other target revisions strictly positive.
ALTER TABLE "deletion_jobs"
DROP CONSTRAINT IF EXISTS "deletion_jobs_positive_ck";
ALTER TABLE "deletion_jobs"
ADD CONSTRAINT "deletion_jobs_positive_ck" CHECK (
(
("target_type" = 'logical_document' AND "target_revision" >= 0)
OR ("target_type" <> 'logical_document' AND "target_revision" >= 1)
)
AND ("capability_grant_id" IS NOT NULL OR "permission_snapshot_revision" >= 1)
AND "row_version" >= 1
AND "execution_attempts" >= 0
AND "max_execution_attempts" >= 1
AND "execution_attempts" <= "max_execution_attempts"
AND ("active_slot" IS NULL OR "active_slot" = 1)
);
ALTER TABLE "deletion_tombstones"
DROP CONSTRAINT IF EXISTS "deletion_tombstones_positive_ck";
ALTER TABLE "deletion_tombstones"
ADD CONSTRAINT "deletion_tombstones_positive_ck" CHECK (
(
("target_type" = 'logical_document' AND "target_revision" >= 0)
OR ("target_type" <> 'logical_document' AND "target_revision" >= 1)
)
AND "row_version" >= 1
);

View File

@ -0,0 +1,71 @@
-- Knowledge Platform schema migration
-- Migration id: 0037_logical_document_zero_revision_deletion
-- Dialect: tidb
-- A logical document starts at row_version 0 and can fail before its first revision is activated.
-- Durable deletion already accepts that CAS value; keep other target revisions strictly positive.
-- TiDB DDL may commit before the migration marker. Guard each independent schema change so a
-- crash after either DROP can resume with the ADD, and marker loss after both ADDs can replay.
SET @kfs_0037_deletion_jobs_positive_sql = IF(
EXISTS(
SELECT 1
FROM information_schema.tidb_check_constraints
WHERE constraint_schema = DATABASE()
AND table_name = 'deletion_jobs'
AND constraint_name = 'deletion_jobs_positive_ck'
),
'ALTER TABLE `deletion_jobs` DROP CHECK `deletion_jobs_positive_ck`',
'DO 0'
);
PREPARE kfs_0037_deletion_jobs_positive_stmt
FROM @kfs_0037_deletion_jobs_positive_sql;
EXECUTE kfs_0037_deletion_jobs_positive_stmt;
DEALLOCATE PREPARE kfs_0037_deletion_jobs_positive_stmt;
SET @kfs_0037_deletion_jobs_positive_sql = IF(
EXISTS(
SELECT 1
FROM information_schema.tidb_check_constraints
WHERE constraint_schema = DATABASE()
AND table_name = 'deletion_jobs'
AND constraint_name = 'deletion_jobs_positive_ck'
),
'DO 0',
'ALTER TABLE `deletion_jobs` ADD CONSTRAINT `deletion_jobs_positive_ck` CHECK (((`target_type` = ''logical_document'' AND `target_revision` >= 0) OR (`target_type` <> ''logical_document'' AND `target_revision` >= 1)) AND (`capability_grant_id` IS NOT NULL OR `permission_snapshot_revision` >= 1) AND `row_version` >= 1 AND `execution_attempts` >= 0 AND `max_execution_attempts` >= 1 AND `execution_attempts` <= `max_execution_attempts` AND (`active_slot` IS NULL OR `active_slot` = 1))'
);
PREPARE kfs_0037_deletion_jobs_positive_stmt
FROM @kfs_0037_deletion_jobs_positive_sql;
EXECUTE kfs_0037_deletion_jobs_positive_stmt;
DEALLOCATE PREPARE kfs_0037_deletion_jobs_positive_stmt;
SET @kfs_0037_deletion_tombstones_positive_sql = IF(
EXISTS(
SELECT 1
FROM information_schema.tidb_check_constraints
WHERE constraint_schema = DATABASE()
AND table_name = 'deletion_tombstones'
AND constraint_name = 'deletion_tombstones_positive_ck'
),
'ALTER TABLE `deletion_tombstones` DROP CHECK `deletion_tombstones_positive_ck`',
'DO 0'
);
PREPARE kfs_0037_deletion_tombstones_positive_stmt
FROM @kfs_0037_deletion_tombstones_positive_sql;
EXECUTE kfs_0037_deletion_tombstones_positive_stmt;
DEALLOCATE PREPARE kfs_0037_deletion_tombstones_positive_stmt;
SET @kfs_0037_deletion_tombstones_positive_sql = IF(
EXISTS(
SELECT 1
FROM information_schema.tidb_check_constraints
WHERE constraint_schema = DATABASE()
AND table_name = 'deletion_tombstones'
AND constraint_name = 'deletion_tombstones_positive_ck'
),
'DO 0',
'ALTER TABLE `deletion_tombstones` ADD CONSTRAINT `deletion_tombstones_positive_ck` CHECK (((`target_type` = ''logical_document'' AND `target_revision` >= 0) OR (`target_type` <> ''logical_document'' AND `target_revision` >= 1)) AND `row_version` >= 1)'
);
PREPARE kfs_0037_deletion_tombstones_positive_stmt
FROM @kfs_0037_deletion_tombstones_positive_sql;
EXECUTE kfs_0037_deletion_tombstones_positive_stmt;
DEALLOCATE PREPARE kfs_0037_deletion_tombstones_positive_stmt;

View File

@ -75,4 +75,6 @@ export const migrationArtifacts = [
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0035_research_task_answer_streaming\n-- Dialect: tidb\n-- Allows durable Research progress streams to carry bounded, batched answer deltas.\n-- TiDB DDL may commit before the migration marker, so both replacement steps are replay safe.\n\nSET @kfs_0035_progress_event_constraint_sql = IF(\n EXISTS(\n SELECT 1\n FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'research_task_progress_events'\n AND constraint_name = 'research_task_progress_event_ck'\n ),\n 'ALTER TABLE `research_task_progress_events` DROP CHECK `research_task_progress_event_ck`',\n 'DO 0'\n);\nPREPARE kfs_0035_progress_event_constraint_stmt\n FROM @kfs_0035_progress_event_constraint_sql;\nEXECUTE kfs_0035_progress_event_constraint_stmt;\nDEALLOCATE PREPARE kfs_0035_progress_event_constraint_stmt;\n\nSET @kfs_0035_progress_event_constraint_sql = IF(\n EXISTS(\n SELECT 1\n FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'research_task_progress_events'\n AND constraint_name = 'research_task_progress_event_ck'\n ),\n 'DO 0',\n 'ALTER TABLE `research_task_progress_events` ADD CONSTRAINT `research_task_progress_event_ck` CHECK (`event_type` IN (''research_task.answer_delta'', ''research_task.canceled'', ''research_task.failed'', ''research_task.paused'', ''research_task.resumed'', ''research_task.stage_changed'', ''research_task.started''))'\n);\nPREPARE kfs_0035_progress_event_constraint_stmt\n FROM @kfs_0035_progress_event_constraint_sql;\nEXECUTE kfs_0035_progress_event_constraint_stmt;\nDEALLOCATE PREPARE kfs_0035_progress_event_constraint_stmt;\n", path: "packages/database/migrations/0035_research_task_answer_streaming.tidb.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0036_page_index_findability\n-- Dialect: postgres\n-- Persists generation-scoped PageIndex navigation quality and a bounded summary-repair request.\n\nCREATE TABLE IF NOT EXISTS \"page_index_findability_evaluations\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"tenant_id\" VARCHAR(255) NOT NULL,\n \"knowledge_space_id\" UUID NOT NULL,\n \"document_asset_id\" UUID NOT NULL,\n \"document_version\" BIGINT NOT NULL,\n \"outline_id\" UUID NOT NULL,\n \"publication_generation_id\" UUID NOT NULL,\n \"publication_fingerprint\" VARCHAR(96) NOT NULL,\n \"compilation_attempt_id\" UUID NOT NULL,\n \"evaluator_version\" VARCHAR(128) NOT NULL,\n \"status\" VARCHAR(32) NOT NULL,\n \"recommended_route\" VARCHAR(16) NOT NULL,\n \"evaluation\" JSONB NOT NULL,\n \"summary_repair_state\" VARCHAR(24) NOT NULL,\n \"summary_repair_attempts\" INTEGER NOT NULL DEFAULT 0,\n \"summary_repair_error\" VARCHAR(2000),\n \"available_at\" TIMESTAMPTZ,\n \"lock_token\" UUID,\n \"locked_by\" VARCHAR(255),\n \"lease_expires_at\" TIMESTAMPTZ,\n \"evaluated_at\" TIMESTAMPTZ NOT NULL,\n \"updated_at\" TIMESTAMPTZ NOT NULL,\n CONSTRAINT \"page_index_findability_scope_fk\"\n FOREIGN KEY (\"tenant_id\", \"knowledge_space_id\")\n REFERENCES \"knowledge_spaces\" (\"tenant_id\", \"id\") ON DELETE CASCADE,\n CONSTRAINT \"page_index_findability_status_ck\"\n CHECK (\"status\" IN ('failed', 'not-evaluated', 'passed')),\n CONSTRAINT \"page_index_findability_route_ck\"\n CHECK (\"recommended_route\" IN ('hybrid', 'layered', 'unchanged')),\n CONSTRAINT \"page_index_findability_repair_ck\"\n CHECK (\n \"summary_repair_state\" IN ('not-requested', 'queued', 'leased', 'dispatched', 'failed')\n AND \"summary_repair_attempts\" >= 0\n AND (\n (\"summary_repair_state\" = 'leased'\n AND \"lock_token\" IS NOT NULL\n AND \"locked_by\" IS NOT NULL\n AND \"lease_expires_at\" IS NOT NULL)\n OR\n (\"summary_repair_state\" <> 'leased'\n AND \"lock_token\" IS NULL\n AND \"locked_by\" IS NULL\n AND \"lease_expires_at\" IS NULL)\n )\n ),\n CONSTRAINT \"page_index_findability_evaluation_ck\"\n CHECK (jsonb_typeof(\"evaluation\") = 'object'),\n CONSTRAINT \"page_index_findability_document_version_ck\"\n CHECK (\"document_version\" >= 1)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS \"page_index_findability_generation_evaluator_uq\"\n ON \"page_index_findability_evaluations\" (\n \"tenant_id\", \"knowledge_space_id\", \"publication_generation_id\", \"evaluator_version\"\n );\nCREATE INDEX IF NOT EXISTS \"page_index_findability_route_idx\"\n ON \"page_index_findability_evaluations\" (\n \"tenant_id\", \"knowledge_space_id\", \"document_asset_id\", \"publication_generation_id\"\n );\nCREATE INDEX IF NOT EXISTS \"page_index_findability_repair_queue_idx\"\n ON \"page_index_findability_evaluations\" (\n \"summary_repair_state\", \"available_at\", \"lease_expires_at\", \"updated_at\", \"id\"\n );\n", path: "packages/database/migrations/0036_page_index_findability.postgres.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0036_page_index_findability\n-- Dialect: tidb\n-- Persists generation-scoped PageIndex navigation quality and a bounded summary-repair request.\n\nCREATE TABLE IF NOT EXISTS `page_index_findability_evaluations` (\n `id` CHAR(36) PRIMARY KEY NOT NULL,\n `tenant_id` VARCHAR(255) NOT NULL,\n `knowledge_space_id` CHAR(36) NOT NULL,\n `document_asset_id` CHAR(36) NOT NULL,\n `document_version` BIGINT NOT NULL,\n `outline_id` CHAR(36) NOT NULL,\n `publication_generation_id` CHAR(36) NOT NULL,\n `publication_fingerprint` VARCHAR(96) NOT NULL,\n `compilation_attempt_id` CHAR(36) NOT NULL,\n `evaluator_version` VARCHAR(128) NOT NULL,\n `status` VARCHAR(32) NOT NULL,\n `recommended_route` VARCHAR(16) NOT NULL,\n `evaluation` JSON NOT NULL,\n `summary_repair_state` VARCHAR(24) NOT NULL,\n `summary_repair_attempts` INT NOT NULL DEFAULT 0,\n `summary_repair_error` VARCHAR(2000),\n `available_at` DATETIME(3),\n `lock_token` CHAR(36),\n `locked_by` VARCHAR(255),\n `lease_expires_at` DATETIME(3),\n `evaluated_at` DATETIME(3) NOT NULL,\n `updated_at` DATETIME(3) NOT NULL,\n CONSTRAINT `page_index_findability_scope_fk`\n FOREIGN KEY (`tenant_id`, `knowledge_space_id`)\n REFERENCES `knowledge_spaces` (`tenant_id`, `id`) ON DELETE CASCADE,\n CONSTRAINT `page_index_findability_status_ck`\n CHECK (`status` IN ('failed', 'not-evaluated', 'passed')),\n CONSTRAINT `page_index_findability_route_ck`\n CHECK (`recommended_route` IN ('hybrid', 'layered', 'unchanged')),\n CONSTRAINT `page_index_findability_repair_ck`\n CHECK (\n `summary_repair_state` IN ('not-requested', 'queued', 'leased', 'dispatched', 'failed')\n AND `summary_repair_attempts` >= 0\n AND (\n (`summary_repair_state` = 'leased'\n AND `lock_token` IS NOT NULL\n AND `locked_by` IS NOT NULL\n AND `lease_expires_at` IS NOT NULL)\n OR\n (`summary_repair_state` <> 'leased'\n AND `lock_token` IS NULL\n AND `locked_by` IS NULL\n AND `lease_expires_at` IS NULL)\n )\n ),\n CONSTRAINT `page_index_findability_evaluation_ck`\n CHECK (JSON_TYPE(`evaluation`) = 'OBJECT'),\n CONSTRAINT `page_index_findability_document_version_ck`\n CHECK (`document_version` >= 1)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS `page_index_findability_generation_evaluator_uq`\n ON `page_index_findability_evaluations` (\n `tenant_id`, `knowledge_space_id`, `publication_generation_id`, `evaluator_version`\n );\nCREATE INDEX IF NOT EXISTS `page_index_findability_route_idx`\n ON `page_index_findability_evaluations` (\n `tenant_id`, `knowledge_space_id`, `document_asset_id`, `publication_generation_id`\n );\nCREATE INDEX IF NOT EXISTS `page_index_findability_repair_queue_idx`\n ON `page_index_findability_evaluations` (\n `summary_repair_state`, `available_at`, `lease_expires_at`, `updated_at`, `id`\n );\n", path: "packages/database/migrations/0036_page_index_findability.tidb.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0037_logical_document_zero_revision_deletion\n-- Dialect: postgres\n\n-- A logical document starts at row_version 0 and can fail before its first revision is activated.\n-- Durable deletion already accepts that CAS value; keep other target revisions strictly positive.\nALTER TABLE \"deletion_jobs\"\n DROP CONSTRAINT IF EXISTS \"deletion_jobs_positive_ck\";\nALTER TABLE \"deletion_jobs\"\n ADD CONSTRAINT \"deletion_jobs_positive_ck\" CHECK (\n (\n (\"target_type\" = 'logical_document' AND \"target_revision\" >= 0)\n OR (\"target_type\" <> 'logical_document' AND \"target_revision\" >= 1)\n )\n AND (\"capability_grant_id\" IS NOT NULL OR \"permission_snapshot_revision\" >= 1)\n AND \"row_version\" >= 1\n AND \"execution_attempts\" >= 0\n AND \"max_execution_attempts\" >= 1\n AND \"execution_attempts\" <= \"max_execution_attempts\"\n AND (\"active_slot\" IS NULL OR \"active_slot\" = 1)\n );\n\nALTER TABLE \"deletion_tombstones\"\n DROP CONSTRAINT IF EXISTS \"deletion_tombstones_positive_ck\";\nALTER TABLE \"deletion_tombstones\"\n ADD CONSTRAINT \"deletion_tombstones_positive_ck\" CHECK (\n (\n (\"target_type\" = 'logical_document' AND \"target_revision\" >= 0)\n OR (\"target_type\" <> 'logical_document' AND \"target_revision\" >= 1)\n )\n AND \"row_version\" >= 1\n );\n", path: "packages/database/migrations/0037_logical_document_zero_revision_deletion.postgres.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0037_logical_document_zero_revision_deletion\n-- Dialect: tidb\n\n-- A logical document starts at row_version 0 and can fail before its first revision is activated.\n-- Durable deletion already accepts that CAS value; keep other target revisions strictly positive.\n-- TiDB DDL may commit before the migration marker. Guard each independent schema change so a\n-- crash after either DROP can resume with the ADD, and marker loss after both ADDs can replay.\nSET @kfs_0037_deletion_jobs_positive_sql = IF(\n EXISTS(\n SELECT 1\n FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'deletion_jobs'\n AND constraint_name = 'deletion_jobs_positive_ck'\n ),\n 'ALTER TABLE `deletion_jobs` DROP CHECK `deletion_jobs_positive_ck`',\n 'DO 0'\n);\nPREPARE kfs_0037_deletion_jobs_positive_stmt\n FROM @kfs_0037_deletion_jobs_positive_sql;\nEXECUTE kfs_0037_deletion_jobs_positive_stmt;\nDEALLOCATE PREPARE kfs_0037_deletion_jobs_positive_stmt;\n\nSET @kfs_0037_deletion_jobs_positive_sql = IF(\n EXISTS(\n SELECT 1\n FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'deletion_jobs'\n AND constraint_name = 'deletion_jobs_positive_ck'\n ),\n 'DO 0',\n 'ALTER TABLE `deletion_jobs` ADD CONSTRAINT `deletion_jobs_positive_ck` CHECK (((`target_type` = ''logical_document'' AND `target_revision` >= 0) OR (`target_type` <> ''logical_document'' AND `target_revision` >= 1)) AND (`capability_grant_id` IS NOT NULL OR `permission_snapshot_revision` >= 1) AND `row_version` >= 1 AND `execution_attempts` >= 0 AND `max_execution_attempts` >= 1 AND `execution_attempts` <= `max_execution_attempts` AND (`active_slot` IS NULL OR `active_slot` = 1))'\n);\nPREPARE kfs_0037_deletion_jobs_positive_stmt\n FROM @kfs_0037_deletion_jobs_positive_sql;\nEXECUTE kfs_0037_deletion_jobs_positive_stmt;\nDEALLOCATE PREPARE kfs_0037_deletion_jobs_positive_stmt;\n\nSET @kfs_0037_deletion_tombstones_positive_sql = IF(\n EXISTS(\n SELECT 1\n FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'deletion_tombstones'\n AND constraint_name = 'deletion_tombstones_positive_ck'\n ),\n 'ALTER TABLE `deletion_tombstones` DROP CHECK `deletion_tombstones_positive_ck`',\n 'DO 0'\n);\nPREPARE kfs_0037_deletion_tombstones_positive_stmt\n FROM @kfs_0037_deletion_tombstones_positive_sql;\nEXECUTE kfs_0037_deletion_tombstones_positive_stmt;\nDEALLOCATE PREPARE kfs_0037_deletion_tombstones_positive_stmt;\n\nSET @kfs_0037_deletion_tombstones_positive_sql = IF(\n EXISTS(\n SELECT 1\n FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'deletion_tombstones'\n AND constraint_name = 'deletion_tombstones_positive_ck'\n ),\n 'DO 0',\n 'ALTER TABLE `deletion_tombstones` ADD CONSTRAINT `deletion_tombstones_positive_ck` CHECK (((`target_type` = ''logical_document'' AND `target_revision` >= 0) OR (`target_type` <> ''logical_document'' AND `target_revision` >= 1)) AND `row_version` >= 1)'\n);\nPREPARE kfs_0037_deletion_tombstones_positive_stmt\n FROM @kfs_0037_deletion_tombstones_positive_sql;\nEXECUTE kfs_0037_deletion_tombstones_positive_stmt;\nDEALLOCATE PREPARE kfs_0037_deletion_tombstones_positive_stmt;\n", path: "packages/database/migrations/0037_logical_document_zero_revision_deletion.tidb.sql" },
] as const satisfies readonly MigrationArtifact[];

View File

@ -132,6 +132,8 @@ describe("migration file rendering", () => {
"packages/database/migrations/0035_research_task_answer_streaming.tidb.sql",
"packages/database/migrations/0036_page_index_findability.postgres.sql",
"packages/database/migrations/0036_page_index_findability.tidb.sql",
"packages/database/migrations/0037_logical_document_zero_revision_deletion.postgres.sql",
"packages/database/migrations/0037_logical_document_zero_revision_deletion.tidb.sql",
]);
expect(artifacts[2]?.content).toContain('ALTER COLUMN "dense_vector" TYPE vector');
expect(artifacts[2]?.content).not.toContain("vector(1536)");
@ -627,6 +629,39 @@ describe("migration file rendering", () => {
);
});
it("allows zero revisions only for logical-document deletion records", () => {
const artifacts = getDatabaseMigrationArtifacts();
for (const dialect of ["postgres", "tidb"] as const) {
const migration = artifacts.find((artifact) =>
artifact.path.endsWith(`0037_logical_document_zero_revision_deletion.${dialect}.sql`),
)?.content;
expect(migration).toContain("deletion_jobs_positive_ck");
expect(migration).toContain("deletion_tombstones_positive_ck");
expect(migration).toContain("target_type");
expect(migration).toContain("logical_document");
expect(migration).toContain("target_revision");
expect(migration).toContain(">= 0");
expect(migration).toContain(
dialect === "tidb" ? "<> ''logical_document''" : "<> 'logical_document'",
);
expect(migration).toContain(">= 1");
}
const tidb = artifacts.find((artifact) =>
artifact.path.endsWith("0037_logical_document_zero_revision_deletion.tidb.sql"),
)?.content;
expect(tidb?.match(/FROM information_schema\.tidb_check_constraints/gu)).toHaveLength(4);
expect(tidb?.match(/'ALTER TABLE `deletion_(?:jobs|tombstones)` DROP CHECK/gu)).toHaveLength(2);
expect(
tidb?.match(/'ALTER TABLE `deletion_(?:jobs|tombstones)` ADD CONSTRAINT/gu),
).toHaveLength(2);
expect(tidb).not.toMatch(
/ALTER TABLE `deletion_(?:jobs|tombstones)`\s+DROP CONSTRAINT[^;]+ADD CONSTRAINT/gu,
);
});
it("keeps migration 0015 safe to replay after DDL commits before its marker", () => {
const artifacts = getDatabaseMigrationArtifacts();
const postgres = artifacts.find(
@ -817,6 +852,7 @@ describe("migration file rendering", () => {
"packages/database/migrations/0034_knowledge_space_emoji_icons.postgres.sql",
"packages/database/migrations/0035_research_task_answer_streaming.postgres.sql",
"packages/database/migrations/0036_page_index_findability.postgres.sql",
"packages/database/migrations/0037_logical_document_zero_revision_deletion.postgres.sql",
]);
expect(
getPendingMigrationArtifacts({
@ -857,6 +893,7 @@ describe("migration file rendering", () => {
"0034_knowledge_space_emoji_icons",
"0035_research_task_answer_streaming",
"0036_page_index_findability",
"0037_logical_document_zero_revision_deletion",
],
dialect: "postgres",
}),

View File

@ -3395,8 +3395,8 @@ const tables = [
{
expression: {
postgres:
'"target_revision" >= 1 AND ("capability_grant_id" IS NOT NULL OR "permission_snapshot_revision" >= 1) AND "row_version" >= 1 AND "execution_attempts" >= 0 AND "max_execution_attempts" >= 1 AND "execution_attempts" <= "max_execution_attempts" AND ("active_slot" IS NULL OR "active_slot" = 1)',
tidb: "`target_revision` >= 1 AND (`capability_grant_id` IS NOT NULL OR `permission_snapshot_revision` >= 1) AND `row_version` >= 1 AND `execution_attempts` >= 0 AND `max_execution_attempts` >= 1 AND `execution_attempts` <= `max_execution_attempts` AND (`active_slot` IS NULL OR `active_slot` = 1)",
'(("target_type" = \'logical_document\' AND "target_revision" >= 0) OR ("target_type" <> \'logical_document\' AND "target_revision" >= 1)) AND ("capability_grant_id" IS NOT NULL OR "permission_snapshot_revision" >= 1) AND "row_version" >= 1 AND "execution_attempts" >= 0 AND "max_execution_attempts" >= 1 AND "execution_attempts" <= "max_execution_attempts" AND ("active_slot" IS NULL OR "active_slot" = 1)',
tidb: "((`target_type` = 'logical_document' AND `target_revision` >= 0) OR (`target_type` <> 'logical_document' AND `target_revision` >= 1)) AND (`capability_grant_id` IS NOT NULL OR `permission_snapshot_revision` >= 1) AND `row_version` >= 1 AND `execution_attempts` >= 0 AND `max_execution_attempts` >= 1 AND `execution_attempts` <= `max_execution_attempts` AND (`active_slot` IS NULL OR `active_slot` = 1)",
},
name: "deletion_jobs_positive_ck",
},
@ -3512,8 +3512,9 @@ const tables = [
},
{
expression: {
postgres: '"target_revision" >= 1 AND "row_version" >= 1',
tidb: "`target_revision` >= 1 AND `row_version` >= 1",
postgres:
'(("target_type" = \'logical_document\' AND "target_revision" >= 0) OR ("target_type" <> \'logical_document\' AND "target_revision" >= 1)) AND "row_version" >= 1',
tidb: "((`target_type` = 'logical_document' AND `target_revision` >= 0) OR (`target_type` <> 'logical_document' AND `target_revision` >= 1)) AND `row_version` >= 1",
},
name: "deletion_tombstones_positive_ck",
},