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); - }); -});