@malloydata/malloy 0.0.433 → 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 (45) 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 +8 -8
  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/source-elements/refined-source.js +1 -1
  22. package/dist/lang/ast/source-elements/sql-source.js +2 -1
  23. package/dist/lang/ast/source-properties/timezone-statement.d.ts +4 -3
  24. package/dist/lang/ast/source-properties/timezone-statement.js +14 -12
  25. package/dist/lang/composite-source-utils.js +10 -3
  26. package/dist/lang/malloy-to-ast.js +2 -8
  27. package/dist/lang/parse-malloy.d.ts +3 -2
  28. package/dist/lang/parse-malloy.js +2 -1
  29. package/dist/lang/test/test-translator.d.ts +2 -1
  30. package/dist/lang/test/test-translator.js +1 -1
  31. package/dist/model/join_instance.d.ts +1 -0
  32. package/dist/model/join_instance.js +9 -1
  33. package/dist/model/query_query.d.ts +25 -0
  34. package/dist/model/query_query.js +142 -17
  35. package/dist/model/sql_block.d.ts +2 -2
  36. package/dist/model/sql_block.js +11 -47
  37. package/dist/model/sql_compiled.d.ts +3 -1
  38. package/dist/model/sql_compiled.js +8 -6
  39. package/dist/model/stage_writer.js +3 -3
  40. package/dist/model/utils.d.ts +2 -2
  41. package/dist/model/utils.js +2 -3
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/femto-config.motly +2 -0
  45. package/package.json +4 -4
@@ -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
  }
@@ -81,7 +81,7 @@ class RefinedSource extends source_1.Source {
81
81
  filters.push(el);
82
82
  }
83
83
  else if (el instanceof timezone_statement_1.TimezoneStatement) {
84
- newTimezone = el.tz;
84
+ newTimezone = el.timezoneName();
85
85
  }
86
86
  else {
87
87
  errTo.logError('unexpected-source-property', `Unexpected source property: '${errTo.elementType}'`);
@@ -19,12 +19,13 @@ class SQLSource extends source_1.Source {
19
19
  this.connectionNameInvalid = false;
20
20
  }
21
21
  sqlSourceRequest(doc) {
22
+ var _a;
22
23
  const partialModel = this.select.containsQueries
23
24
  ? doc.modelDef()
24
25
  : undefined;
25
26
  const [valid, phrases] = this.select.sqlPhrases(this.connectionName.refString);
26
27
  if (valid) {
27
- return (0, sql_block_1.getSourceRequest)(phrases, this.connectionName.refString, partialModel);
28
+ return (0, sql_block_1.getSourceRequest)(phrases, this.connectionName.refString, partialModel, (_a = this.translator()) === null || _a === void 0 ? void 0 : _a.root.virtualMap);
28
29
  }
29
30
  return undefined;
30
31
  }
@@ -1,12 +1,13 @@
1
+ import type { ExprString } from '../expressions/expr-string';
1
2
  import type { QueryBuilder } from '../types/query-builder';
2
3
  import { MalloyElement } from '../types/malloy-element';
3
4
  import type { QueryPropertyInterface } from '../types/query-property-interface';
4
5
  export declare class TimezoneStatement extends MalloyElement implements QueryPropertyInterface {
5
- readonly tz: string;
6
+ private readonly timezone;
6
7
  elementType: string;
7
8
  forceQueryClass: undefined;
8
9
  queryRefinementStage: undefined;
9
- constructor(tz: string);
10
- get isValid(): boolean;
10
+ constructor(timezone: ExprString);
11
+ timezoneName(): string | undefined;
11
12
  queryExecute(executeFor: QueryBuilder): void;
12
13
  }
@@ -5,27 +5,29 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.TimezoneStatement = void 0;
8
+ const dialect_1 = require("../../../dialect/dialect");
8
9
  const malloy_element_1 = require("../types/malloy-element");
9
- const luxon_1 = require("luxon");
10
10
  class TimezoneStatement extends malloy_element_1.MalloyElement {
11
- constructor(tz) {
12
- super();
13
- this.tz = tz;
11
+ constructor(timezone) {
12
+ super({ timezone });
13
+ this.timezone = timezone;
14
14
  this.elementType = 'timezone';
15
15
  this.forceQueryClass = undefined;
16
16
  this.queryRefinementStage = undefined;
17
17
  }
18
- get isValid() {
19
- try {
20
- luxon_1.DateTime.fromISO('2020-02-19T00:00:00', { zone: this.tz });
21
- return true;
22
- }
23
- catch {
24
- return false;
18
+ timezoneName() {
19
+ const name = this.timezone.value;
20
+ if (!dialect_1.Dialect.isSafeTimezoneName(name)) {
21
+ this.logError('invalid-timezone', { timezone: name }, { at: this.timezone.location });
22
+ return undefined;
25
23
  }
24
+ return name;
26
25
  }
27
26
  queryExecute(executeFor) {
28
- executeFor.resultFS.setTimezone(this.tz);
27
+ const timezone = this.timezoneName();
28
+ if (timezone !== undefined) {
29
+ executeFor.resultFS.setTimezone(timezone);
30
+ }
29
31
  }
30
32
  }
31
33
  exports.TimezoneStatement = TimezoneStatement;
@@ -407,9 +407,11 @@ function expandRefUsage(seed, fields, alwaysJoinNames = []) {
407
407
  // given usage into the accumulator. Used both in-loop (joins reached
408
408
  // via reference paths) and upfront (always-on segment-level joins).
409
409
  function activateJoin(joinDef, joinPath) {
410
+ if (!(0, malloy_types_1.isJoined)(joinDef))
411
+ return;
410
412
  const joinKey = (0, utils_2.pathToKey)('join', joinPath);
411
413
  const thisDep = getJoin(activeJoinGraph, joinKey, joinPath);
412
- if (!(0, malloy_types_1.isJoined)(joinDef) || thisDep.checked)
414
+ if (thisDep.checked)
413
415
  return;
414
416
  thisDep.checked = true;
415
417
  const joinFieldUsage = getJoinFieldUsage(joinDef, joinPath);
@@ -462,8 +464,13 @@ function expandRefUsage(seed, fields, alwaysJoinNames = []) {
462
464
  ungroupings.push(...joinedUngroupings(refPath, def.ungroupings));
463
465
  }
464
466
  }
465
- // For paths through joins, additionaly track join relationships
466
- for (let joinLen = 1; joinLen < reference.path.length; joinLen++) {
467
+ // Track the joins along the path. A path which ends at a join and carries
468
+ // a unique key requirement is an aggregate computed over that join's rows,
469
+ // which should then include the join's on-clause fields in the field usage.
470
+ // A path which merely names a join-typed value does not.
471
+ const endsInTraversedJoin = reference.uniqueKeyRequirement !== undefined;
472
+ const joinDepth = reference.path.length - (endsInTraversedJoin ? 0 : 1);
473
+ for (let joinLen = 1; joinLen <= joinDepth; joinLen++) {
467
474
  const joinPath = reference.path.slice(0, joinLen);
468
475
  const joinDef = inNamespace(joinPath, fieldNameSpace);
469
476
  if (!joinDef)
@@ -803,14 +803,8 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
803
803
  return this.visitTimezoneStatement(cx.timezoneStatement());
804
804
  }
805
805
  visitTimezoneStatement(cx) {
806
- const timezone = this.getPlainStringFrom(cx);
807
- const timezoneStatement = this.astAt(new ast.TimezoneStatement(timezone), cx.string());
808
- if (!timezoneStatement.isValid) {
809
- this.astError(timezoneStatement, 'invalid-timezone', {
810
- timezone: timezoneStatement.tz,
811
- });
812
- }
813
- return this.astAt(timezoneStatement, cx);
806
+ const timezone = this.astAt(new ast.ExprString(this.getPlainStringFrom(cx)), cx.string());
807
+ return this.astAt(new ast.TimezoneStatement(timezone), cx);
814
808
  }
815
809
  visitQueryProperties(pcx) {
816
810
  const qProps = this.only(pcx.queryStatement().map(qcx => this.astAt(this.visit(qcx), qcx)), x => ast.isQueryProperty(x) && x, 'query statement');
@@ -1,4 +1,4 @@
1
- import type { DocumentLocation, DocumentPosition, DocumentReference, ImportLocation, ModelDef, SourceDef, SQLSourceDef, DependencyTree, DocumentRange } from '../model/malloy_types';
1
+ import type { DocumentLocation, DocumentPosition, DocumentReference, ImportLocation, ModelDef, SourceDef, SQLSourceDef, DependencyTree, DocumentRange, VirtualMap } from '../model/malloy_types';
2
2
  import type { LogMessage, LogMessageOptions, MessageCode, MessageParameterType } from './parse-log';
3
3
  import { BaseMessageLogger } from './parse-log';
4
4
  import type { ZoneData } from './zone';
@@ -186,6 +186,7 @@ export declare class MalloyChildTranslator extends MalloyTranslation {
186
186
  export declare class MalloyTranslator extends MalloyTranslation {
187
187
  private readonly eventStream;
188
188
  readonly restrictedMode: boolean;
189
+ virtualMap?: VirtualMap | undefined;
189
190
  schemaZone: Zone<SourceDef>;
190
191
  importZone: Zone<string>;
191
192
  pretranslatedModels: Map<string, ModelDef>;
@@ -193,7 +194,7 @@ export declare class MalloyTranslator extends MalloyTranslation {
193
194
  connectionDialectZone: Zone<string>;
194
195
  logger: BaseMessageLogger;
195
196
  readonly root: MalloyTranslator;
196
- constructor(rootURL: string, importURL?: string | null, preload?: ParseUpdate | null, eventStream?: EventStream | null, restrictedMode?: boolean);
197
+ constructor(rootURL: string, importURL?: string | null, preload?: ParseUpdate | null, eventStream?: EventStream | null, restrictedMode?: boolean, virtualMap?: VirtualMap | undefined);
197
198
  update(dd: ParseUpdate): void;
198
199
  private lockZonesIfRestricted;
199
200
  translate(extendingModel?: ModelDef): TranslateResponse;
@@ -780,10 +780,11 @@ exports.MalloyChildTranslator = MalloyChildTranslator;
780
780
  * no need to call again, the translation is finished or error'd.
781
781
  */
782
782
  class MalloyTranslator extends MalloyTranslation {
783
- constructor(rootURL, importURL = null, preload = null, eventStream = null, restrictedMode = false) {
783
+ constructor(rootURL, importURL = null, preload = null, eventStream = null, restrictedMode = false, virtualMap) {
784
784
  super(rootURL, importURL);
785
785
  this.eventStream = eventStream;
786
786
  this.restrictedMode = restrictedMode;
787
+ this.virtualMap = virtualMap;
787
788
  this.schemaZone = new zone_1.Zone();
788
789
  this.importZone = new zone_1.Zone();
789
790
  this.pretranslatedModels = new Map();
@@ -1,4 +1,4 @@
1
- import type { DocumentLocation, FieldDef, ModelDef, NamedModelObject, PipeSegment, Query, QueryFieldDef, StructDef, TurtleDef, SourceDef, TableSourceDef, SQLSourceDef, UserTypeDef } from '../../model/malloy_types';
1
+ import type { DocumentLocation, FieldDef, ModelDef, NamedModelObject, PipeSegment, Query, QueryFieldDef, StructDef, TurtleDef, SourceDef, TableSourceDef, SQLSourceDef, UserTypeDef, VirtualMap } from '../../model/malloy_types';
2
2
  import { MalloyElement } from '../ast';
3
3
  import type { NameSpace } from '../ast/types/name-space';
4
4
  import type { ModelEntry } from '../ast/types/model-entry';
@@ -56,6 +56,7 @@ export interface TestTranslatorOptions {
56
56
  eventStream?: EventStream | null;
57
57
  internalModel?: ModelDef;
58
58
  restrictedMode?: boolean;
59
+ virtualMap?: VirtualMap;
59
60
  /**
60
61
  * Each entry is a compiler-flag tag fragment — the content that
61
62
  * would follow `##! ` in a Malloy source annotation. Rendered to
@@ -339,7 +339,7 @@ const testURI = 'internal://test/langtests/root.malloy';
339
339
  class TestTranslator extends parse_malloy_1.MalloyTranslator {
340
340
  constructor(testSrc, options = {}) {
341
341
  var _a, _b, _c, _d, _e;
342
- super(testURI, (_a = options.importBaseURL) !== null && _a !== void 0 ? _a : null, null, (_b = options.eventStream) !== null && _b !== void 0 ? _b : null, (_c = options.restrictedMode) !== null && _c !== void 0 ? _c : false);
342
+ super(testURI, (_a = options.importBaseURL) !== null && _a !== void 0 ? _a : null, null, (_b = options.eventStream) !== null && _b !== void 0 ? _b : null, (_c = options.restrictedMode) !== null && _c !== void 0 ? _c : false, options.virtualMap);
343
343
  this.testSrc = testSrc;
344
344
  this.allDialectsEnabled = true;
345
345
  /*
@@ -12,6 +12,7 @@ export declare class JoinInstance {
12
12
  joinFilterConditions?: QueryFieldBoolean[];
13
13
  children: JoinInstance[];
14
14
  constructor(queryStruct: QueryStruct, alias: string, parent: JoinInstance | undefined);
15
+ isSubquery(): boolean;
15
16
  parentRelationship(): 'root' | JoinRelationship;
16
17
  forceAllSymmetricCalculations(): boolean;
17
18
  getDialectFieldList(): DialectFieldList;