Skip to content

Commit 44bcc2a

Browse files
committed
fix: serialise mirror Date values in MariaDB-compatible format
Sequelize's default datetime format "YYYY-MM-DD HH:mm:ss.SSS +00:00" includes a trailing "+00:00" offset that recent MariaDB strict mode rejects with: Incorrect datetime value: '2026-04-16 22:41:27.490 +00:00' for column `cadt_mirror_test`.`audit`.`createdAt` at row 1 Every mirror write touching a DATE/DATETIME column was silently dropped by the fire-and-forget safeMirrorDbHandler[V2]. This affected both V1 and V2 mirrors in production - the v2 live-api CI run exposed 59 such errors but still passed because V2 test assertions only check project/methodology-level records, not audit. V1 live-api verification added in this PR surfaced the bug directly on the verified records (projects.projectStatusDate, labels.creditingPeriodStartDate, etc.). The fix is to set `timezone: '+00:00'` at the Sequelize constructor level for both `mirror` and `v2Mirror` configs. At the constructor level (unlike Model.init, where the option is silently ignored), Sequelize converts Date values to UTC and emits the MariaDB-safe "YYYY-MM-DD HH:MM:SS[.SSS]" format. Also updates prepareMysqlMirrorV2's JSDoc to reflect that it does throw (intentionally) when V1/V2 share a MIRROR_DB name - the earlier "Never throws" claim was left over from before validateMirrorDbNames was moved to fail-loud. Both callers (prepareV2Db and startV2ReconnectBackfill) are wrapped in try/catch at the right level.
1 parent 47481a0 commit 44bcc2a

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

src/config/config.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ export default {
6161
host: getConfig().MIRROR_DB.DB_HOST || '',
6262
dialect: 'mysql',
6363
logging: mirrorLogging,
64+
// Tell Sequelize to serialise Date values as UTC without a trailing
65+
// "+00:00" offset. Recent MariaDB strict mode rejects the
66+
// "YYYY-MM-DD HH:MM:SS.SSS +00:00" format Sequelize emits by default,
67+
// with errors like:
68+
// Incorrect datetime value: '2026-04-16 22:41:27.490 +00:00'
69+
// for column `cadt_mirror_test`.`audit`.`createdAt` at row 1
70+
// Setting timezone at the Sequelize constructor level (as opposed to
71+
// Model.init, where it is silently ignored) produces the MariaDB-safe
72+
// "YYYY-MM-DD HH:MM:SS" format. Without this, every mirror write that
73+
// touches a DATE/DATETIME column is silently dropped by the
74+
// fire-and-forget safeMirrorDbHandler.
75+
timezone: '+00:00',
6476
},
6577
// V2 Database Configurations
6678
v2Local: {
@@ -96,5 +108,8 @@ export default {
96108
host: getConfigV2().MIRROR_DB?.DB_HOST || '',
97109
dialect: 'mysql',
98110
logging: mirrorLogging,
111+
// See comment on `mirror` above. Same MariaDB strict-mode datetime
112+
// issue applies to V2.
113+
timezone: '+00:00',
99114
},
100115
};

src/database/v2/index.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,17 @@ const sweepMirrorOrphansV2 = async (source, mirror, name) => {
581581
* Ensure the MySQL mirror database exists and has up-to-date migrations.
582582
* Idempotent: safe to call from startup and again on every reconnect.
583583
*
584-
* Returns true on success, false on failure. Never throws so it won't take
585-
* down the main database path.
584+
* Returns true on success, false on transient I/O failure (e.g. MySQL not
585+
* reachable, CREATE DATABASE fails, migrations fail). I/O failures are
586+
* caught and logged so they don't take down the main database path.
587+
*
588+
* THROWS (intentionally) when V1 and V2 are misconfigured to share the
589+
* same MIRROR_DB.DB_NAME - see validateMirrorDbNames. That is a
590+
* programmer/ops error, not a retryable failure, and surfacing it loudly
591+
* at CADT startup prevents silent data corruption where V1 and V2 would
592+
* clobber each other's rows in the mirror. Callers on both the startup
593+
* path (prepareV2Db) and the reconnect path (startV2ReconnectBackfill)
594+
* are already wrapped in try/catch at the appropriate level.
586595
*/
587596
export const prepareMysqlMirrorV2 = async () => {
588597
if (v2MirrorSetupPromise) {

0 commit comments

Comments
 (0)