diff --git a/srv/lib/scheduler-ready.js b/srv/lib/scheduler-ready.js index 979dc81fa..2b5f1000e 100644 --- a/srv/lib/scheduler-ready.js +++ b/srv/lib/scheduler-ready.js @@ -21,3 +21,20 @@ export function computeReadiness({ schedulerArmed, dbOk }) { db: dbOk ? 'connected' : 'error', }; } + +/** + * Dialect-safe liveness ping SQL for a cheap `cds.db.run(...)`. + * + * #2975 regression: HANA rejects a bare `SELECT 1` ("sql syntax error") — it + * requires `SELECT 1 FROM DUMMY`. SQLite rejects `FROM DUMMY`. The original + * /health/ready probe hardcoded `SELECT 1`, so dbOk was ALWAYS false on HANA, + * /health/ready stayed 503, and CF's startup health gate crash-looped the app + * on every deploy. (The integration test ran on in-memory SQLite, where bare + * `SELECT 1` works, so it never caught this.) + * + * @param {string} [kind] db kind, defaults to cds.db.kind at call sites + * @returns {string} + */ +export function dbPingSql(kind) { + return kind === 'hana' ? 'SELECT 1 FROM DUMMY' : 'SELECT 1'; +} diff --git a/srv/server.js b/srv/server.js index d17dfd8e5..cb82355a6 100644 --- a/srv/server.js +++ b/srv/server.js @@ -105,7 +105,7 @@ import { uploadPetSubmission, decodePhotoUpload } from './lib/petoberfest-upload import { fetchPetPhoto } from './lib/petoberfest-photo-store.js'; import { installDbWrap } from './lib/metrics-db-wrap.js'; import { wireDraftIntegrityPreflight } from './lib/draft-integrity-preflight.js'; -import { isSchedulerReady, computeReadiness } from './lib/scheduler-ready.js'; +import { isSchedulerReady, computeReadiness, dbPingSql } from './lib/scheduler-ready.js'; import './graphql-config.js'; import { makeA2aRouter } from './lib/a2a/rpc-router.js'; import { a2aIasAuthMiddleware } from './lib/a2a/ias-auth-mw.js'; @@ -297,7 +297,7 @@ cds.on('bootstrap', (app) => { app.get('/health/ready', async (req, res) => { let dbOk = false; if (cds.db) { - try { await cds.db.run('SELECT 1'); dbOk = true; } + try { await cds.db.run(dbPingSql(cds.db.kind)); dbOk = true; } catch (_err) { dbOk = false; } } // cds.db absent → dbOk stays false @@ -307,7 +307,7 @@ cds.on('bootstrap', (app) => { app.get('/health/db', async (req, res) => { try { - await cds.db?.run('SELECT 1 FROM DUMMY'); + await cds.db?.run(dbPingSql(cds.db?.kind)); res.json({ status: 'ok', db: 'connected' }); } catch (err) { console.error('[health/db]', err.message); diff --git a/test/unit/srv/lib/scheduler-ready.test.js b/test/unit/srv/lib/scheduler-ready.test.js index c52f8b4c6..fccf5b7ab 100644 --- a/test/unit/srv/lib/scheduler-ready.test.js +++ b/test/unit/srv/lib/scheduler-ready.test.js @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { markSchedulerReady, isSchedulerReady, _resetForTests, computeReadiness } from '../../../../srv/lib/scheduler-ready.js'; +import { markSchedulerReady, isSchedulerReady, _resetForTests, computeReadiness, dbPingSql } from '../../../../srv/lib/scheduler-ready.js'; describe('scheduler-ready latch', () => { beforeEach(() => _resetForTests()); @@ -39,3 +39,15 @@ describe('computeReadiness', () => { expect(computeReadiness({ schedulerArmed: false, dbOk: false }).ready).toBe(false); }); }); + +describe('dbPingSql (#2975 HANA regression)', () => { + it('uses FROM DUMMY on HANA (bare SELECT 1 is a HANA syntax error)', () => { + expect(dbPingSql('hana')).toBe('SELECT 1 FROM DUMMY'); + }); + it('uses bare SELECT 1 on SQLite (FROM DUMMY is a SQLite error)', () => { + expect(dbPingSql('sqlite')).toBe('SELECT 1'); + }); + it('defaults to bare SELECT 1 when kind is undefined', () => { + expect(dbPingSql()).toBe('SELECT 1'); + }); +});