From ee788f4f843073eb26bf07e101f83084e653270f Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Mon, 30 Mar 2026 17:12:57 +0200 Subject: [PATCH 1/2] feat(mssql-driver): Support use named timezones (#10582) Add CUBEJS_DB_MSSQL_USE_NAMED_TIMEZONES env variable to enable server-side DST-aware timezone conversion via AT TIME ZONE in MSSQL. Previously, convertTz() computed a fixed UTC offset at query build time using moment().tz().format('Z'), which didn't account for DST transitions within the queried data range. When enabled, IANA timezone names (e.g. 'America/New_York') are automatically mapped to Windows timezone names (e.g. 'Eastern Standard Time') required by MSSQL's AT TIME ZONE clause. Defaults to false for backward compatibility.. --- packages/cubejs-backend-shared/src/env.ts | 48 +- .../src/adapter/MssqlQuery.ts | 16 +- .../src/adapter/MysqlQuery.ts | 1 + .../src/adapter/windows-iana.ts | 466 ++++++++++++++++++ .../test/global-setup.ts | 2 + .../test/integration/mysql/MySqlDbRunner.js | 14 +- 6 files changed, 516 insertions(+), 31 deletions(-) create mode 100644 packages/cubejs-schema-compiler/src/adapter/windows-iana.ts diff --git a/packages/cubejs-backend-shared/src/env.ts b/packages/cubejs-backend-shared/src/env.ts index 0348648e0f658..cb9072b5af01b 100644 --- a/packages/cubejs-backend-shared/src/env.ts +++ b/packages/cubejs-backend-shared/src/env.ts @@ -1055,33 +1055,29 @@ const variables: Record any> = { * @see https://dev.mysql.com/doc/refman/8.4/en/date-and-time-functions.html#function_convert-tz * @see https://dev.mysql.com/doc/refman/8.4/en/time-zone-support.html */ - mysqlUseNamedTimezones: ({ dataSource }: { dataSource: string }) => { - const val = process.env[ - keyByDataSource( - 'CUBEJS_DB_MYSQL_USE_NAMED_TIMEZONES', - dataSource, - ) - ]; + mysqlUseNamedTimezones: ({ dataSource }: { dataSource: string }) => ( + get(keyByDataSource('CUBEJS_DB_MYSQL_USE_NAMED_TIMEZONES', dataSource)) + // It's true in schema-compiler integration tests + .default('false') + .asBool() + ), - if (val) { - if (val.toLocaleLowerCase() === 'true') { - return true; - } else if (val.toLowerCase() === 'false') { - return false; - } else { - throw new TypeError( - `The ${ - keyByDataSource( - 'CUBEJS_DB_MYSQL_USE_NAMED_TIMEZONES', - dataSource, - ) - } must be either 'true' or 'false'.` - ); - } - } else { - return false; - } - }, + /** **************************************************************** + * MSSQL Driver * + ***************************************************************** */ + + /** + * Use named timezones for date/time conversions via AT TIME ZONE. + * Defaults to FALSE, meaning that numeric offsets for timezone will be used. + * + * @see https://learn.microsoft.com/en-us/sql/t-sql/queries/at-time-zone-transact-sql + */ + mssqlUseNamedTimezones: ({ dataSource }: { dataSource: string }) => ( + get(keyByDataSource('CUBEJS_DB_MSSQL_USE_NAMED_TIMEZONES', dataSource)) + // It's true in schema-compiler integration tests + .default('false') + .asBool() + ), /** **************************************************************** * Databricks Driver * diff --git a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts index 147e3251dce0e..75de415fb55b2 100644 --- a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts @@ -1,11 +1,12 @@ import R from 'ramda'; import moment from 'moment-timezone'; -import { QueryAlias, parseSqlInterval } from '@cubejs-backend/shared'; +import { getEnv, QueryAlias, parseSqlInterval } from '@cubejs-backend/shared'; import { BaseQuery } from './BaseQuery'; import { BaseFilter } from './BaseFilter'; import { BaseSegment } from './BaseSegment'; import { ParamAllocator } from './ParamAllocator'; +import { resolveWindowsTimezone } from './windows-iana'; const abbrs = { EST: 'Eastern Standard Time', @@ -73,6 +74,14 @@ class MssqlSegment extends BaseSegment { } export class MssqlQuery extends BaseQuery { + private readonly useNamedTimezones: boolean; + + public constructor(compilers: any, options: any) { + super(compilers, options); + + this.useNamedTimezones = getEnv('mssqlUseNamedTimezones', { dataSource: this.dataSource }); + } + public newFilter(filter) { return new MssqlFilter(this, filter); } @@ -90,6 +99,11 @@ export class MssqlQuery extends BaseQuery { } public convertTz(field) { + if (this.useNamedTimezones) { + const windowsTz = resolveWindowsTimezone(this.timezone); + return `CAST(${field} AT TIME ZONE 'UTC' AT TIME ZONE '${windowsTz}' AS DATETIME2)`; + } + const offset = moment().tz(this.timezone).format('Z'); // 1. Treating the field as UTC (add '+00:00' offset) diff --git a/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts b/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts index 0bec1a5e2b571..f90b06386d22c 100644 --- a/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts @@ -45,6 +45,7 @@ export class MysqlQuery extends BaseQuery { if (this.useNamedTimezones) { return `CONVERT_TZ(${field}, @@session.time_zone, '${this.timezone}')`; } + return `CONVERT_TZ(${field}, @@session.time_zone, '${moment().tz(this.timezone).format('Z')}')`; } diff --git a/packages/cubejs-schema-compiler/src/adapter/windows-iana.ts b/packages/cubejs-schema-compiler/src/adapter/windows-iana.ts new file mode 100644 index 0000000000000..ec34509f1765d --- /dev/null +++ b/packages/cubejs-schema-compiler/src/adapter/windows-iana.ts @@ -0,0 +1,466 @@ +/* eslint-disable quote-props */ + +/** + * Mapping from IANA timezone names to Windows timezone names. + * Used by MSSQL's AT TIME ZONE which requires Windows timezone names. + * Source: CLDR windowsZones.xml (https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml) + */ +const ianaToWindows: Record = { + 'Africa/Abidjan': 'Greenwich Standard Time', + 'Africa/Accra': 'Greenwich Standard Time', + 'Africa/Addis_Ababa': 'E. Africa Standard Time', + 'Africa/Algiers': 'W. Central Africa Standard Time', + 'Africa/Asmera': 'E. Africa Standard Time', + 'Africa/Bamako': 'Greenwich Standard Time', + 'Africa/Bangui': 'W. Central Africa Standard Time', + 'Africa/Banjul': 'Greenwich Standard Time', + 'Africa/Bissau': 'Greenwich Standard Time', + 'Africa/Blantyre': 'South Africa Standard Time', + 'Africa/Brazzaville': 'W. Central Africa Standard Time', + 'Africa/Bujumbura': 'South Africa Standard Time', + 'Africa/Cairo': 'Egypt Standard Time', + 'Africa/Casablanca': 'Morocco Standard Time', + 'Africa/Ceuta': 'Romance Standard Time', + 'Africa/Conakry': 'Greenwich Standard Time', + 'Africa/Dakar': 'Greenwich Standard Time', + 'Africa/Dar_es_Salaam': 'E. Africa Standard Time', + 'Africa/Djibouti': 'E. Africa Standard Time', + 'Africa/Douala': 'W. Central Africa Standard Time', + 'Africa/El_Aaiun': 'Morocco Standard Time', + 'Africa/Freetown': 'Greenwich Standard Time', + 'Africa/Gaborone': 'South Africa Standard Time', + 'Africa/Harare': 'South Africa Standard Time', + 'Africa/Johannesburg': 'South Africa Standard Time', + 'Africa/Juba': 'South Sudan Standard Time', + 'Africa/Kampala': 'E. Africa Standard Time', + 'Africa/Khartoum': 'Sudan Standard Time', + 'Africa/Kigali': 'South Africa Standard Time', + 'Africa/Kinshasa': 'W. Central Africa Standard Time', + 'Africa/Lagos': 'W. Central Africa Standard Time', + 'Africa/Libreville': 'W. Central Africa Standard Time', + 'Africa/Lome': 'Greenwich Standard Time', + 'Africa/Luanda': 'W. Central Africa Standard Time', + 'Africa/Lubumbashi': 'South Africa Standard Time', + 'Africa/Lusaka': 'South Africa Standard Time', + 'Africa/Malabo': 'W. Central Africa Standard Time', + 'Africa/Maputo': 'South Africa Standard Time', + 'Africa/Maseru': 'South Africa Standard Time', + 'Africa/Mbabane': 'South Africa Standard Time', + 'Africa/Mogadishu': 'E. Africa Standard Time', + 'Africa/Monrovia': 'Greenwich Standard Time', + 'Africa/Nairobi': 'E. Africa Standard Time', + 'Africa/Ndjamena': 'W. Central Africa Standard Time', + 'Africa/Niamey': 'W. Central Africa Standard Time', + 'Africa/Nouakchott': 'Greenwich Standard Time', + 'Africa/Ouagadougou': 'Greenwich Standard Time', + 'Africa/Porto-Novo': 'W. Central Africa Standard Time', + 'Africa/Sao_Tome': 'Sao Tome Standard Time', + 'Africa/Tripoli': 'Libya Standard Time', + 'Africa/Tunis': 'W. Central Africa Standard Time', + 'Africa/Windhoek': 'Namibia Standard Time', + 'America/Adak': 'Aleutian Standard Time', + 'America/Anchorage': 'Alaskan Standard Time', + 'America/Anguilla': 'SA Western Standard Time', + 'America/Antigua': 'SA Western Standard Time', + 'America/Araguaina': 'Tocantins Standard Time', + 'America/Argentina/La_Rioja': 'Argentina Standard Time', + 'America/Argentina/Rio_Gallegos': 'Argentina Standard Time', + 'America/Argentina/Salta': 'Argentina Standard Time', + 'America/Argentina/San_Juan': 'Argentina Standard Time', + 'America/Argentina/San_Luis': 'Argentina Standard Time', + 'America/Argentina/Tucuman': 'Argentina Standard Time', + 'America/Argentina/Ushuaia': 'Argentina Standard Time', + 'America/Aruba': 'SA Western Standard Time', + 'America/Asuncion': 'Paraguay Standard Time', + 'America/Bahia': 'Bahia Standard Time', + 'America/Bahia_Banderas': 'Central Standard Time (Mexico)', + 'America/Barbados': 'SA Western Standard Time', + 'America/Belem': 'SA Eastern Standard Time', + 'America/Belize': 'Central America Standard Time', + 'America/Blanc-Sablon': 'SA Western Standard Time', + 'America/Boa_Vista': 'SA Western Standard Time', + 'America/Bogota': 'SA Pacific Standard Time', + 'America/Boise': 'Mountain Standard Time', + 'America/Buenos_Aires': 'Argentina Standard Time', + 'America/Cambridge_Bay': 'Mountain Standard Time', + 'America/Campo_Grande': 'Central Brazilian Standard Time', + 'America/Cancun': 'Eastern Standard Time (Mexico)', + 'America/Caracas': 'Venezuela Standard Time', + 'America/Catamarca': 'Argentina Standard Time', + 'America/Cayenne': 'SA Eastern Standard Time', + 'America/Cayman': 'SA Pacific Standard Time', + 'America/Chicago': 'Central Standard Time', + 'America/Chihuahua': 'Central Standard Time (Mexico)', + 'America/Ciudad_Juarez': 'Mountain Standard Time', + 'America/Coral_Harbour': 'SA Pacific Standard Time', + 'America/Cordoba': 'Argentina Standard Time', + 'America/Costa_Rica': 'Central America Standard Time', + 'America/Coyhaique': 'Magallanes Standard Time', + 'America/Creston': 'US Mountain Standard Time', + 'America/Cuiaba': 'Central Brazilian Standard Time', + 'America/Curacao': 'SA Western Standard Time', + 'America/Danmarkshavn': 'Greenwich Standard Time', + 'America/Dawson': 'Yukon Standard Time', + 'America/Dawson_Creek': 'US Mountain Standard Time', + 'America/Denver': 'Mountain Standard Time', + 'America/Detroit': 'Eastern Standard Time', + 'America/Dominica': 'SA Western Standard Time', + 'America/Edmonton': 'Mountain Standard Time', + 'America/Eirunepe': 'SA Pacific Standard Time', + 'America/El_Salvador': 'Central America Standard Time', + 'America/Fort_Nelson': 'US Mountain Standard Time', + 'America/Fortaleza': 'SA Eastern Standard Time', + 'America/Glace_Bay': 'Atlantic Standard Time', + 'America/Godthab': 'Greenland Standard Time', + 'America/Goose_Bay': 'Atlantic Standard Time', + 'America/Grand_Turk': 'Turks And Caicos Standard Time', + 'America/Grenada': 'SA Western Standard Time', + 'America/Guadeloupe': 'SA Western Standard Time', + 'America/Guatemala': 'Central America Standard Time', + 'America/Guayaquil': 'SA Pacific Standard Time', + 'America/Guyana': 'SA Western Standard Time', + 'America/Halifax': 'Atlantic Standard Time', + 'America/Havana': 'Cuba Standard Time', + 'America/Hermosillo': 'US Mountain Standard Time', + 'America/Indiana/Knox': 'Central Standard Time', + 'America/Indiana/Marengo': 'US Eastern Standard Time', + 'America/Indiana/Petersburg': 'Eastern Standard Time', + 'America/Indiana/Tell_City': 'Central Standard Time', + 'America/Indiana/Vevay': 'US Eastern Standard Time', + 'America/Indiana/Vincennes': 'Eastern Standard Time', + 'America/Indiana/Winamac': 'Eastern Standard Time', + 'America/Indianapolis': 'US Eastern Standard Time', + 'America/Inuvik': 'Mountain Standard Time', + 'America/Iqaluit': 'Eastern Standard Time', + 'America/Jamaica': 'SA Pacific Standard Time', + 'America/Jujuy': 'Argentina Standard Time', + 'America/Juneau': 'Alaskan Standard Time', + 'America/Kentucky/Monticello': 'Eastern Standard Time', + 'America/Kralendijk': 'SA Western Standard Time', + 'America/La_Paz': 'SA Western Standard Time', + 'America/Lima': 'SA Pacific Standard Time', + 'America/Los_Angeles': 'Pacific Standard Time', + 'America/Louisville': 'Eastern Standard Time', + 'America/Lower_Princes': 'SA Western Standard Time', + 'America/Maceio': 'SA Eastern Standard Time', + 'America/Managua': 'Central America Standard Time', + 'America/Manaus': 'SA Western Standard Time', + 'America/Marigot': 'SA Western Standard Time', + 'America/Martinique': 'SA Western Standard Time', + 'America/Matamoros': 'Central Standard Time', + 'America/Mazatlan': 'Mountain Standard Time (Mexico)', + 'America/Mendoza': 'Argentina Standard Time', + 'America/Menominee': 'Central Standard Time', + 'America/Merida': 'Central Standard Time (Mexico)', + 'America/Metlakatla': 'Alaskan Standard Time', + 'America/Mexico_City': 'Central Standard Time (Mexico)', + 'America/Miquelon': 'Saint Pierre Standard Time', + 'America/Moncton': 'Atlantic Standard Time', + 'America/Monterrey': 'Central Standard Time (Mexico)', + 'America/Montevideo': 'Montevideo Standard Time', + 'America/Montserrat': 'SA Western Standard Time', + 'America/Nassau': 'Eastern Standard Time', + 'America/New_York': 'Eastern Standard Time', + 'America/Nome': 'Alaskan Standard Time', + 'America/Noronha': 'UTC-02', + 'America/North_Dakota/Beulah': 'Central Standard Time', + 'America/North_Dakota/Center': 'Central Standard Time', + 'America/North_Dakota/New_Salem': 'Central Standard Time', + 'America/Ojinaga': 'Central Standard Time', + 'America/Panama': 'SA Pacific Standard Time', + 'America/Paramaribo': 'SA Eastern Standard Time', + 'America/Phoenix': 'US Mountain Standard Time', + 'America/Port-au-Prince': 'Haiti Standard Time', + 'America/Port_of_Spain': 'SA Western Standard Time', + 'America/Porto_Velho': 'SA Western Standard Time', + 'America/Puerto_Rico': 'SA Western Standard Time', + 'America/Punta_Arenas': 'Magallanes Standard Time', + 'America/Rankin_Inlet': 'Central Standard Time', + 'America/Recife': 'SA Eastern Standard Time', + 'America/Regina': 'Canada Central Standard Time', + 'America/Resolute': 'Central Standard Time', + 'America/Rio_Branco': 'SA Pacific Standard Time', + 'America/Santarem': 'SA Eastern Standard Time', + 'America/Santiago': 'Pacific SA Standard Time', + 'America/Santo_Domingo': 'SA Western Standard Time', + 'America/Sao_Paulo': 'E. South America Standard Time', + 'America/Scoresbysund': 'Azores Standard Time', + 'America/Sitka': 'Alaskan Standard Time', + 'America/St_Barthelemy': 'SA Western Standard Time', + 'America/St_Johns': 'Newfoundland Standard Time', + 'America/St_Kitts': 'SA Western Standard Time', + 'America/St_Lucia': 'SA Western Standard Time', + 'America/St_Thomas': 'SA Western Standard Time', + 'America/St_Vincent': 'SA Western Standard Time', + 'America/Swift_Current': 'Canada Central Standard Time', + 'America/Tegucigalpa': 'Central America Standard Time', + 'America/Thule': 'Atlantic Standard Time', + 'America/Tijuana': 'Pacific Standard Time (Mexico)', + 'America/Toronto': 'Eastern Standard Time', + 'America/Tortola': 'SA Western Standard Time', + 'America/Vancouver': 'Pacific Standard Time', + 'America/Whitehorse': 'Yukon Standard Time', + 'America/Winnipeg': 'Central Standard Time', + 'America/Yakutat': 'Alaskan Standard Time', + 'Antarctica/Casey': 'Central Pacific Standard Time', + 'Antarctica/Davis': 'SE Asia Standard Time', + 'Antarctica/DumontDUrville': 'West Pacific Standard Time', + 'Antarctica/Macquarie': 'Tasmania Standard Time', + 'Antarctica/Mawson': 'West Asia Standard Time', + 'Antarctica/McMurdo': 'New Zealand Standard Time', + 'Antarctica/Palmer': 'SA Eastern Standard Time', + 'Antarctica/Rothera': 'SA Eastern Standard Time', + 'Antarctica/Syowa': 'E. Africa Standard Time', + 'Antarctica/Vostok': 'Central Asia Standard Time', + 'Arctic/Longyearbyen': 'W. Europe Standard Time', + 'Asia/Aden': 'Arab Standard Time', + 'Asia/Almaty': 'West Asia Standard Time', + 'Asia/Amman': 'Jordan Standard Time', + 'Asia/Anadyr': 'Russia Time Zone 11', + 'Asia/Aqtau': 'West Asia Standard Time', + 'Asia/Aqtobe': 'West Asia Standard Time', + 'Asia/Ashgabat': 'West Asia Standard Time', + 'Asia/Atyrau': 'West Asia Standard Time', + 'Asia/Baghdad': 'Arabic Standard Time', + 'Asia/Bahrain': 'Arab Standard Time', + 'Asia/Baku': 'Azerbaijan Standard Time', + 'Asia/Bangkok': 'SE Asia Standard Time', + 'Asia/Barnaul': 'Altai Standard Time', + 'Asia/Beirut': 'Middle East Standard Time', + 'Asia/Bishkek': 'Central Asia Standard Time', + 'Asia/Brunei': 'Singapore Standard Time', + 'Asia/Calcutta': 'India Standard Time', + 'Asia/Chita': 'Transbaikal Standard Time', + 'Asia/Colombo': 'Sri Lanka Standard Time', + 'Asia/Damascus': 'Syria Standard Time', + 'Asia/Dhaka': 'Bangladesh Standard Time', + 'Asia/Dili': 'Tokyo Standard Time', + 'Asia/Dubai': 'Arabian Standard Time', + 'Asia/Dushanbe': 'West Asia Standard Time', + 'Asia/Famagusta': 'GTB Standard Time', + 'Asia/Gaza': 'West Bank Standard Time', + 'Asia/Hebron': 'West Bank Standard Time', + 'Asia/Hong_Kong': 'China Standard Time', + 'Asia/Hovd': 'W. Mongolia Standard Time', + 'Asia/Irkutsk': 'North Asia East Standard Time', + 'Asia/Jakarta': 'SE Asia Standard Time', + 'Asia/Jayapura': 'Tokyo Standard Time', + 'Asia/Jerusalem': 'Israel Standard Time', + 'Asia/Kabul': 'Afghanistan Standard Time', + 'Asia/Kamchatka': 'Russia Time Zone 11', + 'Asia/Karachi': 'Pakistan Standard Time', + 'Asia/Katmandu': 'Nepal Standard Time', + 'Asia/Khandyga': 'Yakutsk Standard Time', + 'Asia/Krasnoyarsk': 'North Asia Standard Time', + 'Asia/Kuala_Lumpur': 'Singapore Standard Time', + 'Asia/Kuching': 'Singapore Standard Time', + 'Asia/Kuwait': 'Arab Standard Time', + 'Asia/Macau': 'China Standard Time', + 'Asia/Magadan': 'Magadan Standard Time', + 'Asia/Makassar': 'Singapore Standard Time', + 'Asia/Manila': 'Singapore Standard Time', + 'Asia/Muscat': 'Arabian Standard Time', + 'Asia/Nicosia': 'GTB Standard Time', + 'Asia/Novokuznetsk': 'North Asia Standard Time', + 'Asia/Novosibirsk': 'N. Central Asia Standard Time', + 'Asia/Omsk': 'Omsk Standard Time', + 'Asia/Oral': 'West Asia Standard Time', + 'Asia/Phnom_Penh': 'SE Asia Standard Time', + 'Asia/Pontianak': 'SE Asia Standard Time', + 'Asia/Pyongyang': 'North Korea Standard Time', + 'Asia/Qatar': 'Arab Standard Time', + 'Asia/Qostanay': 'West Asia Standard Time', + 'Asia/Qyzylorda': 'Qyzylorda Standard Time', + 'Asia/Rangoon': 'Myanmar Standard Time', + 'Asia/Riyadh': 'Arab Standard Time', + 'Asia/Saigon': 'SE Asia Standard Time', + 'Asia/Sakhalin': 'Sakhalin Standard Time', + 'Asia/Samarkand': 'West Asia Standard Time', + 'Asia/Seoul': 'Korea Standard Time', + 'Asia/Shanghai': 'China Standard Time', + 'Asia/Singapore': 'Singapore Standard Time', + 'Asia/Srednekolymsk': 'Russia Time Zone 10', + 'Asia/Taipei': 'Taipei Standard Time', + 'Asia/Tashkent': 'West Asia Standard Time', + 'Asia/Tbilisi': 'Georgian Standard Time', + 'Asia/Tehran': 'Iran Standard Time', + 'Asia/Thimphu': 'Bangladesh Standard Time', + 'Asia/Tokyo': 'Tokyo Standard Time', + 'Asia/Tomsk': 'Tomsk Standard Time', + 'Asia/Ulaanbaatar': 'Ulaanbaatar Standard Time', + 'Asia/Urumqi': 'Central Asia Standard Time', + 'Asia/Ust-Nera': 'Vladivostok Standard Time', + 'Asia/Vientiane': 'SE Asia Standard Time', + 'Asia/Vladivostok': 'Vladivostok Standard Time', + 'Asia/Yakutsk': 'Yakutsk Standard Time', + 'Asia/Yekaterinburg': 'Ekaterinburg Standard Time', + 'Asia/Yerevan': 'Caucasus Standard Time', + 'Atlantic/Azores': 'Azores Standard Time', + 'Atlantic/Bermuda': 'Atlantic Standard Time', + 'Atlantic/Canary': 'GMT Standard Time', + 'Atlantic/Cape_Verde': 'Cape Verde Standard Time', + 'Atlantic/Faeroe': 'GMT Standard Time', + 'Atlantic/Madeira': 'GMT Standard Time', + 'Atlantic/Reykjavik': 'Greenwich Standard Time', + 'Atlantic/South_Georgia': 'UTC-02', + 'Atlantic/St_Helena': 'Greenwich Standard Time', + 'Atlantic/Stanley': 'SA Eastern Standard Time', + 'Australia/Adelaide': 'Cen. Australia Standard Time', + 'Australia/Brisbane': 'E. Australia Standard Time', + 'Australia/Broken_Hill': 'Cen. Australia Standard Time', + 'Australia/Darwin': 'AUS Central Standard Time', + 'Australia/Eucla': 'Aus Central W. Standard Time', + 'Australia/Hobart': 'Tasmania Standard Time', + 'Australia/Lindeman': 'E. Australia Standard Time', + 'Australia/Lord_Howe': 'Lord Howe Standard Time', + 'Australia/Melbourne': 'AUS Eastern Standard Time', + 'Australia/Perth': 'W. Australia Standard Time', + 'Australia/Sydney': 'AUS Eastern Standard Time', + 'Etc/GMT': 'UTC', + 'Etc/GMT+1': 'Cape Verde Standard Time', + 'Etc/GMT+10': 'Hawaiian Standard Time', + 'Etc/GMT+11': 'UTC-11', + 'Etc/GMT+12': 'Dateline Standard Time', + 'Etc/GMT+2': 'UTC-02', + 'Etc/GMT+3': 'SA Eastern Standard Time', + 'Etc/GMT+4': 'SA Western Standard Time', + 'Etc/GMT+5': 'SA Pacific Standard Time', + 'Etc/GMT+6': 'Central America Standard Time', + 'Etc/GMT+7': 'US Mountain Standard Time', + 'Etc/GMT+8': 'UTC-08', + 'Etc/GMT+9': 'UTC-09', + 'Etc/GMT-1': 'W. Central Africa Standard Time', + 'Etc/GMT-10': 'West Pacific Standard Time', + 'Etc/GMT-11': 'Central Pacific Standard Time', + 'Etc/GMT-12': 'UTC+12', + 'Etc/GMT-13': 'UTC+13', + 'Etc/GMT-14': 'Line Islands Standard Time', + 'Etc/GMT-2': 'South Africa Standard Time', + 'Etc/GMT-3': 'E. Africa Standard Time', + 'Etc/GMT-4': 'Arabian Standard Time', + 'Etc/GMT-5': 'West Asia Standard Time', + 'Etc/GMT-6': 'Central Asia Standard Time', + 'Etc/GMT-7': 'SE Asia Standard Time', + 'Etc/GMT-8': 'Singapore Standard Time', + 'Etc/GMT-9': 'Tokyo Standard Time', + 'Etc/UTC': 'UTC', + 'Europe/Amsterdam': 'W. Europe Standard Time', + 'Europe/Andorra': 'W. Europe Standard Time', + 'Europe/Astrakhan': 'Astrakhan Standard Time', + 'Europe/Athens': 'GTB Standard Time', + 'Europe/Belgrade': 'Central Europe Standard Time', + 'Europe/Berlin': 'W. Europe Standard Time', + 'Europe/Bratislava': 'Central Europe Standard Time', + 'Europe/Brussels': 'Romance Standard Time', + 'Europe/Bucharest': 'GTB Standard Time', + 'Europe/Budapest': 'Central Europe Standard Time', + 'Europe/Busingen': 'W. Europe Standard Time', + 'Europe/Chisinau': 'E. Europe Standard Time', + 'Europe/Copenhagen': 'Romance Standard Time', + 'Europe/Dublin': 'GMT Standard Time', + 'Europe/Gibraltar': 'W. Europe Standard Time', + 'Europe/Guernsey': 'GMT Standard Time', + 'Europe/Helsinki': 'FLE Standard Time', + 'Europe/Isle_of_Man': 'GMT Standard Time', + 'Europe/Istanbul': 'Turkey Standard Time', + 'Europe/Jersey': 'GMT Standard Time', + 'Europe/Kaliningrad': 'Kaliningrad Standard Time', + 'Europe/Kiev': 'FLE Standard Time', + 'Europe/Kirov': 'Russian Standard Time', + 'Europe/Lisbon': 'GMT Standard Time', + 'Europe/Ljubljana': 'Central Europe Standard Time', + 'Europe/London': 'GMT Standard Time', + 'Europe/Luxembourg': 'W. Europe Standard Time', + 'Europe/Madrid': 'Romance Standard Time', + 'Europe/Malta': 'W. Europe Standard Time', + 'Europe/Mariehamn': 'FLE Standard Time', + 'Europe/Minsk': 'Belarus Standard Time', + 'Europe/Monaco': 'W. Europe Standard Time', + 'Europe/Moscow': 'Russian Standard Time', + 'Europe/Oslo': 'W. Europe Standard Time', + 'Europe/Paris': 'Romance Standard Time', + 'Europe/Podgorica': 'Central Europe Standard Time', + 'Europe/Prague': 'Central Europe Standard Time', + 'Europe/Riga': 'FLE Standard Time', + 'Europe/Rome': 'W. Europe Standard Time', + 'Europe/Samara': 'Russia Time Zone 3', + 'Europe/San_Marino': 'W. Europe Standard Time', + 'Europe/Sarajevo': 'Central European Standard Time', + 'Europe/Saratov': 'Saratov Standard Time', + 'Europe/Simferopol': 'Russian Standard Time', + 'Europe/Skopje': 'Central European Standard Time', + 'Europe/Sofia': 'FLE Standard Time', + 'Europe/Stockholm': 'W. Europe Standard Time', + 'Europe/Tallinn': 'FLE Standard Time', + 'Europe/Tirane': 'Central Europe Standard Time', + 'Europe/Ulyanovsk': 'Astrakhan Standard Time', + 'Europe/Vaduz': 'W. Europe Standard Time', + 'Europe/Vatican': 'W. Europe Standard Time', + 'Europe/Vienna': 'W. Europe Standard Time', + 'Europe/Vilnius': 'FLE Standard Time', + 'Europe/Volgograd': 'Volgograd Standard Time', + 'Europe/Warsaw': 'Central European Standard Time', + 'Europe/Zagreb': 'Central European Standard Time', + 'Europe/Zurich': 'W. Europe Standard Time', + 'Indian/Antananarivo': 'E. Africa Standard Time', + 'Indian/Chagos': 'Central Asia Standard Time', + 'Indian/Christmas': 'SE Asia Standard Time', + 'Indian/Cocos': 'Myanmar Standard Time', + 'Indian/Comoro': 'E. Africa Standard Time', + 'Indian/Kerguelen': 'West Asia Standard Time', + 'Indian/Mahe': 'Mauritius Standard Time', + 'Indian/Maldives': 'West Asia Standard Time', + 'Indian/Mauritius': 'Mauritius Standard Time', + 'Indian/Mayotte': 'E. Africa Standard Time', + 'Indian/Reunion': 'Mauritius Standard Time', + 'Pacific/Apia': 'Samoa Standard Time', + 'Pacific/Auckland': 'New Zealand Standard Time', + 'Pacific/Bougainville': 'Bougainville Standard Time', + 'Pacific/Chatham': 'Chatham Islands Standard Time', + 'Pacific/Easter': 'Easter Island Standard Time', + 'Pacific/Efate': 'Central Pacific Standard Time', + 'Pacific/Enderbury': 'UTC+13', + 'Pacific/Fakaofo': 'UTC+13', + 'Pacific/Fiji': 'Fiji Standard Time', + 'Pacific/Funafuti': 'UTC+12', + 'Pacific/Galapagos': 'Central America Standard Time', + 'Pacific/Gambier': 'UTC-09', + 'Pacific/Guadalcanal': 'Central Pacific Standard Time', + 'Pacific/Guam': 'West Pacific Standard Time', + 'Pacific/Honolulu': 'Hawaiian Standard Time', + 'Pacific/Kiritimati': 'Line Islands Standard Time', + 'Pacific/Kosrae': 'Central Pacific Standard Time', + 'Pacific/Kwajalein': 'UTC+12', + 'Pacific/Majuro': 'UTC+12', + 'Pacific/Marquesas': 'Marquesas Standard Time', + 'Pacific/Midway': 'UTC-11', + 'Pacific/Nauru': 'UTC+12', + 'Pacific/Niue': 'UTC-11', + 'Pacific/Norfolk': 'Norfolk Standard Time', + 'Pacific/Noumea': 'Central Pacific Standard Time', + 'Pacific/Pago_Pago': 'UTC-11', + 'Pacific/Palau': 'Tokyo Standard Time', + 'Pacific/Pitcairn': 'UTC-08', + 'Pacific/Ponape': 'Central Pacific Standard Time', + 'Pacific/Port_Moresby': 'West Pacific Standard Time', + 'Pacific/Rarotonga': 'Hawaiian Standard Time', + 'Pacific/Saipan': 'West Pacific Standard Time', + 'Pacific/Tahiti': 'Hawaiian Standard Time', + 'Pacific/Tarawa': 'UTC+12', + 'Pacific/Tongatapu': 'Tonga Standard Time', + 'Pacific/Truk': 'West Pacific Standard Time', + 'Pacific/Wake': 'UTC+12', + 'Pacific/Wallis': 'UTC+12', + 'UTC': 'UTC', +}; + +export function resolveWindowsTimezone(iana: string): string { + const windowsTz = ianaToWindows[iana]; + if (!windowsTz) { + throw new Error( + `Unknown IANA timezone '${iana}'. Cannot convert to a Windows timezone name for MSSQL AT TIME ZONE.` + ); + } + + return windowsTz; +} diff --git a/packages/cubejs-schema-compiler/test/global-setup.ts b/packages/cubejs-schema-compiler/test/global-setup.ts index 67ca0db633f9c..e25d220b882c6 100644 --- a/packages/cubejs-schema-compiler/test/global-setup.ts +++ b/packages/cubejs-schema-compiler/test/global-setup.ts @@ -1,3 +1,5 @@ export default () => { process.env.TZ = 'UTC'; + process.env.CUBEJS_DB_MYSQL_USE_NAMED_TIMEZONES = 'true'; + process.env.CUBEJS_DB_MSSQL_USE_NAMED_TIMEZONES = 'true'; }; diff --git a/packages/cubejs-schema-compiler/test/integration/mysql/MySqlDbRunner.js b/packages/cubejs-schema-compiler/test/integration/mysql/MySqlDbRunner.js index 614e7c80e8e22..fdf31d9977b75 100644 --- a/packages/cubejs-schema-compiler/test/integration/mysql/MySqlDbRunner.js +++ b/packages/cubejs-schema-compiler/test/integration/mysql/MySqlDbRunner.js @@ -89,14 +89,20 @@ export class MySqlDbRunner extends BaseDbRunner { } async containerLazyInit() { - const version = process.env.TEST_MYSQL_VERSION || '5.7'; + const DEFAULT_VERSION = '5.7'; + const version = process.env.TEST_MYSQL_VERSION || DEFAULT_VERSION; - return new GenericContainer(`mysql:${version}`) + const container = new GenericContainer(`mysql:${version}`) .withEnvironment({ MYSQL_ROOT_PASSWORD: this.password() }) .withExposedPorts(this.port()) // workaround for MySQL 8 unsupported auth - .withCommand(['--default-authentication-plugin=mysql_native_password']) - .start(); + .withCommand(['--default-authentication-plugin=mysql_native_password']); + + if (process.platform === 'darwin' && process.arch === 'arm64' && version === DEFAULT_VERSION) { + container.withPlatform('linux/amd64'); + } + + return container.start(); } port() { From 4a4b67f50fcf6ced8ddb493ec90b3dc9f4c93b1e Mon Sep 17 00:00:00 2001 From: waralexrom <108349432+waralexrom@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:30:00 +0200 Subject: [PATCH 2/2] fix(tesseract): Filter by multi-stage measure (#10579) --- .../integration/postgres/multi-stage.test.ts | 19 ++++++++++ .../multi_stage/multi_stage_query_planner.rs | 2 +- .../src/planner/query_properties.rs | 38 ++++++++----------- .../tests/integration/multi_stage/filters.rs | 25 ++++++++++++ ...s__filter_on_only_multi_stage_measure.snap | 12 ++++++ 5 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/snapshots/cubesqlplanner__tests__integration__multi_stage__filters__filter_on_only_multi_stage_measure.snap diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage.test.ts index 6b344de4e10db..b12d4036e0b9e 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage.test.ts @@ -232,6 +232,25 @@ views: }, ], { joinGraph, cubeEvaluator, compiler })); + it('multi stage measure filter', async () => dbRunner.runQueryTest({ + dimensions: ['orders.status'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + filters: [ + { member: 'orders.cagr_1_y', operator: 'gt', values: ['1.5'] } + ], + timezone: 'UTC' + }, [ + { + orders__date_year: '2023-01-01T00:00:00.000Z', + orders__status: 'completed', + }, + ], + { joinGraph, cubeEvaluator, compiler })); } else { // This test is working only in tesseract test.skip('multi stage over sub query', () => { expect(1).toBe(1); }); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs index 3fc37dfc48e58..aa8d17200a1c3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs @@ -49,7 +49,7 @@ impl MultiStageQueryPlanner { > { let multi_stage_members = self .query_properties - .all_members(false) + .all_used_symbols()? .into_iter() .filter_map(|memb| -> Option> { match has_multi_stage_members(&memb, false) { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs index ba78638d9ad1a..b7b6a8871d85d 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs @@ -659,13 +659,7 @@ impl QueryProperties { if exclude_time_dimensions { dimensions.chain(measures).collect_vec() } else { - let time_dimensions = self.time_dimensions.iter().map(|d| { - /* if let Ok(td) = d.as_time_dimension() { - td.base_symbol().clone() - } else { */ - d.clone() - //} - }); + let time_dimensions = self.time_dimensions.iter().map(|d| d.clone()); dimensions .chain(time_dimensions) .chain(measures) @@ -673,6 +667,20 @@ impl QueryProperties { } } + pub fn all_used_symbols(&self) -> Result>, CubeError> { + let mut members = vec![]; + members.extend(self.time_dimensions.iter().cloned()); + members.extend(self.dimensions.iter().cloned()); + self.fill_all_filter_symbols(&mut members); + members.extend(self.all_used_measures()?); + + let res = members + .into_iter() + .unique_by(|m| m.full_name()) + .collect_vec(); + Ok(res) + } + pub fn get_member_symbols( &self, include_time_dimensions: bool, @@ -708,22 +716,6 @@ impl QueryProperties { } } - /* pub fn group_by(&self) -> Vec { - if self.ungrouped { - vec![] - } else { - self.dimensions - .iter() - .map(|f| Expr::Member(MemberExpression::new(f.clone()))) - .chain( - self.time_dimensions - .iter() - .map(|f| Expr::Member(MemberExpression::new(f.clone()))), - ) - .collect() - } - } */ - pub fn default_order( dimensions: &Vec>, time_dimensions: &Vec>, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/filters.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/filters.rs index 3c0af84a00c3b..18daca6aea240 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/filters.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/filters.rs @@ -173,3 +173,28 @@ async fn test_filter_on_multi_stage_measure() { insta::assert_snapshot!(result); } } + +#[tokio::test(flavor = "multi_thread")] +async fn test_filter_on_only_multi_stage_measure() { + let ctx = create_context(); + + let query = indoc! {r#" + dimensions: + - orders.status + - orders.category + filters: + - member: orders.amount_reduce_category + operator: gt + values: + - "200" + order: + - id: orders.status + - id: orders.category + "#}; + + ctx.build_sql(query).unwrap(); + + if let Some(result) = ctx.try_execute_pg(query, SEED).await { + insta::assert_snapshot!(result); + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/snapshots/cubesqlplanner__tests__integration__multi_stage__filters__filter_on_only_multi_stage_measure.snap b/rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/snapshots/cubesqlplanner__tests__integration__multi_stage__filters__filter_on_only_multi_stage_measure.snap new file mode 100644 index 0000000000000..0623c87fe9740 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/integration/multi_stage/snapshots/cubesqlplanner__tests__integration__multi_stage__filters__filter_on_only_multi_stage_measure.snap @@ -0,0 +1,12 @@ +--- +source: cubesqlplanner/src/tests/integration/multi_stage/filters.rs +expression: result +--- +orders__status | orders__category +---------------+----------------- +completed | books +completed | clothing +completed | electronics +pending | books +pending | clothing +pending | electronics