@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
@@ -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
@@ -22,38 +22,65 @@ const inSeconds = {
22
22
  day: 24 * 3600,
23
23
  week: 7 * 24 * 3600,
24
24
  };
25
+ /**
26
+ * MySQL reports a column type as
27
+ *
28
+ * base [ '(' params ')' ] [ 'unsigned' ] [ 'zerofill' ]
29
+ *
30
+ * and normalizes every alias away before reporting, so a schema read only
31
+ * ever produces canonical base names. A raw cast reaches this map without
32
+ * that normalization, and any alias it spells -- REAL, INTEGER, NUMERIC --
33
+ * falls through to `sql native`.
34
+ *
35
+ * No modifier changes the Malloy type: `unsigned` only widens the range, and
36
+ * the sole type whose range crosses a Malloy boundary is bigint, which is
37
+ * already `bigint`; `zerofill` is presentation. So the map is keyed on the
38
+ * base name alone. DECIMAL is not in the map because its Malloy type depends
39
+ * on its parameters.
40
+ */
25
41
  const mysqlToMalloyTypes = {
26
- // TODO: This assumes tinyint is always going to be a boolean.
27
- 'tinyint': { type: 'boolean' },
42
+ 'tinyint': { type: 'number', numberType: 'integer' },
28
43
  'smallint': { type: 'number', numberType: 'integer' },
29
44
  'mediumint': { type: 'number', numberType: 'integer' },
30
45
  'int': { type: 'number', numberType: 'integer' },
31
46
  'bigint': { type: 'number', numberType: 'bigint' },
32
- 'tinyint unsigned': { type: 'number', numberType: 'integer' },
33
- 'smallint unsigned': { type: 'number', numberType: 'integer' },
34
- 'mediumint unsigned': { type: 'number', numberType: 'integer' },
35
- 'int unsigned': { type: 'number', numberType: 'integer' },
36
- 'bigint unsigned': { type: 'number', numberType: 'bigint' },
47
+ 'float': { type: 'number', numberType: 'float' },
37
48
  'double': { type: 'number', numberType: 'float' },
38
- 'varchar': { type: 'string' },
39
- 'varbinary': { type: 'string' },
40
49
  'char': { type: 'string' },
50
+ 'varchar': { type: 'string' },
51
+ 'tinytext': { type: 'string' },
41
52
  'text': { type: 'string' },
53
+ 'mediumtext': { type: 'string' },
54
+ 'longtext': { type: 'string' },
42
55
  'date': { type: 'date' },
43
56
  'datetime': { type: 'timestamp' },
44
57
  'timestamp': { type: 'timestamp' },
45
58
  'time': { type: 'string' },
46
- 'decimal': { type: 'number', numberType: 'float' },
47
- // TODO: Check if we need special handling for boolean.
48
- 'tinyint(1)': { type: 'boolean' },
49
59
  };
60
+ /**
61
+ * Split a reported type into its base name and numeric parameters.
62
+ *
63
+ * Only numeric parameter lists are read. ENUM and SET carry quoted value
64
+ * lists whose quoting rules a regex should not chase, and nothing needs
65
+ * their values -- both map to `sql native`.
66
+ */
67
+ function parseMySQLType(sqlType) {
68
+ var _a, _b;
69
+ const text = sqlType.trim().toLowerCase();
70
+ const base = (_b = (_a = text.match(/^\w+/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : text;
71
+ const params = text.match(/^\w+\s*\((\d+(?:\s*,\s*\d+)*)\)/);
72
+ return {
73
+ base,
74
+ params: params ? params[1].split(',').map(p => parseInt(p, 10)) : [],
75
+ };
76
+ }
50
77
  function malloyTypeToJSONTableType(malloyType) {
51
78
  switch (malloyType.type) {
52
79
  case 'number':
53
- if (malloyType.numberType === 'integer') {
54
- return 'INT';
55
- }
56
- else if (malloyType.numberType === 'bigint') {
80
+ // BIGINT for both: JSON_TABLE turns an out-of-range value into NULL
81
+ // without warning, and `integer` is not bounded at 32 bits.
82
+ if (malloyType.numberType === 'integer' ||
83
+ malloyType.numberType === 'bigint') {
57
84
  return 'BIGINT';
58
85
  }
59
86
  else {
@@ -62,7 +89,8 @@ function malloyTypeToJSONTableType(malloyType) {
62
89
  case 'string':
63
90
  return 'CHAR(255)'; // JSON_TABLE needs a length
64
91
  case 'boolean':
65
- return 'INT'; // or TINYINT(1) if you prefer
92
+ // MySQL has no boolean; TINYINT(1) is only a spelling of TINYINT.
93
+ return 'INT';
66
94
  case 'record':
67
95
  case 'array':
68
96
  return 'JSON';
@@ -141,11 +169,24 @@ class MySQLDialect extends dialect_1.Dialect {
141
169
  }
142
170
  sqlTypeToMalloyType(sqlType) {
143
171
  var _a, _b;
144
- // Remove trailing params
145
- const baseSqlType = (_b = (_a = sqlType.match(/^(\w+)/)) === null || _a === void 0 ? void 0 : _a.at(0)) !== null && _b !== void 0 ? _b : sqlType;
146
- return (mysqlToMalloyTypes[baseSqlType.toLowerCase()] || {
172
+ const { base, params } = parseMySQLType(sqlType);
173
+ if (base === 'decimal') {
174
+ // DECIMAL is exact, so scale 0 is an integer rather than a float. Above
175
+ // 15 digits it no longer survives a JS double and must be carried as a
176
+ // bigint. MySQL reports precision and scale on every decimal; the
177
+ // defaults match its own when a bare DECIMAL is declared.
178
+ const [precision, scale] = [(_a = params[0]) !== null && _a !== void 0 ? _a : 10, (_b = params[1]) !== null && _b !== void 0 ? _b : 0];
179
+ if (scale > 0) {
180
+ return { type: 'number', numberType: 'float' };
181
+ }
182
+ return {
183
+ type: 'number',
184
+ numberType: precision <= 15 ? 'integer' : 'bigint',
185
+ };
186
+ }
187
+ return (mysqlToMalloyTypes[base] || {
147
188
  type: 'sql native',
148
- rawType: baseSqlType,
189
+ rawType: base,
149
190
  });
150
191
  }
151
192
  sqlGroupSetTable(groupSetCount) {
@@ -220,7 +261,7 @@ class MySQLDialect extends dialect_1.Dialect {
220
261
  // quote in the user-controlled field name cannot close the SQL
221
262
  // string literal.
222
263
  const jsonPathLit = this.sqlLiteralString('$.' + f.rawName);
223
- fields.push(`${this.sqlQuoteIdentifier(f.sqlOutputName)} ${fType} PATH ${jsonPathLit}`);
264
+ fields.push(`${this.sqlQuoteIdentifier(f.rawName)} ${fType} PATH ${jsonPathLit}`);
224
265
  }
225
266
  return fields.join(',\n');
226
267
  }
@@ -313,12 +354,12 @@ class MySQLDialect extends dialect_1.Dialect {
313
354
  sqlConvertToCivilTime(expr, timezone, _typeDef) {
314
355
  // MySQL has no timestamptz type, so typeDef.timestamptz will never be true
315
356
  return {
316
- sql: `CONVERT_TZ(${expr}, 'UTC', '${timezone}')`,
357
+ sql: `CONVERT_TZ(${expr}, 'UTC', ${this.sqlTimezoneLiteral(timezone)})`,
317
358
  typeDef: { type: 'timestamp' },
318
359
  };
319
360
  }
320
361
  sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
321
- return `CONVERT_TZ(${expr}, '${timezone}', 'UTC')`;
362
+ return `CONVERT_TZ(${expr}, ${this.sqlTimezoneLiteral(timezone)}, 'UTC')`;
322
363
  }
323
364
  sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
324
365
  // For week truncation, adjust to Sunday first
@@ -372,7 +413,7 @@ class MySQLDialect extends dialect_1.Dialect {
372
413
  if (malloy_types_1.TD.isTimestamp(te.e.typeDef)) {
373
414
  const tz = (0, dialect_1.qtz)(qi);
374
415
  if (tz) {
375
- extractFrom = `CONVERT_TZ(${extractFrom}, 'UTC', '${tz}')`;
416
+ extractFrom = `CONVERT_TZ(${extractFrom}, 'UTC', ${this.sqlTimezoneLiteral(tz)})`;
376
417
  }
377
418
  }
378
419
  return `${msUnits}(${extractFrom})`;
@@ -382,10 +423,10 @@ class MySQLDialect extends dialect_1.Dialect {
382
423
  const { op, srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
383
424
  const tz = (0, dialect_1.qtz)(qi);
384
425
  if (op === 'timestamp::date' && tz) {
385
- return `CAST(CONVERT_TZ(${srcSQL}, 'UTC', '${tz}') AS DATE) `;
426
+ return `CAST(CONVERT_TZ(${srcSQL}, 'UTC', ${this.sqlTimezoneLiteral(tz)}) AS DATE) `;
386
427
  }
387
428
  else if (op === 'date::timestamp' && tz) {
388
- return ` CONVERT_TZ(${srcSQL}, '${tz}', 'UTC')`;
429
+ return ` CONVERT_TZ(${srcSQL}, ${this.sqlTimezoneLiteral(tz)}, 'UTC')`;
389
430
  }
390
431
  if (!malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
391
432
  if (cast.safe) {
@@ -407,7 +448,7 @@ class MySQLDialect extends dialect_1.Dialect {
407
448
  sqlTimestampLiteral(qi, literal, timezone) {
408
449
  const tz = timezone || (0, dialect_1.qtz)(qi);
409
450
  if (tz) {
410
- return `CONVERT_TZ('${literal}', '${tz}', 'UTC')`;
451
+ return `CONVERT_TZ('${literal}', ${this.sqlTimezoneLiteral(tz)}, 'UTC')`;
411
452
  }
412
453
  return `TIMESTAMP '${literal}'`;
413
454
  }
@@ -459,7 +500,7 @@ class MySQLDialect extends dialect_1.Dialect {
459
500
  }
460
501
  mapFieldsForJsonObject(fieldList, nullValues) {
461
502
  return fieldList
462
- .map(f => `${f.sqlOutputName.replace(/`/g, "'")}, ${nullValues ? 'NULL' : f.sqlExpression}\n`)
503
+ .map(f => `${this.sqlLiteralString(f.rawName)}, ${nullValues ? 'NULL' : f.sqlExpression}\n`)
463
504
  .join(', ');
464
505
  }
465
506
  castToString(expression) {