@malloydata/malloy 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.
Files changed (53) hide show
  1. package/dist/api/core.js +2 -0
  2. package/dist/api/util.js +16 -24
  3. package/dist/dialect/dialect.d.ts +113 -23
  4. package/dist/dialect/dialect.js +83 -13
  5. package/dist/dialect/duckdb/duckdb.d.ts +1 -4
  6. package/dist/dialect/duckdb/duckdb.js +13 -17
  7. package/dist/dialect/mysql/mysql.d.ts +9 -4
  8. package/dist/dialect/mysql/mysql.js +18 -11
  9. package/dist/dialect/pg_impl.d.ts +11 -2
  10. package/dist/dialect/pg_impl.js +79 -15
  11. package/dist/dialect/postgres/postgres.d.ts +1 -4
  12. package/dist/dialect/postgres/postgres.js +6 -18
  13. package/dist/dialect/snowflake/snowflake.d.ts +10 -4
  14. package/dist/dialect/snowflake/snowflake.js +80 -31
  15. package/dist/dialect/standardsql/standardsql.d.ts +9 -4
  16. package/dist/dialect/standardsql/standardsql.js +21 -19
  17. package/dist/dialect/tiny_parser.js +1 -1
  18. package/dist/dialect/trino/trino.d.ts +19 -6
  19. package/dist/dialect/trino/trino.js +163 -31
  20. package/dist/index.d.ts +1 -1
  21. package/dist/lang/ast/expressions/expr-granular-time.js +26 -8
  22. package/dist/lang/ast/expressions/expr-props.d.ts +24 -0
  23. package/dist/lang/ast/expressions/for-range.d.ts +1 -1
  24. package/dist/lang/ast/expressions/for-range.js +5 -4
  25. package/dist/lang/ast/expressions/time-literal.d.ts +9 -7
  26. package/dist/lang/ast/expressions/time-literal.js +43 -50
  27. package/dist/lang/ast/query-items/field-declaration.js +1 -2
  28. package/dist/lang/ast/time-utils.js +1 -1
  29. package/dist/lang/ast/typedesc-utils.d.ts +1 -0
  30. package/dist/lang/ast/typedesc-utils.js +14 -1
  31. package/dist/lang/ast/types/expression-def.js +1 -1
  32. package/dist/lang/ast/types/granular-result.js +2 -1
  33. package/dist/lang/composite-source-utils.js +1 -1
  34. package/dist/lang/lib/Malloy/MalloyLexer.d.ts +76 -75
  35. package/dist/lang/lib/Malloy/MalloyLexer.js +1252 -1243
  36. package/dist/lang/lib/Malloy/MalloyParser.d.ts +77 -75
  37. package/dist/lang/lib/Malloy/MalloyParser.js +515 -510
  38. package/dist/lang/malloy-to-stable-query.js +13 -14
  39. package/dist/lang/test/expr-to-str.js +5 -1
  40. package/dist/malloy.d.ts +3 -2
  41. package/dist/malloy.js +6 -0
  42. package/dist/model/field_instance.js +1 -1
  43. package/dist/model/filter_compilers.d.ts +2 -1
  44. package/dist/model/filter_compilers.js +8 -5
  45. package/dist/model/malloy_types.d.ts +31 -9
  46. package/dist/model/malloy_types.js +49 -6
  47. package/dist/model/query_node.d.ts +2 -2
  48. package/dist/model/query_node.js +1 -0
  49. package/dist/model/query_query.js +15 -3
  50. package/dist/to_stable.js +13 -1
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +6 -6
@@ -18,13 +18,17 @@ exports.timeExtractMap = {
18
18
  * same implementations for the much of the SQL code generation
19
19
  */
20
20
  class PostgresBase extends dialect_1.Dialect {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.hasTimestamptz = true;
24
+ }
21
25
  sqlNowExpr() {
22
26
  return 'LOCALTIMESTAMP';
23
27
  }
24
28
  sqlTimeExtractExpr(qi, from) {
25
29
  const units = exports.timeExtractMap[from.units] || from.units;
26
30
  let extractFrom = from.e.sql;
27
- if (malloy_types_1.TD.isTimestamp(from.e.typeDef)) {
31
+ if (malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
28
32
  const tz = (0, dialect_1.qtz)(qi);
29
33
  if (tz) {
30
34
  extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE '${tz}')`;
@@ -35,15 +39,32 @@ class PostgresBase extends dialect_1.Dialect {
35
39
  }
36
40
  sqlCast(qi, cast) {
37
41
  const expr = cast.e.sql || '';
38
- const { op, srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
42
+ const { srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
39
43
  const tz = (0, dialect_1.qtz)(qi);
40
- if (op === 'timestamp::date' && tz) {
41
- const tstz = `${expr}::TIMESTAMPTZ`;
42
- return `CAST((${tstz}) AT TIME ZONE '${tz}' AS DATE)`;
43
- }
44
- else if (op === 'date::timestamp' && tz) {
45
- return `CAST((${expr})::TIMESTAMP AT TIME ZONE '${tz}' AS TIMESTAMP)`;
44
+ // Timezone-aware casts when query timezone is set
45
+ if (tz && srcTypeDef && dstTypeDef) {
46
+ // TIMESTAMP DATE: convert via TIMESTAMPTZ to query timezone
47
+ if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
48
+ return `CAST((${expr}::TIMESTAMPTZ) AT TIME ZONE '${tz}' AS DATE)`;
49
+ }
50
+ // TIMESTAMPTZ → DATE: convert to query timezone
51
+ if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
52
+ return `CAST((${expr}) AT TIME ZONE '${tz}' AS DATE)`;
53
+ }
54
+ // DATE → TIMESTAMP: interpret date in query timezone, return UTC timestamp
55
+ if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
56
+ return `CAST((${expr})::TIMESTAMP AT TIME ZONE '${tz}' AS TIMESTAMP)`;
57
+ }
58
+ // DATE → TIMESTAMPTZ: interpret date in query timezone
59
+ if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
60
+ return `(${expr})::TIMESTAMP AT TIME ZONE '${tz}'`;
61
+ }
62
+ // TIMESTAMPTZ → TIMESTAMP: convert to query timezone (returns TIMESTAMP)
63
+ if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
64
+ return `(${expr}) AT TIME ZONE '${tz}'`;
65
+ }
46
66
  }
67
+ // No special handling needed, or no query timezone
47
68
  if (!malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
48
69
  const castFunc = cast.safe ? 'TRY_CAST' : 'CAST';
49
70
  return `${castFunc}(${expr} AS ${dstSQLType})`;
@@ -53,15 +74,18 @@ class PostgresBase extends dialect_1.Dialect {
53
74
  sqlRegexpMatch(df) {
54
75
  return `${df.kids.expr.sql} ~ ${df.kids.regex.sql}`;
55
76
  }
56
- sqlLiteralTime(qi, lt) {
57
- if (malloy_types_1.TD.isDate(lt.typeDef)) {
58
- return `DATE '${lt.literal}'`;
59
- }
60
- const tz = lt.timezone || (0, dialect_1.qtz)(qi);
77
+ sqlDateLiteral(_qi, literal) {
78
+ return `DATE '${literal}'`;
79
+ }
80
+ sqlTimestampLiteral(qi, literal, timezone) {
81
+ const tz = timezone || (0, dialect_1.qtz)(qi);
61
82
  if (tz) {
62
- return `TIMESTAMPTZ '${lt.literal} ${tz}'::TIMESTAMP`;
83
+ return `TIMESTAMPTZ '${literal} ${tz}'::TIMESTAMP`;
63
84
  }
64
- return `TIMESTAMP '${lt.literal}'`;
85
+ return `TIMESTAMP '${literal}'`;
86
+ }
87
+ sqlTimestamptzLiteral(_qi, literal, timezone) {
88
+ return `TIMESTAMPTZ '${literal} ${timezone}'`;
65
89
  }
66
90
  sqlLiteralRecord(_lit) {
67
91
  throw new Error('Cannot create a record literal for postgres base dialect');
@@ -73,6 +97,46 @@ class PostgresBase extends dialect_1.Dialect {
73
97
  sqlMaybeQuoteIdentifier(identifier) {
74
98
  return '"' + identifier.replace(/"/g, '""') + '"';
75
99
  }
100
+ sqlConvertToCivilTime(expr, timezone, typeDef) {
101
+ // PostgreSQL/DuckDB: AT TIME ZONE is polymorphic
102
+ // For timestamptz (TIMESTAMPTZ): AT TIME ZONE converts to plain TIMESTAMP (civil in timezone)
103
+ if (typeDef.type === 'timestamptz') {
104
+ return {
105
+ sql: `(${expr}) AT TIME ZONE '${timezone}'`,
106
+ typeDef: { type: 'timestamp' },
107
+ };
108
+ }
109
+ // For plain timestamps: cast to TIMESTAMPTZ (interprets as UTC)
110
+ // Then AT TIME ZONE converts to plain TIMESTAMP (civil in timezone)
111
+ return {
112
+ sql: `(${expr})::TIMESTAMPTZ AT TIME ZONE '${timezone}'`,
113
+ typeDef: { type: 'timestamp' },
114
+ };
115
+ }
116
+ sqlConvertFromCivilTime(expr, timezone, destTypeDef) {
117
+ if (destTypeDef.type === 'timestamptz') {
118
+ return `(${expr}) AT TIME ZONE '${timezone}'`;
119
+ }
120
+ return `((${expr}) AT TIME ZONE '${timezone}')::TIMESTAMP`;
121
+ }
122
+ sqlTruncate(expr, unit, _typeDef, inCivilTime, _timezone) {
123
+ // PostgreSQL/DuckDB starts weeks on Monday, Malloy wants Sunday
124
+ // Add 1 day before truncating, subtract 1 day after
125
+ let truncated;
126
+ if (unit === 'week') {
127
+ truncated = `(DATE_TRUNC('${unit}', (${expr} + INTERVAL '1' DAY)) - INTERVAL '1' DAY)`;
128
+ }
129
+ else {
130
+ truncated = `DATE_TRUNC('${unit}', ${expr})`;
131
+ }
132
+ // DATE_TRUNC returns DATE for calendar units (day/week/month/quarter/year)
133
+ // When in civil time (plain TIMESTAMP), cast DATE back to TIMESTAMP to continue operations
134
+ const calendarUnits = ['day', 'week', 'month', 'quarter', 'year'];
135
+ if (inCivilTime && calendarUnits.includes(unit)) {
136
+ return `(${truncated})::TIMESTAMP`;
137
+ }
138
+ return truncated;
139
+ }
76
140
  }
77
141
  exports.PostgresBase = PostgresBase;
78
142
  //# sourceMappingURL=pg_impl.js.map
@@ -44,9 +44,6 @@ export declare class PostgresDialect extends PostgresBase {
44
44
  sqlFinalStage(lastStageName: string, _fields: string[]): string;
45
45
  sqlSelectAliasAsStruct(alias: string): string;
46
46
  sqlCreateTableAsSelect(_tableName: string, _sql: string): string;
47
- sqlConvertToCivilTime(expr: string, timezone: string): string;
48
- sqlConvertFromCivilTime(expr: string, timezone: string): string;
49
- sqlTruncate(expr: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
50
47
  sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
51
48
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
52
49
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
@@ -63,7 +60,7 @@ export declare class PostgresDialect extends PostgresBase {
63
60
  [name: string]: DialectFunctionOverloadDef[];
64
61
  };
65
62
  malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
66
- sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
63
+ sqlTypeToMalloyType(rawSqlType: string): BasicAtomicTypeDef;
67
64
  castToString(expression: string): string;
68
65
  concat(...values: string[]): string;
69
66
  validateTypeName(sqlType: string): boolean;
@@ -46,6 +46,7 @@ const inSeconds = {
46
46
  'day': 24 * 3600,
47
47
  'week': 7 * 24 * 3600,
48
48
  };
49
+ // TODO there needs be an audit of this data structure
49
50
  const postgresToMalloyTypes = {
50
51
  'character varying': { type: 'string' },
51
52
  'name': { type: 'string' },
@@ -54,10 +55,10 @@ const postgresToMalloyTypes = {
54
55
  'integer': { type: 'number', numberType: 'integer' },
55
56
  'bigint': { type: 'number', numberType: 'integer' },
56
57
  'double precision': { type: 'number', numberType: 'float' },
57
- 'timestamp without time zone': { type: 'timestamp' }, // maybe not
58
+ 'timestamp without time zone': { type: 'timestamp' },
59
+ 'timestamp with time zone': { type: 'timestamptz' },
58
60
  'oid': { type: 'string' },
59
61
  'boolean': { type: 'boolean' },
60
- // ARRAY: "string",
61
62
  'timestamp': { type: 'timestamp' },
62
63
  '"char"': { type: 'string' },
63
64
  'character': { type: 'string' },
@@ -209,20 +210,6 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
209
210
  sqlCreateTableAsSelect(_tableName, _sql) {
210
211
  throw new Error('Not implemented Yet');
211
212
  }
212
- sqlConvertToCivilTime(expr, timezone) {
213
- return `(${expr})::TIMESTAMPTZ AT TIME ZONE '${timezone}'`;
214
- }
215
- sqlConvertFromCivilTime(expr, timezone) {
216
- return `((${expr}) AT TIME ZONE '${timezone}')::TIMESTAMP`;
217
- }
218
- sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
219
- // PostgreSQL starts weeks on Monday, Malloy wants Sunday
220
- // Add 1 day before truncating, subtract 1 day after
221
- if (unit === 'week') {
222
- return `(DATE_TRUNC('${unit}', (${expr} + INTERVAL '1' DAY)) - INTERVAL '1' DAY)`;
223
- }
224
- return `DATE_TRUNC('${unit}', ${expr})`;
225
- }
226
213
  sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
227
214
  // Convert quarter/week to supported units
228
215
  let offsetUnit = unit;
@@ -343,8 +330,9 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
343
330
  }
344
331
  return malloyType.type;
345
332
  }
346
- sqlTypeToMalloyType(sqlType) {
333
+ sqlTypeToMalloyType(rawSqlType) {
347
334
  var _a, _b, _c;
335
+ const sqlType = rawSqlType.toLowerCase();
348
336
  // Remove trailing params
349
337
  const baseSqlType = (_b = (_a = sqlType.match(/^([\w\s]+)/)) === null || _a === void 0 ? void 0 : _a.at(0)) !== null && _b !== void 0 ? _b : sqlType;
350
338
  return ((_c = postgresToMalloyTypes[baseSqlType.trim().toLowerCase()]) !== null && _c !== void 0 ? _c : {
@@ -380,7 +368,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
380
368
  sqlTimeExtractExpr(qi, from) {
381
369
  const units = pg_impl_1.timeExtractMap[from.units] || from.units;
382
370
  let extractFrom = from.e.sql;
383
- if (malloy_types_1.TD.isTimestamp(from.e.typeDef)) {
371
+ if (malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
384
372
  const tz = (0, dialect_1.qtz)(qi);
385
373
  if (tz) {
386
374
  extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE '${tz}')`;
@@ -1,10 +1,11 @@
1
- import type { Sampling, AtomicTypeDef, TimeExtractExpr, TypecastExpr, TimeLiteralNode, MeasureTimeExpr, RegexMatchExpr, BasicAtomicTypeDef, ArrayLiteralNode, RecordLiteralNode } from '../../model/malloy_types';
1
+ import type { Sampling, AtomicTypeDef, TimeExtractExpr, TypecastExpr, MeasureTimeExpr, RegexMatchExpr, BasicAtomicTypeDef, TimestampTypeDef, ArrayLiteralNode, RecordLiteralNode } from '../../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from '../functions';
3
3
  import type { DialectFieldList, FieldReferenceType, QueryInfo } from '../dialect';
4
4
  import { Dialect } from '../dialect';
5
5
  export declare class SnowflakeDialect extends Dialect {
6
6
  name: string;
7
7
  experimental: boolean;
8
+ hasTimestamptz: boolean;
8
9
  defaultNumberType: string;
9
10
  defaultDecimalType: string;
10
11
  udfPrefix: string;
@@ -45,15 +46,20 @@ export declare class SnowflakeDialect extends Dialect {
45
46
  sqlSelectAliasAsStruct(alias: string): string;
46
47
  sqlMaybeQuoteIdentifier(identifier: string): string;
47
48
  sqlCreateTableAsSelect(tableName: string, sql: string): string;
48
- sqlConvertToCivilTime(expr: string, timezone: string): string;
49
- sqlConvertFromCivilTime(expr: string, timezone: string): string;
49
+ sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
50
+ sql: string;
51
+ typeDef: AtomicTypeDef;
52
+ };
53
+ sqlConvertFromCivilTime(expr: string, timezone: string, _destTypeDef: TimestampTypeDef): string;
50
54
  sqlTruncate(expr: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
51
55
  sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: string, typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
52
56
  sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
53
57
  private atTz;
54
58
  sqlNowExpr(): string;
55
59
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
56
- sqlLiteralTime(qi: QueryInfo, lf: TimeLiteralNode): string;
60
+ sqlDateLiteral(_qi: QueryInfo, literal: string): string;
61
+ sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
62
+ sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
57
63
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
58
64
  sqlRegexpMatch(compare: RegexMatchExpr): string;
59
65
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
@@ -23,6 +23,7 @@
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.SnowflakeDialect = void 0;
26
+ const luxon_1 = require("luxon");
26
27
  const utils_1 = require("../../model/utils");
27
28
  const malloy_types_1 = require("../../model/malloy_types");
28
29
  const functions_1 = require("../functions");
@@ -68,9 +69,9 @@ const snowflakeToMalloyTypes = {
68
69
  'timestampntz': { type: 'timestamp' },
69
70
  'timestamp_ntz': { type: 'timestamp' },
70
71
  'timestamp without time zone': { type: 'timestamp' },
71
- 'timestamptz': { type: 'timestamp' },
72
- 'timestamp_tz': { type: 'timestamp' },
73
- 'timestamp with time zone': { type: 'timestamp' },
72
+ 'timestamptz': { type: 'timestamptz' },
73
+ 'timestamp_tz': { type: 'timestamptz' },
74
+ 'timestamp with time zone': { type: 'timestamptz' },
74
75
  /* timestamp_ltz is not supported in malloy snowflake dialect */
75
76
  };
76
77
  class SnowflakeDialect extends dialect_1.Dialect {
@@ -78,6 +79,7 @@ class SnowflakeDialect extends dialect_1.Dialect {
78
79
  super(...arguments);
79
80
  this.name = 'snowflake';
80
81
  this.experimental = false;
82
+ this.hasTimestamptz = true;
81
83
  this.defaultNumberType = 'NUMBER';
82
84
  this.defaultDecimalType = 'NUMBER';
83
85
  this.udfPrefix = '__udf';
@@ -249,11 +251,23 @@ ${(0, utils_1.indent)(sql)}
249
251
  );
250
252
  `;
251
253
  }
252
- sqlConvertToCivilTime(expr, timezone) {
253
- // 3-arg form: explicitly convert from UTC to specified timezone
254
- return `CONVERT_TIMEZONE('UTC', '${timezone}', ${expr})`;
255
- }
256
- sqlConvertFromCivilTime(expr, timezone) {
254
+ sqlConvertToCivilTime(expr, timezone, typeDef) {
255
+ // For timestamptz (TIMESTAMP_TZ): use 2-arg form
256
+ // Returns TIMESTAMP_TZ with timezone preserved
257
+ if (typeDef.type === 'timestamptz') {
258
+ return {
259
+ sql: `CONVERT_TIMEZONE('${timezone}', ${expr})`,
260
+ typeDef: { type: 'timestamptz' },
261
+ };
262
+ }
263
+ // For plain timestamps (TIMESTAMP_NTZ): use 3-arg form
264
+ // Must cast to TIMESTAMP_NTZ first, returns TIMESTAMP_NTZ
265
+ return {
266
+ sql: `CONVERT_TIMEZONE('UTC', '${timezone}', (${expr})::TIMESTAMP_NTZ)`,
267
+ typeDef: { type: 'timestamp' },
268
+ };
269
+ }
270
+ sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
257
271
  // After civil time operations, we have a TIMESTAMP_NTZ in the target timezone
258
272
  // Convert from timezone to UTC, returning TIMESTAMP_NTZ
259
273
  return `CONVERT_TIMEZONE('${timezone}', 'UTC', (${expr})::TIMESTAMP_NTZ)`;
@@ -261,6 +275,8 @@ ${(0, utils_1.indent)(sql)}
261
275
  sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
262
276
  // Snowflake session is configured with WEEK_START=7 (Sunday)
263
277
  // so DATE_TRUNC already truncates to Sunday - no adjustment needed
278
+ // Unlike PostgreSQL/DuckDB, Snowflake's DATE_TRUNC preserves the input type
279
+ // (TIMESTAMP_NTZ → TIMESTAMP_NTZ, TIMESTAMP_TZ → TIMESTAMP_TZ)
264
280
  return `DATE_TRUNC('${unit}', ${expr})`;
265
281
  }
266
282
  sqlOffsetTime(expr, op, magnitude, unit, typeDef, _inCivilTime, _timezone) {
@@ -272,7 +288,7 @@ ${(0, utils_1.indent)(sql)}
272
288
  const extractUnits = extractionMap[from.units] || from.units;
273
289
  let extractFrom = from.e.sql;
274
290
  const tz = (0, dialect_1.qtz)(qi);
275
- if (tz && malloy_types_1.TD.isTimestamp(from.e.typeDef)) {
291
+ if (tz && malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
276
292
  extractFrom = `CONVERT_TIMEZONE('${tz}', ${extractFrom})`;
277
293
  }
278
294
  return `EXTRACT(${extractUnits} FROM ${extractFrom})`;
@@ -291,7 +307,7 @@ ${(0, utils_1.indent)(sql)}
291
307
  }
292
308
  sqlCast(qi, cast) {
293
309
  const src = cast.e.sql || '';
294
- const { op, srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
310
+ const { srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
295
311
  if (malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
296
312
  return src;
297
313
  }
@@ -303,35 +319,65 @@ ${(0, utils_1.indent)(sql)}
303
319
  refer to: https://docs.snowflake.com/en/sql-reference/functions/try_cast`);
304
320
  }
305
321
  const tz = (0, dialect_1.qtz)(qi);
306
- // casting timestamps and dates
307
- if (op === 'timestamp::date') {
308
- let castExpr = src;
309
- if (tz) {
310
- castExpr = `CONVERT_TIMEZONE('${tz}', ${castExpr})`;
322
+ // Timezone-aware casts when query timezone is set
323
+ if (tz && srcTypeDef && dstTypeDef) {
324
+ // TIMESTAMP DATE: convert to query timezone, then to date
325
+ if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
326
+ return `TO_DATE(CONVERT_TIMEZONE('${tz}', ${src}))`;
327
+ }
328
+ // TIMESTAMPTZ → DATE: convert to query timezone, then to date
329
+ if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
330
+ return `TO_DATE(CONVERT_TIMEZONE('${tz}', ${src}))`;
331
+ }
332
+ // DATE → TIMESTAMP: interpret date in query timezone, return UTC timestamp
333
+ if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
334
+ const retExpr = `TO_TIMESTAMP(${src})`;
335
+ return this.atTz(retExpr, tz);
336
+ }
337
+ // DATE → TIMESTAMPTZ: interpret date in query timezone
338
+ if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
339
+ const retExpr = `TO_TIMESTAMP(${src})`;
340
+ return this.atTz(retExpr, tz);
341
+ }
342
+ // TIMESTAMPTZ → TIMESTAMP: convert to query timezone, get UTC wall clock
343
+ if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
344
+ return `CONVERT_TIMEZONE('${tz}', ${src})::TIMESTAMP_NTZ`;
345
+ }
346
+ // TIMESTAMP → TIMESTAMPTZ: interpret as UTC, convert to TIMESTAMPTZ
347
+ if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
348
+ return this.atTz(src, tz);
311
349
  }
312
- return `TO_DATE(${castExpr})`;
313
- }
314
- else if (op === 'date::timestamp') {
315
- const retExpr = `TO_TIMESTAMP(${src})`;
316
- return this.atTz(retExpr, tz);
317
350
  }
318
351
  const castFunc = cast.safe ? 'TRY_CAST' : 'CAST';
319
352
  return `${castFunc}(${src} AS ${dstSQLType})`;
320
353
  }
321
- sqlLiteralTime(qi, lf) {
322
- var _a;
323
- if (malloy_types_1.TD.isDate(lf.typeDef)) {
324
- return `TO_DATE('${lf.literal}')`;
325
- }
326
- const tz = (0, dialect_1.qtz)(qi);
327
- let ret = `'${lf.literal}'::TIMESTAMP_NTZ`;
328
- const targetTimeZone = (_a = lf.timezone) !== null && _a !== void 0 ? _a : tz;
329
- if (targetTimeZone) {
330
- // Interpret the literal as being in targetTimeZone, convert to UTC
331
- ret = `CONVERT_TIMEZONE('${targetTimeZone}', 'UTC', ${ret})`;
354
+ sqlDateLiteral(_qi, literal) {
355
+ return `TO_DATE('${literal}')`;
356
+ }
357
+ sqlTimestampLiteral(qi, literal, timezone) {
358
+ const tz = timezone || (0, dialect_1.qtz)(qi);
359
+ let ret = `'${literal}'::TIMESTAMP_NTZ`;
360
+ if (tz) {
361
+ // Interpret the literal as being in query timezone, convert to UTC
362
+ ret = `CONVERT_TIMEZONE('${tz}', 'UTC', ${ret})`;
332
363
  }
333
364
  return ret;
334
365
  }
366
+ sqlTimestamptzLiteral(_qi, literal, timezone) {
367
+ // Use TIMESTAMP_TZ_FROM_PARTS to create timestamptz
368
+ const dt = luxon_1.DateTime.fromFormat(literal, 'yyyy-LL-dd HH:mm:ss');
369
+ if (!dt.isValid) {
370
+ throw new Error(`Invalid timestamp literal: ${literal}`);
371
+ }
372
+ const year = dt.year;
373
+ const month = dt.month;
374
+ const day = dt.day;
375
+ const hour = dt.hour;
376
+ const minute = dt.minute;
377
+ const second = dt.second;
378
+ const nanosecond = dt.millisecond * 1000000;
379
+ return `TIMESTAMP_TZ_FROM_PARTS(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second}, ${nanosecond}, '${timezone}')`;
380
+ }
335
381
  sqlMeasureTimeExpr(df) {
336
382
  const from = df.kids.left;
337
383
  const to = df.kids.right;
@@ -413,6 +459,9 @@ ${(0, utils_1.indent)(sql)}
413
459
  else if ((0, malloy_types_1.isBasicArray)(malloyType)) {
414
460
  return `ARRAY(${this.malloyTypeToSQLType(malloyType.elementTypeDef)})`;
415
461
  }
462
+ else if (malloyType.type === 'timestamptz') {
463
+ return 'TIMESTAMP_TZ';
464
+ }
416
465
  return malloyType.type;
417
466
  }
418
467
  sqlTypeToMalloyType(sqlType) {
@@ -1,4 +1,4 @@
1
- import type { Sampling, AtomicTypeDef, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralNode, MeasureTimeExpr, BasicAtomicTypeDef, RecordLiteralNode, ArrayLiteralNode, TimestampUnit } from '../../model/malloy_types';
1
+ import type { Sampling, AtomicTypeDef, TimeExtractExpr, TypecastExpr, RegexMatchExpr, MeasureTimeExpr, BasicAtomicTypeDef, RecordLiteralNode, ArrayLiteralNode, TimestampUnit, TimestampTypeDef } from '../../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from '../functions';
3
3
  import type { DialectFieldList, OrderByRequest, QueryInfo } from '../dialect';
4
4
  import { Dialect } from '../dialect';
@@ -51,14 +51,19 @@ export declare class StandardSQLDialect extends Dialect {
51
51
  sqlMaybeQuoteIdentifier(identifier: string): string;
52
52
  sqlNowExpr(): string;
53
53
  sqlTimeExtractExpr(qi: QueryInfo, te: TimeExtractExpr): string;
54
- sqlConvertToCivilTime(expr: string, timezone: string): string;
55
- sqlConvertFromCivilTime(expr: string, timezone: string): string;
54
+ sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
55
+ sql: string;
56
+ typeDef: AtomicTypeDef;
57
+ };
58
+ sqlConvertFromCivilTime(expr: string, timezone: string, _destTypeDef: TimestampTypeDef): string;
56
59
  sqlTruncate(expr: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, timezone?: string): string;
57
60
  sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, _timezone?: string): string;
58
61
  ignoreInProject(fieldName: string): boolean;
59
62
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
60
63
  sqlRegexpMatch(match: RegexMatchExpr): string;
61
- sqlLiteralTime(qi: QueryInfo, lit: TimeLiteralNode): string;
64
+ sqlDateLiteral(_qi: QueryInfo, literal: string): string;
65
+ sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
66
+ sqlTimestamptzLiteral(_qi: QueryInfo, _literal: string, _timezone: string): string;
62
67
  sqlMeasureTimeExpr(measure: MeasureTimeExpr): string;
63
68
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
64
69
  sqlLiteralString(literal: string): string;
@@ -108,7 +108,7 @@ class StandardSQLDialect extends dialect_1.Dialect {
108
108
  const calendarUnits = ['day', 'week', 'month', 'quarter', 'year'];
109
109
  const isCalendarTruncate = truncateTo !== undefined && calendarUnits.includes(truncateTo);
110
110
  const isCalendarOffset = offsetUnit !== undefined && calendarUnits.includes(offsetUnit);
111
- const needed = malloy_types_1.TD.isTimestamp(typeDef) && (isCalendarTruncate || isCalendarOffset);
111
+ const needed = malloy_types_1.TD.isAnyTimestamp(typeDef) && (isCalendarTruncate || isCalendarOffset);
112
112
  const tz = needed ? qtz(qi) || 'UTC' : undefined;
113
113
  return { needed, tz };
114
114
  }
@@ -222,14 +222,18 @@ ${(0, utils_1.indent)(sql)}
222
222
  }
223
223
  sqlTimeExtractExpr(qi, te) {
224
224
  const extractTo = extractMap[te.units] || te.units;
225
- const tz = malloy_types_1.TD.isTimestamp(te.e.typeDef) && qtz(qi);
225
+ const tz = malloy_types_1.TD.isAnyTimestamp(te.e.typeDef) && qtz(qi);
226
226
  const tzAdd = tz ? ` AT TIME ZONE '${tz}'` : '';
227
227
  return `EXTRACT(${extractTo} FROM ${te.e.sql}${tzAdd})`;
228
228
  }
229
- sqlConvertToCivilTime(expr, timezone) {
230
- return `DATETIME(${expr}, '${timezone}')`;
229
+ sqlConvertToCivilTime(expr, timezone, _typeDef) {
230
+ // BigQuery has no timestamptz type, so typeDef.timestamptz will never be true
231
+ return {
232
+ sql: `DATETIME(${expr}, '${timezone}')`,
233
+ typeDef: { type: 'timestamp' },
234
+ };
231
235
  }
232
- sqlConvertFromCivilTime(expr, timezone) {
236
+ sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
233
237
  return `TIMESTAMP(${expr}, '${timezone}')`;
234
238
  }
235
239
  sqlTruncate(expr, unit, typeDef, inCivilTime, timezone) {
@@ -283,21 +287,19 @@ ${(0, utils_1.indent)(sql)}
283
287
  sqlRegexpMatch(match) {
284
288
  return `REGEXP_CONTAINS(${match.kids.expr.sql},${match.kids.regex.sql})`;
285
289
  }
286
- sqlLiteralTime(qi, lit) {
287
- if (malloy_types_1.TD.isDate(lit.typeDef)) {
288
- return `DATE('${lit.literal}')`;
289
- }
290
- else if (malloy_types_1.TD.isTimestamp(lit.typeDef)) {
291
- let timestampArgs = `'${lit.literal}'`;
292
- const tz = lit.timezone || qtz(qi);
293
- if (tz && tz !== 'UTC') {
294
- timestampArgs += `,'${tz}'`;
295
- }
296
- return `TIMESTAMP(${timestampArgs})`;
297
- }
298
- else {
299
- throw new Error(`Unsupported Literal time format ${lit.typeDef}`);
290
+ sqlDateLiteral(_qi, literal) {
291
+ return `DATE('${literal}')`;
292
+ }
293
+ sqlTimestampLiteral(qi, literal, timezone) {
294
+ let timestampArgs = `'${literal}'`;
295
+ const tz = timezone || qtz(qi);
296
+ if (tz && tz !== 'UTC') {
297
+ timestampArgs += `,'${tz}'`;
300
298
  }
299
+ return `TIMESTAMP(${timestampArgs})`;
300
+ }
301
+ sqlTimestamptzLiteral(_qi, _literal, _timezone) {
302
+ throw new Error('BigQuery does not support timestamptz');
301
303
  }
302
304
  sqlMeasureTimeExpr(measure) {
303
305
  const measureMap = {
@@ -93,7 +93,7 @@ class TinyParser {
93
93
  for (const txt of texts) {
94
94
  next = this.getNext();
95
95
  expected = txt;
96
- if (next.text !== txt) {
96
+ if (next.text.toUpperCase() !== txt.toUpperCase()) {
97
97
  next = undefined;
98
98
  break;
99
99
  }
@@ -1,4 +1,4 @@
1
- import type { Expr, Sampling, AtomicTypeDef, TypecastExpr, RegexMatchExpr, MeasureTimeExpr, TimeLiteralNode, TimeExtractExpr, BasicAtomicTypeDef, RecordLiteralNode } from '../../model/malloy_types';
1
+ import type { Expr, Sampling, AtomicTypeDef, ATimestampTypeDef, TypecastExpr, RegexMatchExpr, MeasureTimeExpr, TimeExtractExpr, BasicAtomicTypeDef, RecordLiteralNode } from '../../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from '../functions';
3
3
  import type { DialectFieldList, OrderByClauseType, QueryInfo } from '../dialect';
4
4
  import { PostgresBase } from '../pg_impl';
@@ -51,8 +51,11 @@ export declare class TrinoDialect extends PostgresBase {
51
51
  sqlCreateFunctionCombineLastStage(lastStageName: string, fieldList: DialectFieldList): string;
52
52
  sqlSelectAliasAsStruct(alias: string, fieldList: any): string;
53
53
  keywords: string[];
54
- sqlConvertToCivilTime(expr: string, timezone: string): string;
55
- sqlConvertFromCivilTime(expr: string, _timezone: string): string;
54
+ sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
55
+ sql: string;
56
+ typeDef: AtomicTypeDef;
57
+ };
58
+ sqlConvertFromCivilTime(expr: string, _timezone: string, destTypeDef: ATimestampTypeDef): string;
56
59
  sqlTruncate(expr: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
57
60
  sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
58
61
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
@@ -74,7 +77,9 @@ export declare class TrinoDialect extends PostgresBase {
74
77
  sqlMakeUnnestKey(key: string, rowKey: string): string;
75
78
  sqlStringAggDistinct(distinctKey: string, valueSQL: string, separatorSQL: string): string;
76
79
  validateTypeName(sqlType: string): boolean;
77
- sqlLiteralTime(qi: QueryInfo, lit: TimeLiteralNode): string;
80
+ sqlDateLiteral(_qi: QueryInfo, literal: string): string;
81
+ sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
82
+ sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
78
83
  sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
79
84
  sqlLiteralRecord(lit: RecordLiteralNode): string;
80
85
  }
@@ -83,8 +88,16 @@ export declare class PrestoDialect extends TrinoDialect {
83
88
  supportsPipelinesInViews: boolean;
84
89
  supportsLeftJoinUnnest: boolean;
85
90
  sqlGenerateUUID(): string;
86
- sqlLiteralTime(qi: QueryInfo, lit: TimeLiteralNode): string;
87
- sqlConvertFromCivilTime(expr: string, _timezone: string): string;
91
+ sqlDateLiteral(_qi: QueryInfo, literal: string): string;
92
+ sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
93
+ sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
94
+ sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
95
+ sql: string;
96
+ typeDef: AtomicTypeDef;
97
+ };
98
+ sqlConvertFromCivilTime(expr: string, _timezone: string, destTypeDef: ATimestampTypeDef): string;
99
+ sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
100
+ sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
88
101
  sqlUnnestAlias(source: string, alias: string, _fieldList: DialectFieldList, needDistinctKey: boolean, isArray: boolean, _isInNestedPipeline: boolean): string;
89
102
  getDialectFunctions(): {
90
103
  [name: string]: DialectFunctionOverloadDef[];