diff --git a/knowledge-fs/.harness/changes/2026-08-06-logical-document-zero-revision-deletion.md b/knowledge-fs/.harness/changes/2026-08-06-logical-document-zero-revision-deletion.md new file mode 100644 index 00000000000..6872ae5b16d --- /dev/null +++ b/knowledge-fs/.harness/changes/2026-08-06-logical-document-zero-revision-deletion.md @@ -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. diff --git a/knowledge-fs/apps/api/src/migrate.test.ts b/knowledge-fs/apps/api/src/migrate.test.ts index 581338e1a02..e138632eafa 100644 --- a/knowledge-fs/apps/api/src/migrate.test.ts +++ b/knowledge-fs/apps/api/src/migrate.test.ts @@ -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); }); diff --git a/knowledge-fs/packages/adapters/src/migration-runner.test.ts b/knowledge-fs/packages/adapters/src/migration-runner.test.ts index 5a669b80717..6619472a670 100644 --- a/knowledge-fs/packages/adapters/src/migration-runner.test.ts +++ b/knowledge-fs/packages/adapters/src/migration-runner.test.ts @@ -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, diff --git a/knowledge-fs/packages/database/migrations/0037_logical_document_zero_revision_deletion.postgres.sql b/knowledge-fs/packages/database/migrations/0037_logical_document_zero_revision_deletion.postgres.sql new file mode 100644 index 00000000000..a31ff4b23b7 --- /dev/null +++ b/knowledge-fs/packages/database/migrations/0037_logical_document_zero_revision_deletion.postgres.sql @@ -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 + ); diff --git a/knowledge-fs/packages/database/migrations/0037_logical_document_zero_revision_deletion.tidb.sql b/knowledge-fs/packages/database/migrations/0037_logical_document_zero_revision_deletion.tidb.sql new file mode 100644 index 00000000000..6a3454dd9ab --- /dev/null +++ b/knowledge-fs/packages/database/migrations/0037_logical_document_zero_revision_deletion.tidb.sql @@ -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; diff --git a/knowledge-fs/packages/database/src/migration-artifacts.generated.ts b/knowledge-fs/packages/database/src/migration-artifacts.generated.ts index 7c92afcc296..6a6c42391f8 100644 --- a/knowledge-fs/packages/database/src/migration-artifacts.generated.ts +++ b/knowledge-fs/packages/database/src/migration-artifacts.generated.ts @@ -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[]; diff --git a/knowledge-fs/packages/database/src/migration-file.test.ts b/knowledge-fs/packages/database/src/migration-file.test.ts index c8aad984fc7..bde8d545e9b 100644 --- a/knowledge-fs/packages/database/src/migration-file.test.ts +++ b/knowledge-fs/packages/database/src/migration-file.test.ts @@ -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", }), diff --git a/knowledge-fs/packages/database/src/schema.ts b/knowledge-fs/packages/database/src/schema.ts index fcd924e9eb7..6126745a459 100644 --- a/knowledge-fs/packages/database/src/schema.ts +++ b/knowledge-fs/packages/database/src/schema.ts @@ -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", },