|
21 | 21 | * `DateStyle` (`ISO, MDY`) the server reads `9-03-04` as 2004-09-03 — a |
22 | 22 | * different day, stored or compared silently — and refuses `99-03-04` |
23 | 23 | * (`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 |
25 | 34 | * one answer for the number, its `Date` and its ISO string, plus a 2026 |
26 | 35 | * control. A year below 0 or above 9999 is refused |
27 | 36 | * 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[]]> = [ |
74 | 83 |
|
75 | 84 | const NO_AUDIT = { bypassTenantAudit: true }; |
76 | 85 |
|
| 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 | + |
77 | 106 | function measure(cell: DialectCell): void { |
78 | 107 | describe(`[#20240] a date field's year is four digits — ${cell.label}`, () => { |
79 | 108 | let driver: SqlDriver; |
@@ -107,15 +136,20 @@ function measure(cell: DialectCell): void { |
107 | 136 | }); |
108 | 137 |
|
109 | 138 | 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); |
111 | 140 | await driver.create(WRITES, { id: 'w1', placed_on: Y0999 }, NO_AUDIT); |
112 | 141 | 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']); |
119 | 153 | }); |
120 | 154 | }); |
121 | 155 | } |
|
0 commit comments