@malloydata/malloy-tests 0.0.323 → 0.0.324

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -21,22 +21,22 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@jest/globals": "^29.4.3",
24
- "@malloydata/db-bigquery": "0.0.323",
25
- "@malloydata/db-duckdb": "0.0.323",
26
- "@malloydata/db-postgres": "0.0.323",
27
- "@malloydata/db-snowflake": "0.0.323",
28
- "@malloydata/db-trino": "0.0.323",
29
- "@malloydata/malloy": "0.0.323",
30
- "@malloydata/malloy-tag": "0.0.323",
31
- "@malloydata/render": "0.0.323",
24
+ "@malloydata/db-bigquery": "0.0.324",
25
+ "@malloydata/db-duckdb": "0.0.324",
26
+ "@malloydata/db-postgres": "0.0.324",
27
+ "@malloydata/db-snowflake": "0.0.324",
28
+ "@malloydata/db-trino": "0.0.324",
29
+ "@malloydata/malloy": "0.0.324",
30
+ "@malloydata/malloy-tag": "0.0.324",
31
+ "@malloydata/render": "0.0.324",
32
32
  "events": "^3.3.0",
33
33
  "jsdom": "^22.1.0",
34
- "luxon": "^2.4.0",
34
+ "luxon": "^3.5.0",
35
35
  "madge": "^6.0.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/jsdom": "^21.1.1",
39
- "@types/luxon": "^2.4.0"
39
+ "@types/luxon": "^3.5.0"
40
40
  },
41
- "version": "0.0.323"
41
+ "version": "0.0.324"
42
42
  }
@@ -9,6 +9,7 @@ import {RuntimeList, allDatabases} from '../../runtimes';
9
9
  import '../../util/db-jest-matchers';
10
10
  import {databasesFromEnvironmentOr} from '../../util';
11
11
  import {DateTime as LuxonDateTime} from 'luxon';
12
+ import {Dialect} from '@malloydata/malloy';
12
13
 
13
14
  const runtimes = new RuntimeList(databasesFromEnvironmentOr(allDatabases));
14
15
 
@@ -491,17 +492,25 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
491
492
 
492
493
  describe('temporal filters', () => {
493
494
  function tsLit(at: LuxonDateTime): string {
494
- const typeDef: {type: 'timestamp' | 'date'} = {type: 'timestamp'};
495
- const node: {node: 'timeLiteral'} = {node: 'timeLiteral'};
496
495
  const timeStr = at.toUTC().toFormat(fTimestamp);
497
- const n = {...node, typeDef, literal: timeStr};
498
- return db.dialect.sqlLiteralTime({}, n);
496
+ const node = Dialect.makeTimeLiteralNode(
497
+ db.dialect,
498
+ timeStr,
499
+ undefined,
500
+ undefined,
501
+ 'timestamp'
502
+ );
503
+ return db.dialect.exprToSQL({}, node) || '';
499
504
  }
500
505
  function lit(t: string, type: 'timestamp' | 'date'): string {
501
- const typeDef: {type: 'timestamp' | 'date'} = {type};
502
- const node: {node: 'timeLiteral'} = {node: 'timeLiteral'};
503
- const n = {...node, typeDef, literal: t};
504
- return db.dialect.sqlLiteralTime({}, n);
506
+ const node = Dialect.makeTimeLiteralNode(
507
+ db.dialect,
508
+ t,
509
+ undefined,
510
+ undefined,
511
+ type
512
+ );
513
+ return db.dialect.exprToSQL({}, node) || '';
505
514
  }
506
515
 
507
516
  const fTimestamp = 'yyyy-LL-dd HH:mm:ss';
@@ -1050,9 +1059,9 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
1050
1059
  {n: 'z-null'}
1051
1060
  );
1052
1061
  });
1053
- const tzTesting = dbName !== 'presto' && dbName !== 'trino';
1062
+ const tzTesting = true;
1054
1063
  describe('query time zone', () => {
1055
- test.when(tzTesting)('day literal in query time zone', async () => {
1064
+ test.when(tzTesting)('date literal in query time zone', async () => {
1056
1065
  const rangeQuery = mkRangeQuery(
1057
1066
  "f'2024-01-01'",
1058
1067
  '2024-01-01 00:00:00',
@@ -1061,26 +1070,32 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
1061
1070
  );
1062
1071
  await expect(rangeQuery).malloyResultMatches(db, inRange);
1063
1072
  });
1064
- test.when(tzTesting)('day literal in query time zone', async () => {
1065
- nowIs('2024-01-15 00:34:56', 'America/Mexico_City');
1066
- const rangeQuery = mkRangeQuery(
1067
- "f'today'",
1068
- '2024-01-15 00:00:00',
1069
- '2024-01-16 00:00:00',
1070
- 'America/Mexico_City'
1071
- );
1072
- await expect(rangeQuery).malloyResultMatches(db, inRange);
1073
- });
1074
- test.when(tzTesting)('day literal in query time zone', async () => {
1075
- nowIs('2024-01-01 00:00:00', 'America/Mexico_City');
1076
- const rangeQuery = mkRangeQuery(
1077
- "f'next wednesday'",
1078
- '2024-01-03 00:00:00',
1079
- '2024-01-04 00:00:00',
1080
- 'America/Mexico_City'
1081
- );
1082
- await expect(rangeQuery).malloyResultMatches(db, inRange);
1083
- });
1073
+ test.when(tzTesting)(
1074
+ 'timestamp literal today in query time zone',
1075
+ async () => {
1076
+ nowIs('2024-01-15 00:34:56', 'America/Mexico_City');
1077
+ const rangeQuery = mkRangeQuery(
1078
+ "f'today'",
1079
+ '2024-01-15 00:00:00',
1080
+ '2024-01-16 00:00:00',
1081
+ 'America/Mexico_City'
1082
+ );
1083
+ await expect(rangeQuery).malloyResultMatches(db, inRange);
1084
+ }
1085
+ );
1086
+ test.when(tzTesting)(
1087
+ 'timestamp literal next week in query time zone',
1088
+ async () => {
1089
+ nowIs('2024-01-01 00:00:00', 'America/Mexico_City');
1090
+ const rangeQuery = mkRangeQuery(
1091
+ "f'next wednesday'",
1092
+ '2024-01-03 00:00:00',
1093
+ '2024-01-04 00:00:00',
1094
+ 'America/Mexico_City'
1095
+ );
1096
+ await expect(rangeQuery).malloyResultMatches(db, inRange);
1097
+ }
1098
+ );
1084
1099
  test.when(tzTesting)('day literal in query time zone', async () => {
1085
1100
  const exactTimeModel = mkEqTime('2024-01-15 12:00:00');
1086
1101
  await expect(`
@@ -25,6 +25,7 @@
25
25
  import {RuntimeList, allDatabases} from '../../runtimes';
26
26
  import {databasesFromEnvironmentOr} from '../../util';
27
27
  import '../../util/db-jest-matchers';
28
+ import {Dialect} from '@malloydata/malloy';
28
29
 
29
30
  const runtimes = new RuntimeList(databasesFromEnvironmentOr(allDatabases));
30
31
 
@@ -277,33 +278,33 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
277
278
  ]);
278
279
  }
279
280
  );
280
- const y2020 = runtime.dialect.sqlLiteralTime(
281
- {},
282
- {
283
- node: 'timeLiteral',
284
- literal: '2020-01-01 00:00:00',
285
- typeDef: {type: 'timestamp'},
286
- }
281
+ const y2020Node = Dialect.makeTimeLiteralNode(
282
+ runtime.dialect,
283
+ '2020-01-01 00:00:00',
284
+ undefined,
285
+ undefined,
286
+ 'timestamp'
287
287
  );
288
+ const y2020 = runtime.dialect.exprToSQL({}, y2020Node) || '';
288
289
  const d2020 = new Date('2020-01-01 00:00:00Z');
289
290
  const d2022 = new Date('2022-01-01 00:00:00Z');
290
291
  const d2025 = new Date('2025-01-01 00:00:00Z');
291
- const y2025 = runtime.dialect.sqlLiteralTime(
292
- {},
293
- {
294
- node: 'timeLiteral',
295
- literal: '2025-01-01 00:00:00',
296
- typeDef: {type: 'timestamp'},
297
- }
292
+ const y2025Node = Dialect.makeTimeLiteralNode(
293
+ runtime.dialect,
294
+ '2025-01-01 00:00:00',
295
+ undefined,
296
+ undefined,
297
+ 'timestamp'
298
298
  );
299
- const y2022 = runtime.dialect.sqlLiteralTime(
300
- {},
301
- {
302
- node: 'timeLiteral',
303
- literal: '2022-01-01 00:00:00',
304
- typeDef: {type: 'timestamp'},
305
- }
299
+ const y2025 = runtime.dialect.exprToSQL({}, y2025Node) || '';
300
+ const y2022Node = Dialect.makeTimeLiteralNode(
301
+ runtime.dialect,
302
+ '2022-01-01 00:00:00',
303
+ undefined,
304
+ undefined,
305
+ 'timestamp'
306
306
  );
307
+ const y2022 = runtime.dialect.exprToSQL({}, y2022Node) || '';
307
308
  const times = `${databaseName}.sql("""
308
309
  SELECT ${y2020} as ${q`t`}
309
310
  UNION ALL SELECT ${y2025}
@@ -32,14 +32,24 @@ import {
32
32
  } from '../../util';
33
33
  import {DateTime as LuxonDateTime} from 'luxon';
34
34
  import {API} from '@malloydata/malloy';
35
+ import {TestSelect} from '../../test-select';
35
36
 
36
37
  const runtimes = new RuntimeList(databasesFromEnvironmentOr(allDatabases));
37
38
 
38
39
  // MTOY todo look at this list for timezone problems, I know there are some
39
40
  describe.each(runtimes.runtimeList)('%s date and time', (dbName, runtime) => {
40
- const q = runtime.getQuoter();
41
-
42
- const timeSQL = `SELECT DATE '2021-02-24' as ${q`t_date`}, TIMESTAMP '2021-02-24 03:05:06' as ${q`t_timestamp`} `;
41
+ const ts = new TestSelect(runtime.dialect);
42
+ const timestamptz = runtime.dialect.hasTimestamptz;
43
+ const timeSchema = {
44
+ t_date: ts.mk_date('2021-02-24'),
45
+ t_timestamp: ts.mk_timestamp('2021-02-24 03:05:06'),
46
+ };
47
+ if (timestamptz) {
48
+ timeSchema['t_timestamptz'] = ts.mk_timestamptz(
49
+ '2021-02-24 03:05:06 [UTC]'
50
+ );
51
+ }
52
+ const timeSQL = ts.generate(timeSchema);
43
53
  const sqlEq = mkSqlEqWith(runtime, dbName, {sql: timeSQL});
44
54
 
45
55
  describe('interval measurement', () => {
@@ -179,6 +189,14 @@ describe.each(runtimes.runtimeList)('%s date and time', (dbName, runtime) => {
179
189
  expect(await eq).isSqlEq();
180
190
  });
181
191
 
192
+ test.when(timestamptz)('trunc timestamptz day', async () => {
193
+ await expect(`
194
+ run: ${dbName}.sql("""${timeSQL}""") -> {
195
+ select: result is t_timestamptz.day
196
+ }
197
+ `).malloyResultMatches(runtime, {result: '2021-02-24 00:00:00Z'});
198
+ });
199
+
182
200
  test('trunc week', async () => {
183
201
  const eq = sqlEq('t_timestamp.week', '@2021-02-21 00:00:00');
184
202
  expect(await eq).isSqlEq();
@@ -247,6 +265,24 @@ describe.each(runtimes.runtimeList)('%s date and time', (dbName, runtime) => {
247
265
  expect(await eq).isSqlEq();
248
266
  });
249
267
  });
268
+
269
+ test.when(runtime.dialect.hasTimestamptz)(
270
+ 'extract from timestamptz without query timezone',
271
+ async () => {
272
+ // TIMESTAMPTZ representing midnight UTC
273
+ // Without query timezone, extract should happen in UTC (or stored tz for Trino)
274
+ // Expected: hour = 0, day = 20
275
+ await expect(
276
+ `run: ${dbName}.sql("SELECT 1 as x") -> {
277
+ extend: { dimension: utc_tstz is @2020-02-20 00:00:00[UTC]::timestamptz }
278
+ select:
279
+ utc_hour is hour(utc_tstz)
280
+ utc_day is day(utc_tstz)
281
+ }`
282
+ ).malloyResultMatches(runtime, {utc_hour: 0, utc_day: 20});
283
+ }
284
+ );
285
+
250
286
  describe('date truncation', () => {
251
287
  test('date trunc day', async () => {
252
288
  const eq = sqlEq('t_date.day', '@2021-02-24');
@@ -473,9 +509,7 @@ describe.each(runtimes.runtimeList)('%s date and time', (dbName, runtime) => {
473
509
  });
474
510
  });
475
511
 
476
- test.when(
477
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
478
- )('dependant join dialect fragments', async () => {
512
+ test('dependant join dialect fragments', async () => {
479
513
  await expect(`
480
514
  source: timeData is ${dbName}.sql("""${timeSQL}""")
481
515
  run: timeData -> {
@@ -631,9 +665,7 @@ const utc_2020 = LuxonDateTime.fromObject(
631
665
  );
632
666
 
633
667
  describe.each(runtimes.runtimeList)('%s: tz literals', (dbName, runtime) => {
634
- test.when(
635
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
636
- )(`${dbName} - default timezone is UTC`, async () => {
668
+ test(`${dbName} - default timezone is UTC`, async () => {
637
669
  // this makes sure that the tests which use the test timezome are actually
638
670
  // testing something ... file this under "abundance of caution". It
639
671
  // really tests nothing, but I feel calmer with this here.
@@ -650,9 +682,7 @@ describe.each(runtimes.runtimeList)('%s: tz literals', (dbName, runtime) => {
650
682
  expect(have.valueOf()).toEqual(utc_2020.valueOf());
651
683
  });
652
684
 
653
- test.when(
654
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
655
- )('literal with zone name', async () => {
685
+ test('literal with zone name', async () => {
656
686
  const query = runtime.loadQuery(
657
687
  `
658
688
  run: ${dbName}.sql("SELECT 1 as one") -> {
@@ -668,10 +698,12 @@ describe.each(runtimes.runtimeList)('%s: tz literals', (dbName, runtime) => {
668
698
  });
669
699
 
670
700
  describe.each(runtimes.runtimeList)('%s: query tz', (dbName, runtime) => {
671
- const q = runtime.getQuoter();
672
- test.when(
673
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
674
- )('literal timestamps', async () => {
701
+ const ts = new TestSelect(runtime.dialect);
702
+ const selectMidnight = `"""${ts.generate({
703
+ utc_midnight_ts: ts.mk_timestamp('2020-02-20 00:00:00'),
704
+ date_2020: ts.mk_date('2020-02-20'),
705
+ })}"""`;
706
+ test('literal timestamps', async () => {
675
707
  const query = runtime.loadQuery(
676
708
  `
677
709
  run: ${dbName}.sql("SELECT 1 as one") -> {
@@ -698,24 +730,37 @@ describe.each(runtimes.runtimeList)('%s: query tz', (dbName, runtime) => {
698
730
  ).malloyResultMatches(runtime, {mex_midnight: 18, mex_day: 19});
699
731
  });
700
732
 
701
- test.when(
702
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
703
- )('truncate day', async () => {
733
+ test.when(runtime.dialect.hasTimestamptz)(
734
+ 'extract from timestamptz with query timezone',
735
+ async () => {
736
+ // TIMESTAMPTZ representing midnight UTC
737
+ // With query timezone America/Mexico_City (-06:00), midnight UTC = 6pm Feb 19
738
+ // Expected: hour = 18, day = 19
739
+ await expect(
740
+ `run: ${dbName}.sql("SELECT 1 as x") -> {
741
+ timezone: '${zone}'
742
+ extend: { dimension: utc_tstz is @2020-02-20 00:00:00[UTC]::timestamptz }
743
+ select:
744
+ mex_hour is hour(utc_tstz)
745
+ mex_day is day(utc_tstz)
746
+ }`
747
+ ).malloyResultMatches(runtime, {mex_hour: 18, mex_day: 19});
748
+ }
749
+ );
750
+
751
+ test('truncate day', async () => {
704
752
  // At midnight in london it the 19th in Mexico, so that truncates to
705
753
  // midnight on the 19th
706
754
  const mex_19 = LuxonDateTime.fromISO('2020-02-19T00:00:00', {zone});
707
755
  await expect(
708
- `run: ${dbName}.sql("SELECT 1 as x") -> {
756
+ `run: ${dbName}.sql(${selectMidnight}) -> {
709
757
  timezone: '${zone}'
710
- extend: { dimension: utc_midnight is @2020-02-20 00:00:00[UTC] }
711
- select: mex_day is utc_midnight.day
758
+ select: mex_day is utc_midnight_ts.day
712
759
  }`
713
760
  ).malloyResultMatches(runtime, {mex_day: mex_19.toJSDate()});
714
761
  });
715
762
 
716
- test.when(
717
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
718
- )('truncate week', async () => {
763
+ test('truncate week', async () => {
719
764
  // the 19th in mexico is a wednesday, so trunc to the 15th
720
765
  const mex_19 = LuxonDateTime.fromISO('2020-02-19T00:00:00', {zone});
721
766
  // Find the sunday before then
@@ -729,30 +774,53 @@ describe.each(runtimes.runtimeList)('%s: query tz', (dbName, runtime) => {
729
774
  ).malloyResultMatches(runtime, {mex_week: mex_sunday.toJSDate()});
730
775
  });
731
776
 
732
- test.when(
733
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
734
- )('cast timestamp to date', async () => {
735
- // At midnight in london it is the 19th in Mexico, so when we cast that
736
- // to a date, it should be the 19th.
777
+ test('cast timestamp to date', async () => {
737
778
  await expect(
738
- `run: ${dbName}.sql("SELECT 1 as x") -> {
779
+ `run: ${dbName}.sql(${selectMidnight}) -> {
739
780
  timezone: '${zone}'
740
- extend: { dimension: utc_midnight is @2020-02-20 00:00:00[UTC] }
741
- select: mex_day is day(utc_midnight::date)
781
+ select: mex_date is utc_midnight_ts::date
742
782
  }`
743
- ).malloyResultMatches(runtime, {mex_day: 19});
783
+ ).malloyResultMatches(runtime, {mex_date: '2020-02-19'});
744
784
  });
745
-
746
- test.when(
747
- !brokenIn('trino', dbName) && !brokenIn('presto', dbName) /* mtoy */
748
- )('cast date to timestamp', async () => {
785
+ test.when(runtime.dialect.hasTimestamptz)(
786
+ 'cast timestamptz to date',
787
+ async () => {
788
+ await expect(
789
+ `run: ${dbName}.sql("SELECT 1 as x") -> {
790
+ timezone: '${zone}'
791
+ extend: { dimension: utc_tstz is @2020-02-20 00:00:00[UTC]::timestamptz }
792
+ select: mex_date is utc_tstz::date
793
+ }`
794
+ ).malloyResultMatches(runtime, {mex_date: '2020-02-19'});
795
+ }
796
+ );
797
+ test('cast date to timestamp', async () => {
749
798
  await expect(
750
- `run: ${dbName}.sql(""" SELECT DATE '2020-02-20' AS ${q`mex_20`} """) -> {
799
+ `run: ${dbName}.sql(${selectMidnight}) -> {
751
800
  timezone: '${zone}'
752
- select: mex_ts is mex_20::timestamp
801
+ select: mex_date is date_2020::timestamp
753
802
  }`
754
- ).malloyResultMatches(runtime, {mex_ts: zone_2020.toJSDate()});
803
+ ).malloyResultMatches(runtime, {mex_date: zone_2020.toJSDate()});
755
804
  });
805
+ test('return date 2020-02-20', async () => {
806
+ await expect(
807
+ `run: ${dbName}.sql(${selectMidnight}) -> {
808
+ timezone: '${zone}'
809
+ select: d2020 is date_2020
810
+ }`
811
+ ).malloyResultMatches(runtime, {d2020: '2020-02-20'});
812
+ });
813
+ test.when(runtime.dialect.hasTimestamptz)(
814
+ 'cast date to timestamptz',
815
+ async () => {
816
+ await expect(
817
+ `run: ${dbName}.sql(${selectMidnight}) -> {
818
+ timezone: '${zone}'
819
+ select: mex_date is date_2020::timestamptz
820
+ }`
821
+ ).malloyResultMatches(runtime, {mex_date: zone_2020.toJSDate()});
822
+ }
823
+ );
756
824
 
757
825
  // Test for timezone rendering issue with nested queries
758
826
  test.when(runtime.supportsNesting)(
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import {DateTime} from 'luxon';
25
- import {RuntimeList} from '../../runtimes';
25
+ import {RuntimeList, runtimeFor} from '../../runtimes';
26
26
  import '../../util/db-jest-matchers';
27
27
  import {describeIfDatabaseAvailable} from '../../util';
28
28
 
@@ -118,11 +118,17 @@ describe.each(allDucks.runtimeList)('duckdb:%s', (dbName, runtime) => {
118
118
  });
119
119
 
120
120
  it('supports timezones', async () => {
121
- await runtime.connection.runSQL("SET TimeZone='CET'");
122
- const result = await runtime.connection.runSQL(
123
- "SELECT current_setting('TimeZone')"
124
- );
125
- expect(result.rows[0]).toEqual({"current_setting('TimeZone')": 'CET'});
121
+ // Use isolated connection to avoid affecting other tests
122
+ const isolatedRuntime = runtimeFor(dbName);
123
+ try {
124
+ await isolatedRuntime.connection.runSQL("SET TimeZone='CET'");
125
+ const result = await isolatedRuntime.connection.runSQL(
126
+ "SELECT current_setting('TimeZone')"
127
+ );
128
+ expect(result.rows[0]).toEqual({"current_setting('TimeZone')": 'CET'});
129
+ } finally {
130
+ await isolatedRuntime.connection.close();
131
+ }
126
132
  });
127
133
 
128
134
  it('supports varchars with parameters', async () => {
@@ -115,7 +115,7 @@ describe.each(runtimes.runtimeList)(
115
115
  });
116
116
 
117
117
  it(`runs the date_parse function - ${databaseName}`, async () => {
118
- const expected = Date.parse('15 Sep 2024 00:00:00 UTC');
118
+ const expected = '2024-09-15T00:00:00Z';
119
119
 
120
120
  await expect(`run: ${databaseName}.sql("SELECT 1 as n") -> {
121
121
  select: x is date_parse('2024-09-15', '%Y-%m-%d')::date
@@ -265,7 +265,7 @@ export class TestSelect {
265
265
 
266
266
  return {
267
267
  expr: {
268
- node: 'timeLiteral',
268
+ node: 'dateLiteral',
269
269
  literal: value,
270
270
  typeDef: {type: 'date'},
271
271
  },
@@ -294,7 +294,7 @@ export class TestSelect {
294
294
 
295
295
  return {
296
296
  expr: {
297
- node: 'timeLiteral',
297
+ node: 'timestampLiteral',
298
298
  literal: value,
299
299
  typeDef: {type: 'timestamp'},
300
300
  },
@@ -303,6 +303,42 @@ export class TestSelect {
303
303
  };
304
304
  }
305
305
 
306
+ mk_timestamptz(value: string | null): TypedValue {
307
+ const malloyType: AtomicTypeDef = {type: 'timestamptz'};
308
+
309
+ if (value === null) {
310
+ const castExpr: TypecastExpr = {
311
+ node: 'cast',
312
+ e: nullExpr,
313
+ dstType: {type: 'timestamptz'},
314
+ safe: false,
315
+ };
316
+
317
+ return {
318
+ expr: castExpr,
319
+ malloyType,
320
+ needsCast: true,
321
+ };
322
+ }
323
+
324
+ const match = value.match(/^(.+?)\s*\[(.+?)\]$/);
325
+ if (!match) {
326
+ throw new Error(`Invalid timestamptz format: ${value}. Expected format: 'YYYY-MM-DD
327
+ HH:MM:SS [Timezone]'`);
328
+ }
329
+ const [, ts, timezone] = match;
330
+ return {
331
+ expr: {
332
+ node: 'timestamptzLiteral',
333
+ literal: ts.trim(),
334
+ typeDef: {type: 'timestamptz'},
335
+ timezone,
336
+ },
337
+ malloyType,
338
+ needsCast: false,
339
+ };
340
+ }
341
+
306
342
  mk_array(values: TestValue[]): TypedValue {
307
343
  if (values.length === 0) {
308
344
  throw new Error(
@@ -207,8 +207,33 @@ expect.extend({
207
207
  }
208
208
  const got = result.data.path(...resultPath).value;
209
209
  const pGot = JSON.stringify(got);
210
- const mustBe = value instanceof Date ? value.getTime() : value;
211
- const actuallyGot = got instanceof Date ? got.getTime() : got;
210
+
211
+ let mustBe = value;
212
+ let actuallyGot = got;
213
+
214
+ // If the value is a Date this is some sort of temporal column.
215
+ // If the expected looks like a 'YYYY-MM-DD' value, then expect the
216
+ // Date to be YYYY-MM-DD 00:00:00Z
217
+ // When comparing Date values, we use getTime to verify in a safe way
218
+ // that the correct instant is returned.
219
+ if (got instanceof Date) {
220
+ actuallyGot = got.getTime();
221
+ mustBe = typeof value === 'string' ? new Date(value) : value;
222
+ if (mustBe instanceof Date) {
223
+ mustBe = mustBe.getTime();
224
+ }
225
+ }
226
+ // If expected is a date string like 'YYYY-MM-DD', compare as date strings
227
+ else if (
228
+ typeof value === 'string' &&
229
+ /^\d{4}-\d{2}-\d{2}$/.test(value)
230
+ ) {
231
+ if (got instanceof Date) {
232
+ actuallyGot = got.toISOString().split('T')[0];
233
+ }
234
+ // mustBe stays as the string value
235
+ }
236
+
212
237
  if (typeof mustBe === 'number' && typeof actuallyGot !== 'number') {
213
238
  fails.push(`${expected} Got: Non Numeric '${pGot}'`);
214
239
  } else if (!objectsMatch(actuallyGot, mustBe)) {
@@ -11,9 +11,8 @@ import type {
11
11
  ModelMaterializer,
12
12
  QueryMaterializer,
13
13
  LogMessage,
14
- Dialect,
15
14
  } from '@malloydata/malloy';
16
- import {API, MalloyError} from '@malloydata/malloy';
15
+ import {API, MalloyError, Dialect} from '@malloydata/malloy';
17
16
  import type {Tag} from '@malloydata/malloy-tag';
18
17
 
19
18
  type JestMatcherResult = {
@@ -130,20 +129,22 @@ function errorLogToString(src: string, msgs: LogMessage[]) {
130
129
  return lovely;
131
130
  }
132
131
 
133
- type TL = 'timeLiteral';
134
-
135
- function lit(d: Dialect, t: string, type: 'timestamp' | 'date'): string {
136
- const typeDef: {type: 'timestamp' | 'date'} = {type};
137
- const timeLiteral: TL = 'timeLiteral';
138
- const n = {
139
- node: timeLiteral,
140
- typeDef,
141
- literal: t,
142
- };
143
- return d.sqlLiteralTime({}, n);
132
+ function lit(
133
+ d: Dialect,
134
+ t: string,
135
+ type: 'timestamp' | 'timestamptz' | 'date'
136
+ ): string {
137
+ const node = Dialect.makeTimeLiteralNode(d, t, undefined, undefined, type);
138
+ return d.exprToSQL({}, node) || '';
144
139
  }
145
140
 
146
- type SQLDataType = 'string' | 'number' | 'timestamp' | 'date' | 'boolean';
141
+ type SQLDataType =
142
+ | 'string'
143
+ | 'number'
144
+ | 'timestamp'
145
+ | 'timestamptz'
146
+ | 'date'
147
+ | 'boolean';
147
148
  type SQLRow = unknown[];
148
149
 
149
150
  /**
@@ -177,6 +178,8 @@ export function mkSQLSource(
177
178
  valStr = 'NULL';
178
179
  } else if (schema[colName] === 'timestamp' && typeof val === 'string') {
179
180
  valStr = lit(dialect, val, 'timestamp');
181
+ } else if (schema[colName] === 'timestamptz' && typeof val === 'string') {
182
+ valStr = lit(dialect, val, 'timestamptz');
180
183
  } else if (schema[colName] === 'date' && typeof val === 'string') {
181
184
  valStr = lit(dialect, val, 'date');
182
185
  }