From 6f117e61fa4045368a60eefd9051aa7991549cba Mon Sep 17 00:00:00 2001 From: Thomas Jung Date: Mon, 2 Nov 2026 20:43:40 -0600 Subject: [PATCH] =?UTF-8?q?revert(db):=20remove=20draft=20cascade-constrai?= =?UTF-8?q?nt=20patch=20=E2=80=94=20unneeded=20while=20dad=20delete=20is?= =?UTF-8?q?=20off,=20blocking=20deploy=20(#2993)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #2993 build patch generated ON DELETE CASCADE constraints for draft tables to support CAP 10.2's optimized draft deletion (dad strategy / cds.fiori.draft_admin_integrity). But: 1. The constraints are ONLY needed when draft_admin_integrity is ON. It is OFF (#2992) and staying off — orphan cleanup is handled by scripts/cleanup-draft-orphans.cjs (#2994) instead. So the constraints provide zero value right now. 2. Both delivery forms failed HDI: inline-in-.hdbtable (error 8250014), then standalone .hdbconstraint (error 8212001 — the ON clause needs the base .hdbtable in the same make graph, which a delta deploy doesn't provide). Making it work is unresolved HDI-delta research. 3. It aborts tutorials-db-deployer on every deploy, blocking urgent PROD fixes (the #2909 draft-edit fix + #2995/#3000 health-gate fixes) from reaching DEV and then prod. Remove the build-time patch entirely: the .deploy/mta.yaml builder line, the script, and its unit test. Re-enabling the dad strategy later is tracked by #2993 and requires solving the HDI make-graph dependency first (likely a CAP-native constraint mechanism, not a hand-rolled patch). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .deploy/mta.yaml | 3 - scripts/patch-draft-cascade-constraints.cjs | 123 ------------------ .../patch-draft-cascade-constraints.test.js | 85 ------------ 3 files changed, 211 deletions(-) delete mode 100644 scripts/patch-draft-cascade-constraints.cjs delete mode 100644 test/unit/patch-draft-cascade-constraints.test.js diff --git a/.deploy/mta.yaml b/.deploy/mta.yaml index bb32c1a56..1e06c8326 100644 --- a/.deploy/mta.yaml +++ b/.deploy/mta.yaml @@ -51,9 +51,6 @@ build-parameters: - bash -c "cd .. && npm run prebuild:parsers-bundle" - bash -c "cd .. && node scripts/bundle-shared.cjs && npx cds build --production" - bash -c "cd .. && node -e 'for (const p of [\"gen/db/src/gen/.hdiconfig\",\"gen/db-qa/src/gen/.hdiconfig\"]) { const fs=require(\"fs\"); const d=JSON.parse(fs.readFileSync(p,\"utf8\")); d.file_suffixes=d.file_suffixes||{}; d.file_suffixes.hdbprocedure={plugin_name:\"com.sap.hana.di.procedure\"}; fs.writeFileSync(p, JSON.stringify(d,null,2)+\"\\n\"); }'" - # Patch prod draft tables with ON DELETE CASCADE FK (#2993). Prod tree only — - # gen/db-qa emits no draft tables so it is intentionally not patched. - - bash -c "cd .. && node scripts/patch-draft-cascade-constraints.cjs gen/db/src/gen" - bash -c "rm -f ../gen/db-qa/src/gen/sap.changelog.*.hdbindex" # Build app/explore + emit the bundle manifest into hugo/data/. # Hugo reads site.Data.explore_bundle at template-render time diff --git a/scripts/patch-draft-cascade-constraints.cjs b/scripts/patch-draft-cascade-constraints.cjs deleted file mode 100644 index 248971f22..000000000 --- a/scripts/patch-draft-cascade-constraints.cjs +++ /dev/null @@ -1,123 +0,0 @@ -// scripts/patch-draft-cascade-constraints.cjs -// Build-time generator: emits a standalone *.hdbconstraint artifact per Fiori -// draft table so SAP HANA enforces ON DELETE CASCADE of draft rows when their -// DraftAdministrativeData record is removed (the `dad` draft delete strategy / -// cds.fiori.draft_admin_integrity). -// -// Background (#2993): the FK must NOT be inlined into the generated -// *_drafts.hdbtable — HDI rejects that with error 8250014 -// ("this artifact type must not have references to other objects"). The correct -// HDI artifact is a separate .hdbconstraint (plugin com.sap.hana.di.constraint, -// already registered in the generated .hdiconfig). -// -// The referenced table DRAFT_DRAFTADMINISTRATIVEDATA is created by the CAP -// runtime, but it lives in the SAME HDI container schema as the draft tables and -// has primary key DRAFTUUID (verified against the deployed DEV container), so -// the ALTER TABLE ... ADD CONSTRAINT that HDI renders at deploy time resolves. -// -// .hdbconstraint file format (confirmed from SAP-samples: hana-shine-xsa -// core-db/src/data/PO_CONST.hdbconstraint and others) — SQL constraint body with -// NO `ALTER TABLE ... ADD CONSTRAINT` verb: -// -// CONSTRAINT -// ON "" FOREIGN KEY ("") -// REFERENCES "" ("") ON DELETE CASCADE -// -// Usage (CLI): -// node scripts/patch-draft-cascade-constraints.cjs [genDir] -// genDir defaults to gen/db/src/gen -// -// Exported API: -// patchDraftCascadeConstraints(genDir) -> { patched: string[], skipped: string[] } - -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -// Bare (unquoted) in the generated .hdbtable → HANA folds to UPPERCASE. -const FK_COLUMN_SRC = 'DraftAdministrativeData_DraftUUID'; -const FK_COLUMN = 'DRAFTADMINISTRATIVEDATA_DRAFTUUID'; -const PARENT_TABLE = 'DRAFT_DRAFTADMINISTRATIVEDATA'; -const PARENT_COLUMN = 'DRAFTUUID'; - -// Extract the physical table name from `COLUMN TABLE (` (first line). -function tableNameOf(hdbtableContent) { - const m = hdbtableContent.match(/COLUMN\s+TABLE\s+("?)([A-Za-z0-9_.]+)\1\s*\(/); - return m ? m[2] : null; -} - -/** - * For each *_drafts.hdbtable under genDir that carries the DraftAdministrativeData - * FK column, write a sibling *_drafts.hdbconstraint declaring the ON DELETE - * CASCADE foreign key. Leaves the .hdbtable untouched. - * - * @param {string} genDir Path to the gen dir (default gen/db/src/gen). - * @returns {{ patched: string[], skipped: string[] }} - * @throws {Error} when no *_drafts.hdbtable files are found. - */ -function patchDraftCascadeConstraints(genDir) { - const entries = fs.readdirSync(genDir); - const draftFiles = entries.filter((f) => f.endsWith('_drafts.hdbtable')); - if (draftFiles.length === 0) { - throw new Error( - `no *_drafts.hdbtable files found in ${genDir} — run cds build --production first` - ); - } - - const patched = []; - const skipped = []; - - for (const filename of draftFiles) { - const filepath = path.join(genDir, filename); - const content = fs.readFileSync(filepath, 'utf8'); - - // Only tables that actually carry the draft-admin FK column. - if (!content.includes(FK_COLUMN_SRC)) { - skipped.push(filename); - continue; - } - - const table = tableNameOf(content); - if (!table) { - // Can't determine the table name — skip rather than emit a broken artifact. - skipped.push(filename); - continue; - } - - // Deterministic constraint name, bounded well under HANA's 127-char limit. - const constraintName = `${table}_dadFkCascade`; - const constraintFile = filename.replace(/\.hdbtable$/, '.hdbconstraint'); - const constraintPath = path.join(genDir, constraintFile); - - const body = - `CONSTRAINT ${constraintName}\n` + - `ON "${table}" FOREIGN KEY ("${FK_COLUMN}")\n` + - `REFERENCES "${PARENT_TABLE}" ("${PARENT_COLUMN}") ON DELETE CASCADE\n`; - - // Idempotent: skip rewrite if an identical artifact already exists. - if (fs.existsSync(constraintPath) && fs.readFileSync(constraintPath, 'utf8') === body) { - skipped.push(filename); - continue; - } - - fs.writeFileSync(constraintPath, body, { encoding: 'utf8' }); - patched.push(constraintFile); - } - - return { patched, skipped }; -} - -// CLI entry point -if (require.main === module) { - const genDir = process.argv[2] || 'gen/db/src/gen'; - try { - const result = patchDraftCascadeConstraints(genDir); - console.log(`wrote ${result.patched.length} .hdbconstraint / skipped ${result.skipped.length}`); - } catch (err) { - console.error(err.message); - process.exit(1); - } -} - -module.exports = { patchDraftCascadeConstraints }; diff --git a/test/unit/patch-draft-cascade-constraints.test.js b/test/unit/patch-draft-cascade-constraints.test.js deleted file mode 100644 index 6ddfdc91e..000000000 --- a/test/unit/patch-draft-cascade-constraints.test.js +++ /dev/null @@ -1,85 +0,0 @@ -// test/unit/patch-draft-cascade-constraints.test.js -// Unit tests for the build-time cascade-constraint generator (#2993). -// No CDS boot — pure file-system fixture only. -// -// The generator emits a standalone *.hdbconstraint per draft table (NOT an -// inline FK in the .hdbtable — HDI rejects that, error 8250014). - -import { describe, it, expect, beforeEach } from 'vitest'; -import fs from 'node:fs'; -import path from 'node:path'; -import os from 'node:os'; -import { patchDraftCascadeConstraints } from '../../scripts/patch-draft-cascade-constraints.cjs'; - -const FIXTURE_WITH_FK = `COLUMN TABLE AdminService_Missions_drafts ( - ID NVARCHAR(36) NOT NULL, - IsActiveEntity BOOLEAN, - HasActiveEntity BOOLEAN, - HasDraftEntity BOOLEAN, - DraftAdministrativeData_DraftUUID NVARCHAR(36) NOT NULL, - PRIMARY KEY(ID) -)`; - -const FIXTURE_WITHOUT_FK = `COLUMN TABLE AdminService_NoFk_drafts ( - ID NVARCHAR(36) NOT NULL, - IsActiveEntity BOOLEAN, - PRIMARY KEY(ID) -)`; - -let tmp; -let emptyTmp; - -const tablePath = (name) => path.join(tmp, name); -const read = (name) => fs.readFileSync(path.join(tmp, name), 'utf8'); -const exists = (name) => fs.existsSync(path.join(tmp, name)); - -beforeEach(() => { - tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'patch-draft-test-')); - emptyTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'patch-draft-empty-')); - fs.writeFileSync(tablePath('AdminService.Missions_drafts.hdbtable'), FIXTURE_WITH_FK, 'utf8'); - fs.writeFileSync(tablePath('AdminService.NoFk_drafts.hdbtable'), FIXTURE_WITHOUT_FK, 'utf8'); -}); - -describe('patchDraftCascadeConstraints', () => { - it('writes a sibling .hdbconstraint with the ON DELETE CASCADE FK', () => { - const r = patchDraftCascadeConstraints(tmp); - expect(exists('AdminService.Missions_drafts.hdbconstraint')).toBe(true); - expect(r.patched).toContain('AdminService.Missions_drafts.hdbconstraint'); - - const c = read('AdminService.Missions_drafts.hdbconstraint'); - // CONSTRAINT / ON "" FOREIGN KEY (...) / REFERENCES ... CASCADE - expect(c).toMatch(/^CONSTRAINT\s+\S+/m); - expect(c).toMatch(/ON\s+"AdminService_Missions_drafts"\s+FOREIGN KEY\s*\("DRAFTADMINISTRATIVEDATA_DRAFTUUID"\)/); - expect(c).toMatch(/REFERENCES\s+"DRAFT_DRAFTADMINISTRATIVEDATA"\s*\("DRAFTUUID"\)\s+ON DELETE CASCADE/); - // No ALTER TABLE / ADD CONSTRAINT verb — .hdbconstraint is the bare body. - expect(c).not.toMatch(/ALTER\s+TABLE/i); - expect(c).not.toMatch(/ADD\s+CONSTRAINT/i); - // Quoted UPPERCASE column (HANA folds the bare declaration to upper). - expect(c).not.toMatch(/"DraftAdministrativeData_DraftUUID"/); - }); - - it('leaves the .hdbtable completely untouched (HDI rejects inline FK)', () => { - const before = read('AdminService.Missions_drafts.hdbtable'); - patchDraftCascadeConstraints(tmp); - expect(read('AdminService.Missions_drafts.hdbtable')).toBe(before); - expect(before).not.toMatch(/FOREIGN KEY/i); - }); - - it('skips draft tables without the FK column (no constraint file)', () => { - patchDraftCascadeConstraints(tmp); - expect(exists('AdminService.NoFk_drafts.hdbconstraint')).toBe(false); - }); - - it('is idempotent — second run rewrites identical content', () => { - patchDraftCascadeConstraints(tmp); - const first = read('AdminService.Missions_drafts.hdbconstraint'); - const r2 = patchDraftCascadeConstraints(tmp); - expect(read('AdminService.Missions_drafts.hdbconstraint')).toBe(first); - // Second run detects the identical artifact and skips the rewrite. - expect(r2.skipped).toContain('AdminService.Missions_drafts.hdbtable'); - }); - - it('throws when no draft tables are found', () => { - expect(() => patchDraftCascadeConstraints(emptyTmp)).toThrow(/no .*_drafts\.hdbtable/i); - }); -});