@malloydata/malloy 0.0.432 → 0.0.434

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 (55) hide show
  1. package/dist/api/foundation/compile.d.ts +2 -1
  2. package/dist/api/foundation/compile.js +8 -6
  3. package/dist/api/foundation/config.js +6 -15
  4. package/dist/api/foundation/runtime.d.ts +2 -7
  5. package/dist/api/foundation/runtime.js +10 -16
  6. package/dist/api/foundation/types.d.ts +1 -0
  7. package/dist/connection/validate_table_path.d.ts +1 -2
  8. package/dist/connection/validate_table_path.js +1 -2
  9. package/dist/dialect/databricks/databricks.js +6 -6
  10. package/dist/dialect/dialect.d.ts +21 -2
  11. package/dist/dialect/dialect.js +33 -3
  12. package/dist/dialect/duckdb/duckdb.js +7 -21
  13. package/dist/dialect/mysql/mysql.js +70 -29
  14. package/dist/dialect/pg_impl.js +12 -12
  15. package/dist/dialect/postgres/postgres.js +3 -3
  16. package/dist/dialect/snowflake/snowflake.js +12 -12
  17. package/dist/dialect/standardsql/standardsql.d.ts +1 -1
  18. package/dist/dialect/standardsql/standardsql.js +16 -13
  19. package/dist/dialect/trino/trino.d.ts +3 -3
  20. package/dist/dialect/trino/trino.js +32 -28
  21. package/dist/lang/ast/expressions/expr-aggregate-function.js +20 -26
  22. package/dist/lang/ast/field-space/def-space.js +1 -0
  23. package/dist/lang/ast/field-space/index-field-space.js +7 -33
  24. package/dist/lang/ast/field-space/parameter-space.js +2 -0
  25. package/dist/lang/ast/field-space/query-spaces.d.ts +10 -0
  26. package/dist/lang/ast/field-space/query-spaces.js +33 -32
  27. package/dist/lang/ast/field-space/static-space.d.ts +34 -1
  28. package/dist/lang/ast/field-space/static-space.js +118 -75
  29. package/dist/lang/ast/source-elements/refined-source.js +1 -1
  30. package/dist/lang/ast/source-elements/sql-source.js +2 -1
  31. package/dist/lang/ast/source-properties/timezone-statement.d.ts +4 -3
  32. package/dist/lang/ast/source-properties/timezone-statement.js +14 -12
  33. package/dist/lang/ast/types/lookup-result.d.ts +2 -0
  34. package/dist/lang/composite-source-utils.js +10 -3
  35. package/dist/lang/malloy-to-ast.js +2 -8
  36. package/dist/lang/parse-log.d.ts +1 -3
  37. package/dist/lang/parse-malloy.d.ts +3 -2
  38. package/dist/lang/parse-malloy.js +2 -1
  39. package/dist/lang/test/test-translator.d.ts +2 -1
  40. package/dist/lang/test/test-translator.js +1 -1
  41. package/dist/model/join_instance.d.ts +1 -0
  42. package/dist/model/join_instance.js +9 -1
  43. package/dist/model/query_query.d.ts +25 -0
  44. package/dist/model/query_query.js +142 -17
  45. package/dist/model/sql_block.d.ts +2 -2
  46. package/dist/model/sql_block.js +11 -47
  47. package/dist/model/sql_compiled.d.ts +3 -1
  48. package/dist/model/sql_compiled.js +8 -6
  49. package/dist/model/stage_writer.js +3 -3
  50. package/dist/model/utils.d.ts +2 -2
  51. package/dist/model/utils.js +2 -3
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/femto-config.motly +2 -0
  55. package/package.json +4 -4
@@ -45,7 +45,7 @@ class PostgresBase extends dialect_1.Dialect {
45
45
  if (malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
46
46
  const tz = (0, dialect_1.qtz)(qi);
47
47
  if (tz) {
48
- extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE '${tz}')`;
48
+ extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE ${this.sqlTimezoneLiteral(tz)})`;
49
49
  }
50
50
  }
51
51
  const extracted = `EXTRACT(${units} FROM ${extractFrom})`;
@@ -59,23 +59,23 @@ class PostgresBase extends dialect_1.Dialect {
59
59
  if (tz && srcTypeDef && dstTypeDef) {
60
60
  // TIMESTAMP → DATE: convert via TIMESTAMPTZ to query timezone
61
61
  if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
62
- return `CAST((${expr}::TIMESTAMPTZ) AT TIME ZONE '${tz}' AS DATE)`;
62
+ return `CAST((${expr}::TIMESTAMPTZ) AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS DATE)`;
63
63
  }
64
64
  // TIMESTAMPTZ → DATE: convert to query timezone
65
65
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
66
- return `CAST((${expr}) AT TIME ZONE '${tz}' AS DATE)`;
66
+ return `CAST((${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS DATE)`;
67
67
  }
68
68
  // DATE → TIMESTAMP: interpret date in query timezone, return UTC timestamp
69
69
  if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
70
- return `CAST((${expr})::TIMESTAMP AT TIME ZONE '${tz}' AS TIMESTAMP)`;
70
+ return `CAST((${expr})::TIMESTAMP AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS TIMESTAMP)`;
71
71
  }
72
72
  // DATE → TIMESTAMPTZ: interpret date in query timezone
73
73
  if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
74
- return `(${expr})::TIMESTAMP AT TIME ZONE '${tz}'`;
74
+ return `(${expr})::TIMESTAMP AT TIME ZONE ${this.sqlTimezoneLiteral(tz)}`;
75
75
  }
76
76
  // TIMESTAMPTZ → TIMESTAMP: convert to query timezone (returns TIMESTAMP)
77
77
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
78
- return `(${expr}) AT TIME ZONE '${tz}'`;
78
+ return `(${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(tz)}`;
79
79
  }
80
80
  }
81
81
  // No special handling needed, or no query timezone
@@ -94,12 +94,12 @@ class PostgresBase extends dialect_1.Dialect {
94
94
  sqlTimestampLiteral(qi, literal, timezone) {
95
95
  const tz = timezone || (0, dialect_1.qtz)(qi);
96
96
  if (tz) {
97
- return `TIMESTAMPTZ '${literal} ${tz}'::TIMESTAMP`;
97
+ return `TIMESTAMPTZ '${literal} ${this.checkedTimezoneName(tz)}'::TIMESTAMP`;
98
98
  }
99
99
  return `TIMESTAMP '${literal}'`;
100
100
  }
101
101
  sqlTimestamptzLiteral(_qi, literal, timezone) {
102
- return `TIMESTAMPTZ '${literal} ${timezone}'`;
102
+ return `TIMESTAMPTZ '${literal} ${this.checkedTimezoneName(timezone)}'`;
103
103
  }
104
104
  sqlLiteralRecord(_lit) {
105
105
  throw new Error('Cannot create a record literal for postgres base dialect');
@@ -113,22 +113,22 @@ class PostgresBase extends dialect_1.Dialect {
113
113
  // For timestamptz (TIMESTAMPTZ): AT TIME ZONE converts to plain TIMESTAMP (civil in timezone)
114
114
  if (typeDef.type === 'timestamptz') {
115
115
  return {
116
- sql: `(${expr}) AT TIME ZONE '${timezone}'`,
116
+ sql: `(${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(timezone)}`,
117
117
  typeDef: { type: 'timestamp' },
118
118
  };
119
119
  }
120
120
  // For plain timestamps: cast to TIMESTAMPTZ (interprets as UTC)
121
121
  // Then AT TIME ZONE converts to plain TIMESTAMP (civil in timezone)
122
122
  return {
123
- sql: `(${expr})::TIMESTAMPTZ AT TIME ZONE '${timezone}'`,
123
+ sql: `(${expr})::TIMESTAMPTZ AT TIME ZONE ${this.sqlTimezoneLiteral(timezone)}`,
124
124
  typeDef: { type: 'timestamp' },
125
125
  };
126
126
  }
127
127
  sqlConvertFromCivilTime(expr, timezone, destTypeDef) {
128
128
  if (destTypeDef.type === 'timestamptz') {
129
- return `(${expr}) AT TIME ZONE '${timezone}'`;
129
+ return `(${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(timezone)}`;
130
130
  }
131
- return `((${expr}) AT TIME ZONE '${timezone}')::TIMESTAMP`;
131
+ return `((${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(timezone)})::TIMESTAMP`;
132
132
  }
133
133
  sqlTruncate(expr, unit, _typeDef, inCivilTime, _timezone) {
134
134
  // PostgreSQL/DuckDB starts weeks on Monday, Malloy wants Sunday
@@ -94,7 +94,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
94
94
  }
95
95
  mapFields(fieldList) {
96
96
  return fieldList
97
- .map(f => `\n ${f.sqlExpression}${f.typeDef.type === 'number' ? `::${this.defaultNumberType}` : ''} as ${f.sqlOutputName}`)
97
+ .map(f => `\n ${f.sqlExpression}${f.typeDef.type === 'number' ? `::${this.defaultNumberType}` : ''} as ${this.sqlQuoteIdentifier(f.rawName)}`)
98
98
  .join(', ');
99
99
  }
100
100
  sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
@@ -110,7 +110,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
110
110
  }
111
111
  sqlAnyValueTurtle(groupSet, fieldList) {
112
112
  const fields = fieldList
113
- .map(f => `${f.sqlExpression} as ${f.sqlOutputName}`)
113
+ .map(f => `${f.sqlExpression} as ${this.sqlQuoteIdentifier(f.rawName)}`)
114
114
  .join(', ');
115
115
  return `ANY_VALUE(CASE WHEN group_set=${groupSet} THEN STRUCT(${fields}))`;
116
116
  }
@@ -393,7 +393,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
393
393
  if (malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
394
394
  const tz = (0, dialect_1.qtz)(qi);
395
395
  if (tz) {
396
- extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE '${tz}')`;
396
+ extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE ${this.sqlTimezoneLiteral(tz)})`;
397
397
  }
398
398
  }
399
399
  // PostgreSQL before 14 returns a double precision for EXTRACT, cast to integer
@@ -105,7 +105,7 @@ class SnowflakeDialect extends dialect_1.Dialect {
105
105
  }
106
106
  mapFields(fieldList) {
107
107
  return fieldList
108
- .map(f => `\n ${f.sqlExpression} as ${f.sqlOutputName}`)
108
+ .map(f => `\n ${f.sqlExpression} as ${this.sqlQuoteIdentifier(f.rawName)}`)
109
109
  .join(', ');
110
110
  }
111
111
  mapFieldsForObjectConstruct(fieldList) {
@@ -138,7 +138,7 @@ class SnowflakeDialect extends dialect_1.Dialect {
138
138
  sqlCoaleseMeasuresInline(groupSet, fieldList) {
139
139
  const fields = this.mapFieldsForObjectConstruct(fieldList);
140
140
  const nullValues = fieldList
141
- .map(f => `'${f.sqlOutputName}', NULL`)
141
+ .map(f => `${this.sqlLiteralString(f.rawName)}, NULL`)
142
142
  .join(', ');
143
143
  return `COALESCE(ARRAY_AGG(CASE WHEN group_set=${groupSet} THEN OBJECT_CONSTRUCT_KEEP_NULL(${fields}) END)[0], OBJECT_CONSTRUCT_KEEP_NULL(${nullValues}))`;
144
144
  }
@@ -254,21 +254,21 @@ ${(0, utils_1.indent)(sql)}
254
254
  // Returns TIMESTAMP_TZ with timezone preserved
255
255
  if (typeDef.type === 'timestamptz') {
256
256
  return {
257
- sql: `CONVERT_TIMEZONE('${timezone}', ${expr})`,
257
+ sql: `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(timezone)}, ${expr})`,
258
258
  typeDef: { type: 'timestamptz' },
259
259
  };
260
260
  }
261
261
  // For plain timestamps (TIMESTAMP_NTZ): use 3-arg form
262
262
  // Must cast to TIMESTAMP_NTZ first, returns TIMESTAMP_NTZ
263
263
  return {
264
- sql: `CONVERT_TIMEZONE('UTC', '${timezone}', (${expr})::TIMESTAMP_NTZ)`,
264
+ sql: `CONVERT_TIMEZONE('UTC', ${this.sqlTimezoneLiteral(timezone)}, (${expr})::TIMESTAMP_NTZ)`,
265
265
  typeDef: { type: 'timestamp' },
266
266
  };
267
267
  }
268
268
  sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
269
269
  // After civil time operations, we have a TIMESTAMP_NTZ in the target timezone
270
270
  // Convert from timezone to UTC, returning TIMESTAMP_NTZ
271
- return `CONVERT_TIMEZONE('${timezone}', 'UTC', (${expr})::TIMESTAMP_NTZ)`;
271
+ return `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(timezone)}, 'UTC', (${expr})::TIMESTAMP_NTZ)`;
272
272
  }
273
273
  sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
274
274
  // Snowflake session is configured with WEEK_START=7 (Sunday)
@@ -287,7 +287,7 @@ ${(0, utils_1.indent)(sql)}
287
287
  let extractFrom = from.e.sql;
288
288
  const tz = (0, dialect_1.qtz)(qi);
289
289
  if (tz && malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
290
- extractFrom = `CONVERT_TIMEZONE('${tz}', ${extractFrom})`;
290
+ extractFrom = `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, ${extractFrom})`;
291
291
  }
292
292
  return `EXTRACT(${extractUnits} FROM ${extractFrom})`;
293
293
  }
@@ -295,7 +295,7 @@ ${(0, utils_1.indent)(sql)}
295
295
  if (tz !== undefined) {
296
296
  return `(
297
297
  TO_CHAR(${sqlExpr}::TIMESTAMP_NTZ, 'YYYY-MM-DD HH24:MI:SS.FF9') ||
298
- TO_CHAR(CONVERT_TIMEZONE('${tz}', '1970-01-01 00:00:00'), 'TZHTZM')
298
+ TO_CHAR(CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, '1970-01-01 00:00:00'), 'TZHTZM')
299
299
  )::TIMESTAMP_TZ`;
300
300
  }
301
301
  return `${sqlExpr}::TIMESTAMP_NTZ`;
@@ -321,11 +321,11 @@ ${(0, utils_1.indent)(sql)}
321
321
  if (tz && srcTypeDef && dstTypeDef) {
322
322
  // TIMESTAMP → DATE: convert to query timezone, then to date
323
323
  if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
324
- return `TO_DATE(CONVERT_TIMEZONE('${tz}', ${src}))`;
324
+ return `TO_DATE(CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, ${src}))`;
325
325
  }
326
326
  // TIMESTAMPTZ → DATE: convert to query timezone, then to date
327
327
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
328
- return `TO_DATE(CONVERT_TIMEZONE('${tz}', ${src}))`;
328
+ return `TO_DATE(CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, ${src}))`;
329
329
  }
330
330
  // DATE → TIMESTAMP: interpret date in query timezone, return UTC timestamp
331
331
  if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
@@ -339,7 +339,7 @@ ${(0, utils_1.indent)(sql)}
339
339
  }
340
340
  // TIMESTAMPTZ → TIMESTAMP: convert to query timezone, get UTC wall clock
341
341
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
342
- return `CONVERT_TIMEZONE('${tz}', ${src})::TIMESTAMP_NTZ`;
342
+ return `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, ${src})::TIMESTAMP_NTZ`;
343
343
  }
344
344
  // TIMESTAMP → TIMESTAMPTZ: interpret as UTC, convert to TIMESTAMPTZ
345
345
  if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
@@ -357,7 +357,7 @@ ${(0, utils_1.indent)(sql)}
357
357
  let ret = `'${literal}'::TIMESTAMP_NTZ`;
358
358
  if (tz) {
359
359
  // Interpret the literal as being in query timezone, convert to UTC
360
- ret = `CONVERT_TIMEZONE('${tz}', 'UTC', ${ret})`;
360
+ ret = `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, 'UTC', ${ret})`;
361
361
  }
362
362
  return ret;
363
363
  }
@@ -374,7 +374,7 @@ ${(0, utils_1.indent)(sql)}
374
374
  const minute = dt.minute;
375
375
  const second = dt.second;
376
376
  const nanosecond = dt.millisecond * 1000000;
377
- return `TIMESTAMP_TZ_FROM_PARTS(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second}, ${nanosecond}, '${timezone}')`;
377
+ return `TIMESTAMP_TZ_FROM_PARTS(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second}, ${nanosecond}, ${this.sqlTimezoneLiteral(timezone)})`;
378
378
  }
379
379
  sqlMeasureTimeExpr(df) {
380
380
  const from = df.kids.left;
@@ -55,7 +55,7 @@ export declare class StandardSQLDialect extends Dialect {
55
55
  sqlCreateFunction(id: string, funcText: string): string;
56
56
  sqlCreateTableAsSelect(tableName: string, sql: string): string;
57
57
  sqlCreateFunctionCombineLastStage(lastStageName: string): string;
58
- sqlSelectAliasAsStruct(alias: string): string;
58
+ sqlSelectAliasAsStruct(alias: string, dialectFieldList: DialectFieldList): string;
59
59
  sqlNowExpr(): string;
60
60
  sqlTimeExtractExpr(qi: QueryInfo, te: TimeExtractExpr): string;
61
61
  sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
@@ -150,7 +150,7 @@ class StandardSQLDialect extends dialect_1.Dialect {
150
150
  // can array agg or any_value a struct...
151
151
  sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
152
152
  const fields = fieldList
153
- .map(f => `\n ${f.sqlExpression} as ${f.sqlOutputName}`)
153
+ .map(f => `\n ${f.sqlExpression} as ${this.sqlQuoteIdentifier(f.rawName)}`)
154
154
  .join(', ');
155
155
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
156
156
  // BigQuery ARRAY_AGG takes LIMIT as its final sub-clause, after ORDER BY.
@@ -162,7 +162,7 @@ class StandardSQLDialect extends dialect_1.Dialect {
162
162
  }
163
163
  sqlAnyValueTurtle(groupSet, fieldList) {
164
164
  const fields = fieldList
165
- .map(f => `${f.sqlExpression} as ${f.sqlOutputName}`)
165
+ .map(f => `${f.sqlExpression} as ${this.sqlQuoteIdentifier(f.rawName)}`)
166
166
  .join(', ');
167
167
  return `ANY_VALUE(CASE WHEN group_set=${groupSet} THEN STRUCT(${fields}) END)`;
168
168
  }
@@ -171,10 +171,10 @@ class StandardSQLDialect extends dialect_1.Dialect {
171
171
  }
172
172
  sqlCoaleseMeasuresInline(groupSet, fieldList) {
173
173
  const fields = fieldList
174
- .map(f => `${f.sqlExpression} as ${f.sqlOutputName}`)
174
+ .map(f => `${f.sqlExpression} as ${this.sqlQuoteIdentifier(f.rawName)}`)
175
175
  .join(', ');
176
176
  const nullValues = fieldList
177
- .map(f => `NULL as ${f.sqlOutputName}`)
177
+ .map(f => `NULL as ${this.sqlQuoteIdentifier(f.rawName)}`)
178
178
  .join(', ');
179
179
  return `COALESCE(ANY_VALUE(CASE WHEN group_set=${groupSet} THEN STRUCT(${fields}) END), STRUCT(${nullValues}))`;
180
180
  }
@@ -240,8 +240,11 @@ ${(0, utils_1.indent)(sql)}
240
240
  sqlCreateFunctionCombineLastStage(lastStageName) {
241
241
  return `SELECT ARRAY((SELECT AS STRUCT * FROM ${lastStageName}))\n`;
242
242
  }
243
- sqlSelectAliasAsStruct(alias) {
244
- return `(SELECT AS STRUCT ${alias}.*)`;
243
+ sqlSelectAliasAsStruct(alias, dialectFieldList) {
244
+ const fields = dialectFieldList
245
+ .map(d => `${alias}.${this.sqlQuoteIdentifier(d.rawName)}`)
246
+ .join(', ');
247
+ return `(SELECT AS STRUCT ${fields})`;
245
248
  }
246
249
  sqlNowExpr() {
247
250
  return 'CURRENT_TIMESTAMP()';
@@ -249,18 +252,18 @@ ${(0, utils_1.indent)(sql)}
249
252
  sqlTimeExtractExpr(qi, te) {
250
253
  const extractTo = extractMap[te.units] || te.units;
251
254
  const tz = malloy_types_1.TD.isAnyTimestamp(te.e.typeDef) && qtz(qi);
252
- const tzAdd = tz ? ` AT TIME ZONE '${tz}'` : '';
255
+ const tzAdd = tz ? ` AT TIME ZONE ${this.sqlTimezoneLiteral(tz)}` : '';
253
256
  return `EXTRACT(${extractTo} FROM ${te.e.sql}${tzAdd})`;
254
257
  }
255
258
  sqlConvertToCivilTime(expr, timezone, _typeDef) {
256
259
  // BigQuery has no timestamptz type, so typeDef.timestamptz will never be true
257
260
  return {
258
- sql: `DATETIME(${expr}, '${timezone}')`,
261
+ sql: `DATETIME(${expr}, ${this.sqlTimezoneLiteral(timezone)})`,
259
262
  typeDef: { type: 'timestamp' },
260
263
  };
261
264
  }
262
265
  sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
263
- return `TIMESTAMP(${expr}, '${timezone}')`;
266
+ return `TIMESTAMP(${expr}, ${this.sqlTimezoneLiteral(timezone)})`;
264
267
  }
265
268
  sqlTruncate(expr, unit, typeDef, inCivilTime, timezone) {
266
269
  if (inCivilTime) {
@@ -272,7 +275,7 @@ ${(0, utils_1.indent)(sql)}
272
275
  return `DATE_TRUNC(${expr}, ${unit})`;
273
276
  }
274
277
  // TIMESTAMP truncation with optional timezone
275
- const tzParam = timezone ? `, '${timezone}'` : '';
278
+ const tzParam = timezone ? `, ${this.sqlTimezoneLiteral(timezone)}` : '';
276
279
  return `TIMESTAMP_TRUNC(${expr}, ${unit}${tzParam})`;
277
280
  }
278
281
  sqlOffsetTime(expr, op, magnitude, unit, typeDef, inCivilTime, _timezone) {
@@ -299,10 +302,10 @@ ${(0, utils_1.indent)(sql)}
299
302
  const tz = qtz(qi);
300
303
  const src = cast.e.sql || '';
301
304
  if (op === 'timestamp::date' && tz) {
302
- return `DATE(${src},'${tz}')`;
305
+ return `DATE(${src},${this.sqlTimezoneLiteral(tz)})`;
303
306
  }
304
307
  if (op === 'date::timestamp' && tz) {
305
- return `TIMESTAMP(${src}, '${tz}')`;
308
+ return `TIMESTAMP(${src}, ${this.sqlTimezoneLiteral(tz)})`;
306
309
  }
307
310
  if (!malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
308
311
  const castFunc = cast.safe ? 'SAFE_CAST' : 'CAST';
@@ -320,7 +323,7 @@ ${(0, utils_1.indent)(sql)}
320
323
  let timestampArgs = `'${literal}'`;
321
324
  const tz = timezone || qtz(qi);
322
325
  if (tz && tz !== 'UTC') {
323
- timestampArgs += `,'${tz}'`;
326
+ timestampArgs += `,${this.sqlTimezoneLiteral(tz)}`;
324
327
  }
325
328
  return `TIMESTAMP(${timestampArgs})`;
326
329
  }
@@ -1,4 +1,4 @@
1
- import type { Expr, Sampling, AtomicTypeDef, ATimestampTypeDef, TypecastExpr, RegexMatchExpr, MeasureTimeExpr, TimeExtractExpr, BasicAtomicTypeDef, RecordLiteralNode } from '../../model/malloy_types';
1
+ import type { Expr, Sampling, AtomicTypeDef, ATimestampTypeDef, TypecastExpr, RegexMatchExpr, MeasureTimeExpr, TimeExtractExpr, BasicAtomicTypeDef, RecordLiteralNode, OrderBy } from '../../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from '../functions';
3
3
  import type { CompiledOrderBy, DialectFieldList, OrderByClauseType, QueryInfo } from '../dialect';
4
4
  import { PostgresBase } from '../pg_impl';
@@ -49,8 +49,8 @@ export declare class TrinoDialect extends PostgresBase {
49
49
  sqlUnnestPipelineHead(isSingleton: boolean, sourceSQLExpression: string): string;
50
50
  sqlCreateFunction(id: string, funcText: string): string;
51
51
  sqlCreateTableAsSelect(tableName: string, sql: string): string;
52
- sqlCreateFunctionCombineLastStage(lastStageName: string, fieldList: DialectFieldList): string;
53
- sqlSelectAliasAsStruct(alias: string, fieldList: any): string;
52
+ sqlCreateFunctionCombineLastStage(lastStageName: string, fieldList: DialectFieldList, orderBy: OrderBy[] | undefined): string;
53
+ sqlSelectAliasAsStruct(alias: string, fieldList: DialectFieldList): string;
54
54
  keywords: string[];
55
55
  sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
56
56
  sql: string;
@@ -82,7 +82,7 @@ class TrinoDialect extends pg_impl_1.PostgresBase {
82
82
  this.orderByClause = 'output_name';
83
83
  this.nullMatchesFunctionSignature = false;
84
84
  this.supportsSelectReplace = false;
85
- this.supportsComplexFilteredSources = false;
85
+ this.supportsComplexFilteredSources = true;
86
86
  this.supportsTempTables = false;
87
87
  this.supportsCountApprox = true;
88
88
  this.supportsHyperLogLog = true;
@@ -223,7 +223,8 @@ class TrinoDialect extends pg_impl_1.PostgresBase {
223
223
  }
224
224
  buildTypeExpression(fieldList) {
225
225
  return fieldList
226
- .map(dlf => `${dlf.sqlOutputName} ${this.malloyTypeToSQLType(dlf.typeDef)}`)
226
+ .map(dlf => `${this.sqlQuoteIdentifier(dlf.rawName)} ` +
227
+ this.malloyTypeToSQLType(dlf.typeDef))
227
228
  .join(', \n');
228
229
  }
229
230
  // can array agg or any_value a struct...
@@ -325,15 +326,18 @@ ${(0, utils_1.indent)(sql)}
325
326
  );
326
327
  `;
327
328
  }
328
- sqlCreateFunctionCombineLastStage(lastStageName, fieldList) {
329
+ sqlCreateFunctionCombineLastStage(lastStageName, fieldList, orderBy) {
329
330
  const fields = fieldList.map(f => f.sqlExpression).join(', ');
330
331
  const definitions = this.buildTypeExpression(fieldList);
331
- return `SELECT ARRAY_AGG(CAST(ROW(${fields}) as ROW(${definitions}))) FROM ${lastStageName}\n`;
332
+ const o = this.sqlCombineLastStageOrderBy(orderBy, fieldList);
333
+ return `SELECT ARRAY_AGG(CAST(ROW(${fields}) as ROW(${definitions}))${o}) FROM ${lastStageName}\n`;
332
334
  }
333
335
  sqlSelectAliasAsStruct(alias, fieldList) {
334
- const fields = fieldList.map(f => f.sqlExpression).join(', ');
336
+ const fields = fieldList
337
+ .map(f => `${alias}.${this.sqlQuoteIdentifier(f.rawName)}`)
338
+ .join(', ');
335
339
  const definitions = this.buildTypeExpression(fieldList);
336
- return `CAST(ROW(${fields}) as ROW(${definitions})`;
340
+ return `CAST(ROW(${fields}) as ROW(${definitions}))`;
337
341
  }
338
342
  sqlConvertToCivilTime(expr, timezone, typeDef) {
339
343
  // Trino civil time = TIMESTAMPTZ with query timezone as stored timezone
@@ -341,13 +345,13 @@ ${(0, utils_1.indent)(sql)}
341
345
  if (typeDef.type === 'timestamp') {
342
346
  // TIMESTAMP (UTC wall clock) → TIMESTAMPTZ in query timezone
343
347
  return {
344
- sql: `at_timezone(with_timezone(${expr}, 'UTC'), '${timezone}')`,
348
+ sql: `at_timezone(with_timezone(${expr}, 'UTC'), ${this.sqlTimezoneLiteral(timezone)})`,
345
349
  typeDef: { type: 'timestamptz' },
346
350
  };
347
351
  }
348
352
  // TIMESTAMPTZ → TIMESTAMPTZ in query timezone (same instant, different stored tz)
349
353
  return {
350
- sql: `at_timezone(${expr}, '${timezone}')`,
354
+ sql: `at_timezone(${expr}, ${this.sqlTimezoneLiteral(timezone)})`,
351
355
  typeDef: { type: 'timestamptz' },
352
356
  };
353
357
  }
@@ -392,27 +396,27 @@ ${(0, utils_1.indent)(sql)}
392
396
  if (tz && srcTypeDef && dstTypeDef) {
393
397
  // TIMESTAMP → DATE: interpret as UTC, convert to query timezone
394
398
  if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
395
- return `CAST(at_timezone(with_timezone(${expr}, 'UTC'), '${tz}') AS DATE)`;
399
+ return `CAST(at_timezone(with_timezone(${expr}, 'UTC'), ${this.sqlTimezoneLiteral(tz)}) AS DATE)`;
396
400
  }
397
401
  // TIMESTAMPTZ → DATE: convert to query timezone
398
402
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
399
- return `CAST(at_timezone(${expr}, '${tz}') AS DATE)`;
403
+ return `CAST(at_timezone(${expr}, ${this.sqlTimezoneLiteral(tz)}) AS DATE)`;
400
404
  }
401
405
  // DATE → TIMESTAMP: interpret date in query timezone, return UTC wall clock
402
406
  if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
403
- return `CAST(at_timezone(with_timezone(CAST(${expr} AS TIMESTAMP), '${tz}'), 'UTC') AS TIMESTAMP)`;
407
+ return `CAST(at_timezone(with_timezone(CAST(${expr} AS TIMESTAMP), ${this.sqlTimezoneLiteral(tz)}), 'UTC') AS TIMESTAMP)`;
404
408
  }
405
409
  // DATE → TIMESTAMPTZ: interpret date in query timezone
406
410
  if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
407
- return `with_timezone(CAST(${expr} AS TIMESTAMP), '${tz}')`;
411
+ return `with_timezone(CAST(${expr} AS TIMESTAMP), ${this.sqlTimezoneLiteral(tz)})`;
408
412
  }
409
413
  // TIMESTAMPTZ → TIMESTAMP: convert to query timezone wall clock
410
414
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
411
- return `CAST(at_timezone(${expr}, '${tz}') AS TIMESTAMP)`;
415
+ return `CAST(at_timezone(${expr}, ${this.sqlTimezoneLiteral(tz)}) AS TIMESTAMP)`;
412
416
  }
413
417
  // TIMESTAMP → TIMESTAMPTZ: interpret TIMESTAMP as being in query timezone
414
418
  if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
415
- return `with_timezone(${expr}, '${tz}')`;
419
+ return `with_timezone(${expr}, ${this.sqlTimezoneLiteral(tz)})`;
416
420
  }
417
421
  }
418
422
  // No special handling needed, or no query timezone
@@ -567,13 +571,13 @@ ${(0, utils_1.indent)(sql)}
567
571
  const tz = timezone || qtz(qi);
568
572
  if (tz) {
569
573
  // Interpret wall clock time in timezone, convert to UTC wall clock, cast to TIMESTAMP
570
- return `CAST(at_timezone(with_timezone(TIMESTAMP '${literal}', '${tz}'), 'UTC') AS TIMESTAMP)`;
574
+ return `CAST(at_timezone(with_timezone(TIMESTAMP '${literal}', ${this.sqlTimezoneLiteral(tz)}), 'UTC') AS TIMESTAMP)`;
571
575
  }
572
576
  return `TIMESTAMP '${literal}'`;
573
577
  }
574
578
  sqlTimestamptzLiteral(_qi, literal, timezone) {
575
579
  // Use with_timezone to create a TIMESTAMP WITH TIME ZONE
576
- return `with_timezone(TIMESTAMP '${literal}', '${timezone}')`;
580
+ return `with_timezone(TIMESTAMP '${literal}', ${this.sqlTimezoneLiteral(timezone)})`;
577
581
  }
578
582
  sqlTimeExtractExpr(qi, from) {
579
583
  const pgUnits = pg_impl_1.timeExtractMap[from.units] || from.units;
@@ -584,11 +588,11 @@ ${(0, utils_1.indent)(sql)}
584
588
  // Convert both TIMESTAMP and TIMESTAMPTZ to query timezone for extraction
585
589
  if (from.e.typeDef.type === 'timestamp') {
586
590
  // TIMESTAMP: interpret as UTC, convert to query timezone
587
- extractFrom = `at_timezone(with_timezone(${extractFrom}, 'UTC'), '${tz}')`;
591
+ extractFrom = `at_timezone(with_timezone(${extractFrom}, 'UTC'), ${this.sqlTimezoneLiteral(tz)})`;
588
592
  }
589
593
  else {
590
594
  // TIMESTAMPTZ: convert to query timezone
591
- extractFrom = `at_timezone(${extractFrom}, '${tz}')`;
595
+ extractFrom = `at_timezone(${extractFrom}, ${this.sqlTimezoneLiteral(tz)})`;
592
596
  }
593
597
  }
594
598
  }
@@ -628,18 +632,18 @@ class PrestoDialect extends TrinoDialect {
628
632
  sqlTimestampLiteral(qi, literal, timezone) {
629
633
  const tz = timezone || qtz(qi);
630
634
  if (tz) {
631
- return `CAST(TIMESTAMP '${literal} ${tz}' AT TIME ZONE 'UTC' AS TIMESTAMP)`;
635
+ return `CAST(TIMESTAMP '${literal} ${this.checkedTimezoneName(tz)}' AT TIME ZONE 'UTC' AS TIMESTAMP)`;
632
636
  }
633
637
  return `TIMESTAMP '${literal}'`;
634
638
  }
635
639
  sqlTimestamptzLiteral(_qi, literal, timezone) {
636
- return `TIMESTAMP '${literal} ${timezone}'`;
640
+ return `TIMESTAMP '${literal} ${this.checkedTimezoneName(timezone)}'`;
637
641
  }
638
642
  sqlConvertToCivilTime(expr, timezone, _typeDef) {
639
643
  // Presto's AT TIME ZONE operator (not function) produces TIMESTAMPTZ
640
644
  // Reinterprets the instant in the target timezone
641
645
  return {
642
- sql: `${expr} AT TIME ZONE '${timezone}'`,
646
+ sql: `${expr} AT TIME ZONE ${this.sqlTimezoneLiteral(timezone)}`,
643
647
  typeDef: { type: 'timestamptz' },
644
648
  };
645
649
  }
@@ -658,26 +662,26 @@ class PrestoDialect extends TrinoDialect {
658
662
  if (tz && srcTypeDef && dstTypeDef) {
659
663
  // TIMESTAMP → DATE: interpret as UTC, convert to query timezone
660
664
  if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
661
- return `CAST((${expr} AT TIME ZONE 'UTC') AT TIME ZONE '${tz}' AS DATE)`;
665
+ return `CAST((${expr} AT TIME ZONE 'UTC') AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS DATE)`;
662
666
  }
663
667
  // TIMESTAMPTZ → DATE: convert to query timezone
664
668
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
665
- return `CAST(${expr} AT TIME ZONE '${tz}' AS DATE)`;
669
+ return `CAST(${expr} AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS DATE)`;
666
670
  }
667
671
  // DATE → TIMESTAMP: interpret date in query timezone, return UTC wall clock
668
672
  // Presto doesn't have a way to interpret TIMESTAMP in a non-UTC timezone,
669
673
  // so we build a TIMESTAMPTZ literal string and cast it
670
674
  if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
671
- const tstzLiteral = `CAST(CAST(${expr} AS VARCHAR) || ' 00:00:00 ${tz}' AS TIMESTAMP WITH TIME ZONE)`;
675
+ const tstzLiteral = `CAST(CAST(${expr} AS VARCHAR) || ' 00:00:00 ${this.checkedTimezoneName(tz)}' AS TIMESTAMP WITH TIME ZONE)`;
672
676
  return `CAST(${tstzLiteral} AS TIMESTAMP)`;
673
677
  }
674
678
  // DATE → TIMESTAMPTZ: interpret date in query timezone
675
679
  if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
676
- return `CAST(CAST(${expr} AS VARCHAR) || ' 00:00:00 ${tz}' AS TIMESTAMP WITH TIME ZONE)`;
680
+ return `CAST(CAST(${expr} AS VARCHAR) || ' 00:00:00 ${this.checkedTimezoneName(tz)}' AS TIMESTAMP WITH TIME ZONE)`;
677
681
  }
678
682
  // TIMESTAMPTZ → TIMESTAMP: convert to query timezone wall clock
679
683
  if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
680
- return `CAST(${expr} AT TIME ZONE '${tz}' AS TIMESTAMP)`;
684
+ return `CAST(${expr} AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS TIMESTAMP)`;
681
685
  }
682
686
  // TIMESTAMP → TIMESTAMPTZ: interpret TIMESTAMP as UTC
683
687
  if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
@@ -701,11 +705,11 @@ class PrestoDialect extends TrinoDialect {
701
705
  if (from.e.typeDef.type === 'timestamp') {
702
706
  // TIMESTAMP: interpret as UTC, convert to query timezone
703
707
  // Presto uses AT TIME ZONE operator
704
- extractFrom = `(${extractFrom} AT TIME ZONE 'UTC') AT TIME ZONE '${tz}'`;
708
+ extractFrom = `(${extractFrom} AT TIME ZONE 'UTC') AT TIME ZONE ${this.sqlTimezoneLiteral(tz)}`;
705
709
  }
706
710
  else {
707
711
  // TIMESTAMPTZ: convert to query timezone
708
- extractFrom = `${extractFrom} AT TIME ZONE '${tz}'`;
712
+ extractFrom = `${extractFrom} AT TIME ZONE ${this.sqlTimezoneLiteral(tz)}`;
709
713
  }
710
714
  }
711
715
  }
@@ -45,6 +45,7 @@ const static_space_1 = require("../field-space/static-space");
45
45
  const TDU = __importStar(require("../typedesc-utils"));
46
46
  const field_references_1 = require("../query-items/field-references");
47
47
  const expression_def_1 = require("../types/expression-def");
48
+ const field_space_1 = require("../types/field-space");
48
49
  const space_field_1 = require("../types/space-field");
49
50
  const expr_id_reference_1 = require("./expr-id-reference");
50
51
  class ExprAggregateFunction extends expression_def_1.ExpressionDef {
@@ -224,37 +225,30 @@ function joinPathEq(a1, a2) {
224
225
  }
225
226
  function getJoinUsage(fs, expr) {
226
227
  const result = [];
228
+ /**
229
+ * Walk a path from translated IR. Every name in it is known to resolve at
230
+ * private level, so a failure is an internal error. The names are
231
+ * unparented; readEntry records no references for them.
232
+ */
227
233
  const lookupWithPath = (fs, path) => {
228
- const head = path[0];
229
- const rest = path.slice(1);
230
- const def = fs.entry(head);
231
- if (def === undefined) {
232
- throw new Error(`Invalid field lookup ${head}`);
233
- }
234
- if (def instanceof static_space_1.StructSpaceField && rest.length > 0) {
235
- const restDef = lookupWithPath(def.fieldSpace, rest);
236
- return {
237
- ...restDef,
238
- joinPath: [{ ...def.joinPathElement, name: head }, ...restDef.joinPath],
239
- };
234
+ const names = path.map(n => new field_space_1.FieldName(n));
235
+ const last = names[names.length - 1];
236
+ const ns = (0, static_space_1.resolveNamespace)(fs, names.slice(0, -1), 'private', last.name);
237
+ if (ns.error) {
238
+ throw new Error(ns.error.message);
240
239
  }
241
- else if (def instanceof space_field_1.SpaceField) {
242
- if (rest.length !== 0) {
243
- throw new Error(`${head} cannot contain a ${rest.join('.')}`);
244
- }
245
- const fieldDef = def.fieldDef();
246
- if (fieldDef) {
247
- return {
248
- fs,
249
- def: fieldDef,
250
- joinPath: [],
251
- };
252
- }
253
- throw new Error('No field def');
240
+ const read = (0, static_space_1.readEntry)(ns.space, last, 'private');
241
+ if (read.error) {
242
+ throw new Error(read.error.message);
254
243
  }
255
- else {
244
+ if (!(read.found instanceof space_field_1.SpaceField)) {
256
245
  throw new Error('expected a field def or struct');
257
246
  }
247
+ const def = read.found.fieldDef();
248
+ if (def === undefined) {
249
+ throw new Error('No field def');
250
+ }
251
+ return { fs: ns.space, def, joinPath: ns.joinPath };
258
252
  };
259
253
  for (const frag of (0, utils_1.exprWalk)(expr)) {
260
254
  if (frag.node === 'field') {
@@ -23,6 +23,7 @@ class DefSpace extends passthrough_space_1.PassthroughSpace {
23
23
  error: {
24
24
  message: `Circular reference to '${this.circular.defineName}' in definition`,
25
25
  code: 'circular-reference-in-field-definition',
26
+ at: symbol[0],
26
27
  },
27
28
  found: undefined,
28
29
  };