Skip to content

Commit 677aaeb

Browse files
committed
test(driver-sql): assert the date write path on the stored text — mysql2 reads a year below 100 back a century late
The live MySQL cell read year 9 back through the driver as 1909-03-04: mysql2's parseDate rebuilds a DATE with Date.UTC(y, m - 1, d), which maps years 0..99 to 1900..1999. The server stored 0009-03-04. The write-path cell now reads the stored text by a raw per-dialect cast on every dialect, and adds the year-99 write. Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2f219ba commit 677aaeb

1 file changed

Lines changed: 42 additions & 8 deletions

File tree

‎packages/drivers/driver-sql/src/sql-driver-20240-date-year-spelling.test.ts‎

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@
2121
* `DateStyle` (`ISO, MDY`) the server reads `9-03-04` as 2004-09-03 — a
2222
* different day, stored or compared silently — and refuses `99-03-04`
2323
* (`22008`). The unpadded spelling was restored by ablation to measure those
24-
* two, since this file's 0999 fixture reads right there. Each row below asserts
24+
* two, since this file's 0999 fixture reads right there. MySQL 8.0 read
25+
* `9-03-04` as year 9 but stored `99-03-04` as 1999-03-04.
26+
*
27+
* The write path is asserted on the STORED text, read by a raw cast
28+
* (`storedText`), never through the driver's read path. On MySQL that path
29+
* hands a year below 100 back a century late: mysql2 rebuilds a `DATE` as
30+
* `new Date(Date.UTC(y, m - 1, d))` (`parseDate`, with the driver's
31+
* `timezone: 'Z'`), and `Date.UTC` maps years 0..99 to 1900..1999, so a stored
32+
* `0009-03-04` is presented as `1909-03-04`. That is a read-path defect of its
33+
* own, unchanged by this card and reported beside it. Each row below asserts
2534
* one answer for the number, its `Date` and its ISO string, plus a 2026
2635
* control. A year below 0 or above 9999 is refused
2736
* as a comparand one layer up, at the engine's temporal-comparand door
@@ -74,6 +83,26 @@ const CELLS: ReadonlyArray<readonly [string, number, string, string[]]> = [
7483

7584
const NO_AUDIT = { bypassTenantAudit: true };
7685

86+
/** knex's raw result shape differs per client; this is the only place that knows. */
87+
function rowsOf(cell: DialectCell, res: any): any[] {
88+
if (cell.id === 'pg') return res?.rows ?? [];
89+
if (cell.id === 'mysql') return Array.isArray(res) ? (res[0] ?? []) : [];
90+
return Array.isArray(res) ? res : (res?.rows ?? []);
91+
}
92+
93+
/** The text the server STORED for one row's `date` — a raw cast, not the driver's read path. */
94+
async function storedText(driver: SqlDriver, cell: DialectCell, id: string): Promise<string | null> {
95+
const sql =
96+
cell.id === 'pg'
97+
? `select "placed_on"::text as t from "${WRITES}" where "id" = ?`
98+
: cell.id === 'mysql'
99+
? `select cast(\`placed_on\` as char) as t from \`${WRITES}\` where \`id\` = ?`
100+
: `select cast("placed_on" as text) as t from "${WRITES}" where "id" = ?`;
101+
const rows = rowsOf(cell, await driver.execute(sql, [id]));
102+
expect(rows, `no stored row for ${id}`).toHaveLength(1);
103+
return rows[0].t ?? null;
104+
}
105+
77106
function measure(cell: DialectCell): void {
78107
describe(`[#20240] a date field's year is four digits — ${cell.label}`, () => {
79108
let driver: SqlDriver;
@@ -107,15 +136,20 @@ function measure(cell: DialectCell): void {
107136
});
108137

109138
it('the write path stores the four-digit year — create and update, a number and a Date', async () => {
110-
const read = async (id: string) => (await driver.findOne(WRITES, { where: { id } }, NO_AUDIT))?.placed_on;
139+
const stored = (id: string) => storedText(driver, cell, id);
111140
await driver.create(WRITES, { id: 'w1', placed_on: Y0999 }, NO_AUDIT);
112141
await driver.create(WRITES, { id: 'w2', placed_on: new Date(Y0999) }, NO_AUDIT);
113-
expect(await read('w1')).toBe('0999-06-15');
114-
expect(await read('w2')).toBe('0999-06-15');
115-
await driver.update(WRITES, 'w1', { placed_on: Date.parse('0009-03-04T00:00:00.000Z') }, NO_AUDIT);
116-
expect(await read('w1')).toBe('0009-03-04');
117-
// …so the stored day and a comparand for it are one text.
118-
expect(await ids(WRITES, { placed_on: { $eq: '0999-06-15' } })).toEqual(['w2']);
142+
expect(await stored('w1')).toBe('0999-06-15');
143+
expect(await stored('w2')).toBe('0999-06-15');
144+
// A year from 100 up also reads back as written through the driver.
145+
expect((await driver.findOne(WRITES, { where: { id: 'w1' } }, NO_AUDIT))?.placed_on).toBe('0999-06-15');
146+
await driver.update(WRITES, 'w1', { placed_on: Y0009 }, NO_AUDIT);
147+
await driver.update(WRITES, 'w2', { placed_on: new Date(Y0099) }, NO_AUDIT);
148+
expect(await stored('w1')).toBe('0009-03-04');
149+
expect(await stored('w2')).toBe('0099-03-04');
150+
// …so the stored day and a comparand for it are one day.
151+
expect(await ids(WRITES, { placed_on: { $eq: '0009-03-04' } })).toEqual(['w1']);
152+
expect(await ids(WRITES, { placed_on: { $eq: new Date(Y0099) } })).toEqual(['w2']);
119153
});
120154
});
121155
}

0 commit comments

Comments
 (0)