#28254·directus

Our implementation of the year function on Oracle is using the wrong date standard

Author: NitwelCreated Sep 17, 2026Updated Sep 17, 2026
LabelsBugEngine
### Describe the Bug

Oracle's year() helper uses the ISO year format mask, not the calendar year:

oracle.ts:18

javascript
return this.knex.raw(`TO_CHAR(??.??${parseLocaltime(options?.type)}, 'IYYY')`, [table, column]);

IYYY is the ISO-8601 week-numbering year, which disagrees with the calendar year whenever Jan 1 falls on a Fri/Sat/Sun (that date belongs to the last ISO week of the previous year). The test seeds exactly such a date:

n | release | calendar year | Oracle IYYY -- | -- | -- | -- 1 | 2001-01-01 | 2001 | 2001 2 | 2002-01-01 | 2002 | 2002 3 | 2003-01-01 | 2003 | 2003 4 | 2004-01-01 | 2004 | 2004 5 | 2005-01-01 | 2005 | 2004 ← Saturday, ISO week 53 of 2004

So on Oracle items 4 and 5 both sort as 2004. Sorting has no secondary tiebreak (apply-query/index.ts:124 orders on the single column only), so the tied pair comes back in Oracle's physical row order — which for the descending query yields [4, 5, 3, 2, 1] instead of [5, 4, 3, 2, 1].

Every other dialect returns the calendar year: EXTRACT(YEAR FROM …) on Postgres, YEAR() on MySQL, DATEPART(year, …) on MSSQL, strftime('%Y', …) on SQLite. Oracle is the only outlier.


Oracle's year() helper uses the ISO year format mask, not the calendar year: [oracle.ts:18](vscode-webview://0vp8aq9d7ft81p4i348am7os985bgnvltdb2vdfnqorjp4b18f99/api/src/database/helpers/fn/dialects/oracle.ts#L18) return this.knex.raw(`TO_CHAR(??.??${parseLocaltime(options?.type)}, 'IYYY')`, [table, column]); IYYY is the ISO-8601 week-numbering year, which disagrees with the calendar year whenever Jan 1 falls on a Fri/Sat/Sun (that date belongs to the last ISO week of the previous year). The test seeds exactly such a date: n release calendar year Oracle IYYY 1 2001-01-01 2001 2001 2 2002-01-01 2002 2002 3 2003-01-01 2003 2003 4 2004-01-01 2004 2004 5 2005-01-01 2005 2004 ← Saturday, ISO week 53 of 2004 So on Oracle items 4 and 5 both sort as 2004. Sorting has no secondary tiebreak ([apply-query/index.ts:124](vscode-webview://0vp8aq9d7ft81p4i348am7os985bgnvltdb2vdfnqorjp4b18f99/api/src/database/run-ast/lib/apply-query/index.ts#L124) orders on the single column only), so the tied pair comes back in Oracle's physical row order — which for the descending query yields [4, 5, 3, 2, 1] instead of [5, 4, 3, 2, 1]. Every other dialect returns the calendar year: EXTRACT(YEAR FROM …) on Postgres, YEAR() on MySQL, DATEPART(year, …) on MSSQL, strftime('%Y', …) on SQLite. Oracle is the only outlier. # Fix year(table, column, options) { return this.knex.raw(`TO_CHAR(??.??${parseLocaltime(options?.type)}, 'YYYY')`, [table, column]); } Two related notes while you're in there: Oracle's TO_CHAR returns a string ('2001'), whereas the other dialects return a number. For 4-digit years string ordering matches numeric ordering so sorting is unaffected, but year(release) as a selected field will come back typed differently on Oracle. Wrapping in TO_NUMBER(...) would align it. week() uses 'IW' (ISO week), matching Postgres's EXTRACT(WEEK) but not MySQL's WEEK() or SQLite's %W. That's a pre-existing cross-dialect inconsistency, separate from this failure. Want me to apply the year() fix (and optionally the TO_NUMBER wrap)? ### To Reproduce `pnpm vitest --project oracle sort` to run the test ### Directus Version latest ### Hosting Strategy Self-Hosted (Docker Image) ### Database oracle