@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
@@ -2,7 +2,7 @@ import type { LogMessage } from '../../lang';
2
2
  import type { Connection, InfoConnection, LookupConnection, FetchSchemaOptions } from '../../connection/types';
3
3
  import type { ConnectionTypeDef, ConnectionPropertyDefinition } from '../../connection/registry';
4
4
  import type { URLReader, EventStream } from '../../runtime_types';
5
- import type { SQLSourceDef, QueryRunStats } from '../../model';
5
+ import type { SQLSourceDef, QueryRunStats, VirtualMap } from '../../model';
6
6
  import type { RunSQLOptions } from '../../run_sql_options';
7
7
  import type { CacheManager } from './cache';
8
8
  import { Parse } from './document';
@@ -36,6 +36,7 @@ export interface MalloyCompileOptions {
36
36
  eventStream?: EventStream;
37
37
  importBaseURL?: URL;
38
38
  testEnvironment?: boolean;
39
+ virtualMap?: VirtualMap;
39
40
  }
40
41
  export interface MalloyRunOptions {
41
42
  connection?: Connection;
@@ -119,9 +119,10 @@ class Malloy {
119
119
  * @return A (promise of a) compiled `Model`.
120
120
  */
121
121
  static async compile(req) {
122
- var _a, _b, _c, _d, _e;
122
+ var _a, _b, _c, _d, _e, _f;
123
123
  let { url, source, importBaseURL, cacheManager } = req;
124
124
  const { parse, urlReader, connections, model, refreshSchemaCache, noThrowOnError, eventStream, restrictedMode, method, } = req;
125
+ const virtualMap = (_a = req.virtualMap) !== null && _a !== void 0 ? _a : parse === null || parse === void 0 ? void 0 : parse._translator.virtualMap;
125
126
  if (restrictedMode) {
126
127
  // Restricted-mode compiles do not participate in the model-def
127
128
  // cache. The cache key is the URL, but restricted vs. unrestricted
@@ -172,7 +173,8 @@ class Malloy {
172
173
  throw new Error(`Malloy.compile: restrictedMode (${restrictedMode}) does not match the pre-parsed translator's restrictedMode (${parse._translator.restrictedMode}). Set restrictedMode at parse time.`);
173
174
  }
174
175
  translator = parse._translator;
175
- const invalidationKey = (_a = parse._invalidationKey) !== null && _a !== void 0 ? _a : (await (0, readers_1.getInvalidationKey)(urlReader, url));
176
+ translator.virtualMap = virtualMap;
177
+ const invalidationKey = (_b = parse._invalidationKey) !== null && _b !== void 0 ? _b : (await (0, readers_1.getInvalidationKey)(urlReader, url));
176
178
  invalidationKeys[_url] = invalidationKey;
177
179
  }
178
180
  else {
@@ -187,7 +189,7 @@ class Malloy {
187
189
  }
188
190
  translator = new lang_1.MalloyTranslator(_url, importBaseURL.toString(), {
189
191
  urls: { [_url]: source },
190
- }, eventStream, restrictedMode !== null && restrictedMode !== void 0 ? restrictedMode : false);
192
+ }, eventStream, restrictedMode !== null && restrictedMode !== void 0 ? restrictedMode : false, virtualMap);
191
193
  }
192
194
  for (;;) {
193
195
  const result = translator.translate(model === null || model === void 0 ? void 0 : model._modelDef);
@@ -208,14 +210,14 @@ class Malloy {
208
210
  const existingQueryModel = !result.modelWasModified && model
209
211
  ? model.getExistingQueryModel()
210
212
  : undefined;
211
- return new core_1.Model(result.modelDef, result.problems || [], [...((_b = model === null || model === void 0 ? void 0 : model.fromSources) !== null && _b !== void 0 ? _b : []), ...((_c = result.fromSources) !== null && _c !== void 0 ? _c : [])], existingQueryModel);
213
+ return new core_1.Model(result.modelDef, result.problems || [], [...((_c = model === null || model === void 0 ? void 0 : model.fromSources) !== null && _c !== void 0 ? _c : []), ...((_d = result.fromSources) !== null && _d !== void 0 ? _d : [])], existingQueryModel);
212
214
  }
213
215
  else if (noThrowOnError) {
214
216
  const emptyModel = (0, model_1.mkModelDef)('modelDidNotCompile');
215
217
  const modelFromCompile = (model === null || model === void 0 ? void 0 : model._modelDef) || emptyModel;
216
218
  return new core_1.Model(modelFromCompile, result.problems || [], [
217
- ...((_d = model === null || model === void 0 ? void 0 : model.fromSources) !== null && _d !== void 0 ? _d : []),
218
- ...((_e = result.fromSources) !== null && _e !== void 0 ? _e : []),
219
+ ...((_e = model === null || model === void 0 ? void 0 : model.fromSources) !== null && _e !== void 0 ? _e : []),
220
+ ...((_f = result.fromSources) !== null && _f !== void 0 ? _f : []),
219
221
  ]);
220
222
  }
221
223
  else {
@@ -169,7 +169,7 @@ class MalloyConfig {
169
169
  this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, rawConnectionEntries(pojo), mergedOverlays, log);
170
170
  this._connections = this._managedLookup;
171
171
  this._overlays = mergedOverlays;
172
- this.virtualMap = toVirtualMap(prepared.virtualMap, log);
172
+ this.virtualMap = toVirtualMap(prepared.virtualMap);
173
173
  this.configURL = configURL;
174
174
  this.rootDirectory = rootDirectory;
175
175
  this.manifestPath = prepared.manifestPath;
@@ -444,10 +444,10 @@ function validateURLString(value, fieldName, log) {
444
444
  }
445
445
  /**
446
446
  * Convert the raw virtualMap POJO shape (a dict of dicts of strings) into
447
- * the runtime Map-of-Maps representation. Invalid entries are dropped and
448
- * logged — they'd be pasted into FROM clauses downstream otherwise.
447
+ * the runtime Map-of-Maps representation. Table paths are checked against
448
+ * the executing dialect when a virtual source is substituted into SQL.
449
449
  */
450
- function toVirtualMap(raw, log) {
450
+ function toVirtualMap(raw) {
451
451
  if (!isRecord(raw))
452
452
  return undefined;
453
453
  const outer = new Map();
@@ -456,18 +456,9 @@ function toVirtualMap(raw, log) {
456
456
  continue;
457
457
  const innerMap = new Map();
458
458
  for (const [virtualName, tablePath] of Object.entries(inner)) {
459
- if (typeof tablePath !== 'string')
460
- continue;
461
- const invalid = (0, validate_table_path_1.validateCanonicalTablePathAnyDialect)(tablePath);
462
- if (invalid !== undefined) {
463
- log.push({
464
- message: `virtualMap entry '${connName}.${virtualName}': ${invalid}`,
465
- severity: 'error',
466
- code: 'config-validation',
467
- });
468
- continue;
459
+ if (typeof tablePath === 'string') {
460
+ innerMap.set(virtualName, tablePath);
469
461
  }
470
- innerMap.set(virtualName, tablePath);
471
462
  }
472
463
  if (innerMap.size > 0)
473
464
  outer.set(connName, innerMap);
@@ -172,13 +172,8 @@ export declare class Runtime {
172
172
  /** @internal */
173
173
  _invalidateGivensCache(): void;
174
174
  /**
175
- * The virtual map for virtual source resolution.
176
- * When set, compiled queries automatically resolve virtual sources
177
- * against this map. Can be overridden per-query via
178
- * CompileQueryOptions.virtualMap.
179
- *
180
- * When constructed with a MalloyConfig, falls through to
181
- * config.virtualMap.
175
+ * Default virtual-source bindings for translation and query SQL generation.
176
+ * Falls back to config.virtualMap when no runtime map is assigned.
182
177
  */
183
178
  get virtualMap(): VirtualMap | undefined;
184
179
  set virtualMap(map: VirtualMap | undefined);
@@ -302,13 +302,8 @@ class Runtime {
302
302
  this._resolvedGivensPromise = undefined;
303
303
  }
304
304
  /**
305
- * The virtual map for virtual source resolution.
306
- * When set, compiled queries automatically resolve virtual sources
307
- * against this map. Can be overridden per-query via
308
- * CompileQueryOptions.virtualMap.
309
- *
310
- * When constructed with a MalloyConfig, falls through to
311
- * config.virtualMap.
305
+ * Default virtual-source bindings for translation and query SQL generation.
306
+ * Falls back to config.virtualMap when no runtime map is assigned.
312
307
  */
313
308
  get virtualMap() {
314
309
  var _a, _b;
@@ -365,6 +360,7 @@ class Runtime {
365
360
  }
366
361
  const compilable = source instanceof URL ? { url: source } : { source };
367
362
  return new ModelMaterializer(this, async () => {
363
+ var _a;
368
364
  const m = await compile_1.Malloy.compile({
369
365
  ...compilable,
370
366
  urlReader: this.urlReader,
@@ -375,6 +371,7 @@ class Runtime {
375
371
  importBaseURL: options === null || options === void 0 ? void 0 : options.importBaseURL,
376
372
  testEnvironment: options === null || options === void 0 ? void 0 : options.testEnvironment,
377
373
  cacheManager: this.cacheManager,
374
+ virtualMap: (_a = options === null || options === void 0 ? void 0 : options.virtualMap) !== null && _a !== void 0 ? _a : this.virtualMap,
378
375
  });
379
376
  return this._withRuntimeContext(m);
380
377
  }, options);
@@ -640,6 +637,7 @@ class ModelMaterializer extends FluentState {
640
637
  ...callQueryOptions,
641
638
  };
642
639
  return this.makeQueryMaterializer(async () => {
640
+ var _a;
643
641
  const compilable = query instanceof URL ? { url: query } : { source: query };
644
642
  const queryModel = await compile_1.Malloy.compile({
645
643
  ...compilable,
@@ -651,6 +649,7 @@ class ModelMaterializer extends FluentState {
651
649
  importBaseURL,
652
650
  testEnvironment: testEnvironment || this.runtime.isTestRuntime,
653
651
  ...compileQueryOptions,
652
+ virtualMap: (_a = compileQueryOptions.virtualMap) !== null && _a !== void 0 ? _a : this.runtime.virtualMap,
654
653
  });
655
654
  return queryModel.preparedQuery;
656
655
  }, compileQueryOptions);
@@ -725,6 +724,8 @@ class ModelMaterializer extends FluentState {
725
724
  * or loading further related objects.
726
725
  */
727
726
  extendModel(query, options) {
727
+ var _a, _b;
728
+ const virtualMap = (_a = options === null || options === void 0 ? void 0 : options.virtualMap) !== null && _a !== void 0 ? _a : (_b = this.compileQueryOptions) === null || _b === void 0 ? void 0 : _b.virtualMap;
728
729
  if (this.runtime.isTestRuntime) {
729
730
  if (options === undefined) {
730
731
  options = { testEnvironment: true };
@@ -749,9 +750,10 @@ class ModelMaterializer extends FluentState {
749
750
  importBaseURL: options === null || options === void 0 ? void 0 : options.importBaseURL,
750
751
  testEnvironment: options === null || options === void 0 ? void 0 : options.testEnvironment,
751
752
  ...this.compileQueryOptions,
753
+ virtualMap: virtualMap !== null && virtualMap !== void 0 ? virtualMap : this.runtime.virtualMap,
752
754
  });
753
755
  return this.runtime._withRuntimeContext(queryModel);
754
- }, options);
756
+ }, { ...options, virtualMap });
755
757
  }
756
758
  async search(sourceName, searchTerm, limit = 1000, searchField = undefined) {
757
759
  const model = await this.materialize();
@@ -1064,15 +1066,7 @@ class QueryMaterializer extends FluentState {
1064
1066
  connectionDigests[connName] = conn.getDigest();
1065
1067
  }
1066
1068
  }
1067
- // Use virtualMap from options if provided, otherwise fall back to Runtime's.
1068
1069
  const virtualMap = (_b = mergedOptions.virtualMap) !== null && _b !== void 0 ? _b : this.runtime.virtualMap;
1069
- if (virtualMap) {
1070
- for (const [connName, inner] of virtualMap) {
1071
- for (const [virtualName, tablePath] of inner) {
1072
- (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(tablePath, `virtualMap entry '${connName}.${virtualName}'`);
1073
- }
1074
- }
1075
- }
1076
1070
  // Per-query supply for a finalized given is rejected at API entry
1077
1071
  // — the finalized-givens set is the runtime's "this can't be
1078
1072
  // overridden by the caller" guarantee. Fail before any IO so misuse
@@ -43,6 +43,7 @@ export type CompileMethod = 'loadModel' | 'extendModel' | 'query';
43
43
  export interface CompileOptions {
44
44
  refreshSchemaCache?: boolean | number;
45
45
  noThrowOnError?: boolean;
46
+ virtualMap?: VirtualMap;
46
47
  }
47
48
  /** Options given to the Malloy compiler (QueryModel) */
48
49
  export interface CompileQueryOptions {
@@ -2,8 +2,7 @@
2
2
  export declare function validateCanonicalTablePath(dialectName: string, tablePath: string): string | undefined;
3
3
  /**
4
4
  * Validate against any registered dialect. Used at boundaries where the
5
- * destination dialect isn't synchronously known (virtualMap, manifest
6
- * entries).
5
+ * destination dialect isn't synchronously known (manifest entries).
7
6
  */
8
7
  export declare function validateCanonicalTablePathAnyDialect(tablePath: string): string | undefined;
9
8
  /** Throw if `tablePath` isn't canonical SQL in any registered dialect. */
@@ -28,8 +28,7 @@ function validateCanonicalTablePath(dialectName, tablePath) {
28
28
  }
29
29
  /**
30
30
  * Validate against any registered dialect. Used at boundaries where the
31
- * destination dialect isn't synchronously known (virtualMap, manifest
32
- * entries).
31
+ * destination dialect isn't synchronously known (manifest entries).
33
32
  */
34
33
  function validateCanonicalTablePathAnyDialect(tablePath) {
35
34
  let suggestion;
@@ -304,13 +304,13 @@ class DatabricksDialect extends dialect_1.Dialect {
304
304
  // Databricks has no timestamptz type; timestamps are stored as UTC
305
305
  // Convert from UTC to local timezone for civil time operations
306
306
  return {
307
- sql: `FROM_UTC_TIMESTAMP(${expr}, '${timezone}')`,
307
+ sql: `FROM_UTC_TIMESTAMP(${expr}, ${this.sqlTimezoneLiteral(timezone)})`,
308
308
  typeDef: { type: 'timestamp' },
309
309
  };
310
310
  }
311
311
  sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
312
312
  // Convert from local timezone back to UTC
313
- return `TO_UTC_TIMESTAMP(${expr}, '${timezone}')`;
313
+ return `TO_UTC_TIMESTAMP(${expr}, ${this.sqlTimezoneLiteral(timezone)})`;
314
314
  }
315
315
  sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
316
316
  // Databricks DATE_TRUNC starts weeks on Monday; Malloy wants Sunday.
@@ -342,7 +342,7 @@ class DatabricksDialect extends dialect_1.Dialect {
342
342
  if (malloy_types_1.TD.isTimestamp(te.e.typeDef)) {
343
343
  const tz = (0, dialect_1.qtz)(qi);
344
344
  if (tz) {
345
- extractFrom = `FROM_UTC_TIMESTAMP(${extractFrom}, '${tz}')`;
345
+ extractFrom = `FROM_UTC_TIMESTAMP(${extractFrom}, ${this.sqlTimezoneLiteral(tz)})`;
346
346
  }
347
347
  }
348
348
  if (extractionMap[te.units]) {
@@ -356,10 +356,10 @@ class DatabricksDialect extends dialect_1.Dialect {
356
356
  const { op, srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
357
357
  const tz = (0, dialect_1.qtz)(qi);
358
358
  if (op === 'timestamp::date' && tz) {
359
- return `CAST(FROM_UTC_TIMESTAMP(${srcSQL}, '${tz}') AS DATE)`;
359
+ return `CAST(FROM_UTC_TIMESTAMP(${srcSQL}, ${this.sqlTimezoneLiteral(tz)}) AS DATE)`;
360
360
  }
361
361
  else if (op === 'date::timestamp' && tz) {
362
- return `TO_UTC_TIMESTAMP(CAST(${srcSQL} AS TIMESTAMP_NTZ), '${tz}')`;
362
+ return `TO_UTC_TIMESTAMP(CAST(${srcSQL} AS TIMESTAMP_NTZ), ${this.sqlTimezoneLiteral(tz)})`;
363
363
  }
364
364
  if (!malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
365
365
  if (cast.safe) {
@@ -381,7 +381,7 @@ class DatabricksDialect extends dialect_1.Dialect {
381
381
  sqlTimestampLiteral(qi, literal, timezone) {
382
382
  const tz = timezone || (0, dialect_1.qtz)(qi);
383
383
  if (tz) {
384
- return `TO_UTC_TIMESTAMP(TIMESTAMP_NTZ '${literal}', '${tz}')`;
384
+ return `TO_UTC_TIMESTAMP(TIMESTAMP_NTZ '${literal}', ${this.sqlTimezoneLiteral(tz)})`;
385
385
  }
386
386
  return `TIMESTAMP '${literal}'`;
387
387
  }
@@ -1,11 +1,20 @@
1
1
  import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralExpr, RecordLiteralNode, ArrayLiteralNode, BasicAtomicTypeDef, OrderBy, TimestampUnit, ATimestampTypeDef, TimeExpr, TemporalFieldType } from '../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from './functions';
3
3
  import type { ValidateTablePathResult } from './table-path';
4
+ /**
5
+ * One field of a struct, for the dialect methods which emit SQL over it.
6
+ *
7
+ * `typeDef` is the Malloy type, for the dialects which have to declare a SQL
8
+ * type for the field -- the ROW and STRUCT types they build. `sqlExpression`
9
+ * is SQL producing the value, valid in the scope the consumer emits into.
10
+ * `rawName` is the field's name with no quoting: a consumer needing it as an
11
+ * identifier quotes it itself, because only the consumer knows whether it
12
+ * wants an identifier or a string literal holding the name.
13
+ */
4
14
  interface DialectField {
5
15
  typeDef: AtomicTypeDef;
6
16
  sqlExpression: string;
7
17
  rawName: string;
8
- sqlOutputName: string;
9
18
  }
10
19
  export type DialectFieldList = DialectField[];
11
20
  /**
@@ -188,6 +197,7 @@ export declare abstract class Dialect {
188
197
  abstract sqlAnyValue(groupSet: number, fieldName: string): string;
189
198
  abstract sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
190
199
  sqlTurtleOrderByClause(orderBy: CompiledOrderBy[]): string;
200
+ sqlCombineLastStageOrderBy(orderBy: OrderBy[] | undefined, fieldList: DialectFieldList): string;
191
201
  abstract sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
192
202
  abstract sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
193
203
  abstract sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -457,6 +467,16 @@ export declare abstract class Dialect {
457
467
  * the civil time in the specified timezone
458
468
  */
459
469
  abstract sqlTimestamptzLiteral(qi: QueryInfo, literal: string, timezone: string): string;
470
+ /**
471
+ * Reject characters that can break SQL literals or
472
+ * be changed by SQL formatting. Timezone-name validity belongs to the
473
+ * database, whose formats may include offsets and POSIX TZ strings.
474
+ * Static so the translator can apply the same rule before dialect resolution.
475
+ */
476
+ static isSafeTimezoneName(timezone: string): boolean;
477
+ /** Return an unquoted timezone for inclusion in a larger SQL literal. */
478
+ protected checkedTimezoneName(timezone: string): string;
479
+ sqlTimezoneLiteral(timezone: string): string;
460
480
  /**
461
481
  * Render a Malloy string as a SQL string literal. The escape style is
462
482
  * driven by `stringLiteralStyle`; dialects normally do not override
@@ -490,7 +510,6 @@ export declare abstract class Dialect {
490
510
  * NULLs last, but should go away once MySQL fully implements NULLs last.
491
511
  */
492
512
  sqlOrderBy(orderTerms: string[], _orderFor?: OrderByRequest): string;
493
- sqlTzStr(qi: QueryInfo): string;
494
513
  sqlMakeUnnestKey(key: string, rowKey: string): string;
495
514
  sqlStringAggDistinct(distinctKey: string, valueSQL: string, separatorSQL: string): string;
496
515
  abstract sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
@@ -323,6 +323,20 @@ class Dialect {
323
323
  const terms = orderBy.map(o => ` ${o.field} ${o.dir.toUpperCase()}`);
324
324
  return ' ' + this.sqlOrderBy(terms, 'turtle');
325
325
  }
326
+ // Format the ordering of the last stage of a nest's pipeline into an ORDER BY
327
+ // clause for the aggregate which packs that stage's rows into an array. An
328
+ // ordinal names an output field by number, counting from one; the field list
329
+ // is a zero-based array.
330
+ sqlCombineLastStageOrderBy(orderBy, fieldList) {
331
+ if (orderBy === undefined || orderBy.length === 0) {
332
+ return '';
333
+ }
334
+ const terms = orderBy.map(o => {
335
+ const name = typeof o.field === 'string' ? o.field : fieldList[o.field - 1].rawName;
336
+ return `${this.sqlQuoteIdentifier(name)} ${o.dir || 'asc'}`;
337
+ });
338
+ return ` ORDER BY ${terms.join(', ')}`;
339
+ }
326
340
  sqlFinalStage(_lastStageName, _fields) {
327
341
  throw new Error('Dialect has no final Stage but called Anyway');
328
342
  }
@@ -482,6 +496,25 @@ class Dialect {
482
496
  }
483
497
  return sql;
484
498
  }
499
+ /**
500
+ * Reject characters that can break SQL literals or
501
+ * be changed by SQL formatting. Timezone-name validity belongs to the
502
+ * database, whose formats may include offsets and POSIX TZ strings.
503
+ * Static so the translator can apply the same rule before dialect resolution.
504
+ */
505
+ static isSafeTimezoneName(timezone) {
506
+ return !/['"`\\\r\n]/.test(timezone);
507
+ }
508
+ /** Return an unquoted timezone for inclusion in a larger SQL literal. */
509
+ checkedTimezoneName(timezone) {
510
+ if (!Dialect.isSafeTimezoneName(timezone)) {
511
+ throw new Error(`Invalid timezone ${JSON.stringify(timezone)}: quotes, backslashes, and line breaks are not allowed`);
512
+ }
513
+ return timezone;
514
+ }
515
+ sqlTimezoneLiteral(timezone) {
516
+ return `'${this.checkedTimezoneName(timezone)}'`;
517
+ }
485
518
  /**
486
519
  * Render a Malloy string as a SQL string literal. The escape style is
487
520
  * driven by `stringLiteralStyle`; dialects normally do not override
@@ -604,9 +637,6 @@ class Dialect {
604
637
  sqlOrderBy(orderTerms, _orderFor) {
605
638
  return `ORDER BY ${orderTerms.join(',')}`;
606
639
  }
607
- sqlTzStr(qi) {
608
- return `"${qi.queryTimezone}"`;
609
- }
610
640
  sqlMakeUnnestKey(key, rowKey) {
611
641
  return this.concat(key, "'x'", rowKey);
612
642
  }
@@ -94,7 +94,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
94
94
  }
95
95
  sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
96
96
  const fields = fieldList
97
- .map(f => `\n ${f.sqlOutputName}: ${f.sqlExpression}`)
97
+ .map(f => `\n ${this.sqlQuoteIdentifier(f.rawName)}: ${f.sqlExpression}`)
98
98
  .join(', ');
99
99
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
100
100
  const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
@@ -107,7 +107,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
107
107
  }
108
108
  sqlAnyValueTurtle(groupSet, fieldList) {
109
109
  const fields = fieldList
110
- .map(f => `${f.sqlOutputName}:=${f.sqlExpression}`)
110
+ .map(f => `${this.sqlQuoteIdentifier(f.rawName)}:=${f.sqlExpression}`)
111
111
  .join(', ');
112
112
  return `ANY_VALUE(CASE WHEN group_set=${groupSet} THEN STRUCT_PACK(${fields}) END)`;
113
113
  }
@@ -116,10 +116,10 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
116
116
  }
117
117
  sqlCoaleseMeasuresInline(groupSet, fieldList) {
118
118
  const fields = fieldList
119
- .map(f => `${f.sqlOutputName}: ${f.sqlExpression} `)
119
+ .map(f => `${this.sqlQuoteIdentifier(f.rawName)}: ${f.sqlExpression} `)
120
120
  .join(', ');
121
121
  const nullValues = fieldList
122
- .map(f => `${f.sqlOutputName}: NULL`)
122
+ .map(f => `${this.sqlQuoteIdentifier(f.rawName)}: NULL`)
123
123
  .join(', ');
124
124
  return `COALESCE(FIRST({${fields}}) FILTER(WHERE group_set=${groupSet}), {${nullValues}})`;
125
125
  }
@@ -181,28 +181,14 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
181
181
  return `DROP MACRO IF EXISTS ${id}; \n${hackSplitComment}\n CREATE MACRO ${id}(_param) AS (\n${(0, utils_1.indent)(funcText)}\n);\n${hackSplitComment}\n`;
182
182
  }
183
183
  sqlCreateFunctionCombineLastStage(lastStageName, dialectFieldList, orderBy) {
184
- let o = '';
185
- if (orderBy) {
186
- const clauses = [];
187
- for (const c of orderBy) {
188
- if (typeof c.field === 'string') {
189
- clauses.push(`${c.field} ${c.dir || 'asc'}`);
190
- }
191
- else {
192
- clauses.push(`${dialectFieldList[c.field].sqlOutputName} ${c.dir || 'asc'}`);
193
- }
194
- }
195
- if (clauses.length > 0) {
196
- o = ` ORDER BY ${clauses.join(', ')}`;
197
- }
198
- }
184
+ const o = this.sqlCombineLastStageOrderBy(orderBy, dialectFieldList);
199
185
  return `SELECT LIST(STRUCT_PACK(${dialectFieldList
200
- .map(d => this.sqlQuoteIdentifier(d.sqlOutputName))
186
+ .map(d => this.sqlQuoteIdentifier(d.rawName))
201
187
  .join(',')})${o}) FROM ${lastStageName}\n`;
202
188
  }
203
189
  sqlSelectAliasAsStruct(alias, dialectFieldList) {
204
190
  return `STRUCT_PACK(${dialectFieldList
205
- .map(d => `${alias}.${d.sqlOutputName}`)
191
+ .map(d => `${alias}.${this.sqlQuoteIdentifier(d.rawName)}`)
206
192
  .join(', ')})`;
207
193
  }
208
194
  // The simple way to do this is to add a comment on the table
@@ -261,7 +261,7 @@ class MySQLDialect extends dialect_1.Dialect {
261
261
  // quote in the user-controlled field name cannot close the SQL
262
262
  // string literal.
263
263
  const jsonPathLit = this.sqlLiteralString('$.' + f.rawName);
264
- fields.push(`${this.sqlQuoteIdentifier(f.sqlOutputName)} ${fType} PATH ${jsonPathLit}`);
264
+ fields.push(`${this.sqlQuoteIdentifier(f.rawName)} ${fType} PATH ${jsonPathLit}`);
265
265
  }
266
266
  return fields.join(',\n');
267
267
  }
@@ -354,12 +354,12 @@ class MySQLDialect extends dialect_1.Dialect {
354
354
  sqlConvertToCivilTime(expr, timezone, _typeDef) {
355
355
  // MySQL has no timestamptz type, so typeDef.timestamptz will never be true
356
356
  return {
357
- sql: `CONVERT_TZ(${expr}, 'UTC', '${timezone}')`,
357
+ sql: `CONVERT_TZ(${expr}, 'UTC', ${this.sqlTimezoneLiteral(timezone)})`,
358
358
  typeDef: { type: 'timestamp' },
359
359
  };
360
360
  }
361
361
  sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
362
- return `CONVERT_TZ(${expr}, '${timezone}', 'UTC')`;
362
+ return `CONVERT_TZ(${expr}, ${this.sqlTimezoneLiteral(timezone)}, 'UTC')`;
363
363
  }
364
364
  sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
365
365
  // For week truncation, adjust to Sunday first
@@ -413,7 +413,7 @@ class MySQLDialect extends dialect_1.Dialect {
413
413
  if (malloy_types_1.TD.isTimestamp(te.e.typeDef)) {
414
414
  const tz = (0, dialect_1.qtz)(qi);
415
415
  if (tz) {
416
- extractFrom = `CONVERT_TZ(${extractFrom}, 'UTC', '${tz}')`;
416
+ extractFrom = `CONVERT_TZ(${extractFrom}, 'UTC', ${this.sqlTimezoneLiteral(tz)})`;
417
417
  }
418
418
  }
419
419
  return `${msUnits}(${extractFrom})`;
@@ -423,10 +423,10 @@ class MySQLDialect extends dialect_1.Dialect {
423
423
  const { op, srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
424
424
  const tz = (0, dialect_1.qtz)(qi);
425
425
  if (op === 'timestamp::date' && tz) {
426
- return `CAST(CONVERT_TZ(${srcSQL}, 'UTC', '${tz}') AS DATE) `;
426
+ return `CAST(CONVERT_TZ(${srcSQL}, 'UTC', ${this.sqlTimezoneLiteral(tz)}) AS DATE) `;
427
427
  }
428
428
  else if (op === 'date::timestamp' && tz) {
429
- return ` CONVERT_TZ(${srcSQL}, '${tz}', 'UTC')`;
429
+ return ` CONVERT_TZ(${srcSQL}, ${this.sqlTimezoneLiteral(tz)}, 'UTC')`;
430
430
  }
431
431
  if (!malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
432
432
  if (cast.safe) {
@@ -448,7 +448,7 @@ class MySQLDialect extends dialect_1.Dialect {
448
448
  sqlTimestampLiteral(qi, literal, timezone) {
449
449
  const tz = timezone || (0, dialect_1.qtz)(qi);
450
450
  if (tz) {
451
- return `CONVERT_TZ('${literal}', '${tz}', 'UTC')`;
451
+ return `CONVERT_TZ('${literal}', ${this.sqlTimezoneLiteral(tz)}, 'UTC')`;
452
452
  }
453
453
  return `TIMESTAMP '${literal}'`;
454
454
  }
@@ -500,7 +500,7 @@ class MySQLDialect extends dialect_1.Dialect {
500
500
  }
501
501
  mapFieldsForJsonObject(fieldList, nullValues) {
502
502
  return fieldList
503
- .map(f => `${f.sqlOutputName.replace(/`/g, "'")}, ${nullValues ? 'NULL' : f.sqlExpression}\n`)
503
+ .map(f => `${this.sqlLiteralString(f.rawName)}, ${nullValues ? 'NULL' : f.sqlExpression}\n`)
504
504
  .join(', ');
505
505
  }
506
506
  castToString(expression) {
@@ -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