@malloydata/malloy 0.0.433 → 0.0.435

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 (74) hide show
  1. package/dist/api/foundation/compile.d.ts +2 -2
  2. package/dist/api/foundation/compile.js +8 -9
  3. package/dist/api/foundation/config.js +6 -15
  4. package/dist/api/foundation/runtime.d.ts +2 -8
  5. package/dist/api/foundation/runtime.js +11 -39
  6. package/dist/api/foundation/types.d.ts +1 -1
  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 +35 -3
  11. package/dist/dialect/dialect.js +67 -5
  12. package/dist/dialect/dialect_map.js +2 -0
  13. package/dist/dialect/duckdb/duckdb.js +7 -21
  14. package/dist/dialect/index.d.ts +2 -1
  15. package/dist/dialect/index.js +3 -1
  16. package/dist/dialect/mysql/mysql.js +8 -8
  17. package/dist/dialect/pg_impl.js +12 -12
  18. package/dist/dialect/postgres/postgres.js +3 -3
  19. package/dist/dialect/snowflake/snowflake.js +12 -12
  20. package/dist/dialect/sqlserver/dialect_functions.d.ts +2 -0
  21. package/dist/dialect/sqlserver/dialect_functions.js +42 -0
  22. package/dist/dialect/sqlserver/function_overrides.d.ts +2 -0
  23. package/dist/dialect/sqlserver/function_overrides.js +55 -0
  24. package/dist/dialect/sqlserver/index.d.ts +1 -0
  25. package/dist/dialect/sqlserver/index.js +22 -0
  26. package/dist/dialect/sqlserver/sqlserver.d.ts +102 -0
  27. package/dist/dialect/sqlserver/sqlserver.js +538 -0
  28. package/dist/dialect/sqlserver/windows_zones.d.ts +1 -0
  29. package/dist/dialect/sqlserver/windows_zones.js +613 -0
  30. package/dist/dialect/standardsql/standardsql.d.ts +1 -1
  31. package/dist/dialect/standardsql/standardsql.js +16 -13
  32. package/dist/dialect/trino/trino.d.ts +3 -3
  33. package/dist/dialect/trino/trino.js +32 -28
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +5 -3
  36. package/dist/lang/ast/source-elements/named-source.js +0 -2
  37. package/dist/lang/ast/source-elements/refined-source.js +1 -1
  38. package/dist/lang/ast/source-elements/sql-source.js +2 -4
  39. package/dist/lang/ast/source-elements/table-source.js +1 -2
  40. package/dist/lang/ast/source-properties/timezone-statement.d.ts +4 -3
  41. package/dist/lang/ast/source-properties/timezone-statement.js +14 -12
  42. package/dist/lang/ast/statements/import-statement.js +4 -0
  43. package/dist/lang/ast/types/malloy-element.d.ts +0 -7
  44. package/dist/lang/ast/types/malloy-element.js +0 -18
  45. package/dist/lang/composite-source-utils.js +10 -3
  46. package/dist/lang/malloy-to-ast.js +2 -8
  47. package/dist/lang/parse-malloy.d.ts +20 -4
  48. package/dist/lang/parse-malloy.js +50 -15
  49. package/dist/lang/parse-tree-walkers/find-external-references.d.ts +3 -3
  50. package/dist/lang/parse-tree-walkers/find-external-references.js +17 -16
  51. package/dist/lang/test/test-translator.d.ts +12 -2
  52. package/dist/lang/test/test-translator.js +16 -2
  53. package/dist/model/expression_compiler.js +1 -1
  54. package/dist/model/join_instance.d.ts +1 -0
  55. package/dist/model/join_instance.js +9 -1
  56. package/dist/model/query_query.d.ts +28 -1
  57. package/dist/model/query_query.js +299 -156
  58. package/dist/model/source_def_utils.d.ts +7 -1
  59. package/dist/model/source_def_utils.js +20 -0
  60. package/dist/model/sql_block.d.ts +2 -2
  61. package/dist/model/sql_block.js +11 -47
  62. package/dist/model/sql_compiled.d.ts +3 -1
  63. package/dist/model/sql_compiled.js +8 -6
  64. package/dist/model/stage_writer.js +3 -3
  65. package/dist/model/utils.d.ts +2 -2
  66. package/dist/model/utils.js +2 -3
  67. package/dist/test/index.js +16 -8
  68. package/dist/test/matchers.d.ts +1 -1
  69. package/dist/test/matchers.js +1 -1
  70. package/dist/test/test-models.js +6 -13
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/femto-config.motly +15 -1
  74. package/package.json +7 -5
@@ -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';
@@ -35,7 +35,7 @@ export interface MalloyCompileOptions {
35
35
  noThrowOnError?: boolean;
36
36
  eventStream?: EventStream;
37
37
  importBaseURL?: URL;
38
- testEnvironment?: boolean;
38
+ virtualMap?: VirtualMap;
39
39
  }
40
40
  export interface MalloyRunOptions {
41
41
  connection?: Connection;
@@ -88,9 +88,6 @@ class Malloy {
88
88
  const translator = new lang_1.MalloyTranslator(url.toString(), importBaseURL.toString(), {
89
89
  urls: { [url.toString()]: source },
90
90
  }, eventStream, (_a = options === null || options === void 0 ? void 0 : options.restrictedMode) !== null && _a !== void 0 ? _a : false);
91
- if (options === null || options === void 0 ? void 0 : options.testEnvironment) {
92
- translator.allDialectsEnabled = true;
93
- }
94
91
  return new document_1.Parse(translator, invalidationKey);
95
92
  }
96
93
  static parse({ url, urlReader, source, eventStream, options, }) {
@@ -119,9 +116,10 @@ class Malloy {
119
116
  * @return A (promise of a) compiled `Model`.
120
117
  */
121
118
  static async compile(req) {
122
- var _a, _b, _c, _d, _e;
119
+ var _a, _b, _c, _d, _e, _f;
123
120
  let { url, source, importBaseURL, cacheManager } = req;
124
121
  const { parse, urlReader, connections, model, refreshSchemaCache, noThrowOnError, eventStream, restrictedMode, method, } = req;
122
+ const virtualMap = (_a = req.virtualMap) !== null && _a !== void 0 ? _a : parse === null || parse === void 0 ? void 0 : parse._translator.virtualMap;
125
123
  if (restrictedMode) {
126
124
  // Restricted-mode compiles do not participate in the model-def
127
125
  // cache. The cache key is the URL, but restricted vs. unrestricted
@@ -172,7 +170,8 @@ class Malloy {
172
170
  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
171
  }
174
172
  translator = parse._translator;
175
- const invalidationKey = (_a = parse._invalidationKey) !== null && _a !== void 0 ? _a : (await (0, readers_1.getInvalidationKey)(urlReader, url));
173
+ translator.virtualMap = virtualMap;
174
+ const invalidationKey = (_b = parse._invalidationKey) !== null && _b !== void 0 ? _b : (await (0, readers_1.getInvalidationKey)(urlReader, url));
176
175
  invalidationKeys[_url] = invalidationKey;
177
176
  }
178
177
  else {
@@ -187,7 +186,7 @@ class Malloy {
187
186
  }
188
187
  translator = new lang_1.MalloyTranslator(_url, importBaseURL.toString(), {
189
188
  urls: { [_url]: source },
190
- }, eventStream, restrictedMode !== null && restrictedMode !== void 0 ? restrictedMode : false);
189
+ }, eventStream, restrictedMode !== null && restrictedMode !== void 0 ? restrictedMode : false, virtualMap);
191
190
  }
192
191
  for (;;) {
193
192
  const result = translator.translate(model === null || model === void 0 ? void 0 : model._modelDef);
@@ -208,14 +207,14 @@ class Malloy {
208
207
  const existingQueryModel = !result.modelWasModified && model
209
208
  ? model.getExistingQueryModel()
210
209
  : 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);
210
+ 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
211
  }
213
212
  else if (noThrowOnError) {
214
213
  const emptyModel = (0, model_1.mkModelDef)('modelDidNotCompile');
215
214
  const modelFromCompile = (model === null || model === void 0 ? void 0 : model._modelDef) || emptyModel;
216
215
  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 : []),
216
+ ...((_e = model === null || model === void 0 ? void 0 : model.fromSources) !== null && _e !== void 0 ? _e : []),
217
+ ...((_f = result.fromSources) !== null && _f !== void 0 ? _f : []),
219
218
  ]);
220
219
  }
221
220
  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);
@@ -42,7 +42,6 @@ declare class FluentState<T> {
42
42
  * An environment for compiling and running Malloy queries.
43
43
  */
44
44
  export declare class Runtime {
45
- isTestRuntime: boolean;
46
45
  private _urlReader;
47
46
  private _connections;
48
47
  private _eventStream;
@@ -172,13 +171,8 @@ export declare class Runtime {
172
171
  /** @internal */
173
172
  _invalidateGivensCache(): void;
174
173
  /**
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.
174
+ * Default virtual-source bindings for translation and query SQL generation.
175
+ * Falls back to config.virtualMap when no runtime map is assigned.
182
176
  */
183
177
  get virtualMap(): VirtualMap | undefined;
184
178
  set virtualMap(map: VirtualMap | undefined);
@@ -51,7 +51,6 @@ class FluentState {
51
51
  class Runtime {
52
52
  constructor({ urlReader, connections, connection, config, buildManifest, eventStream, cacheManager, givens, }) {
53
53
  var _a, _b;
54
- this.isTestRuntime = false;
55
54
  if (config !== undefined) {
56
55
  this._config = config;
57
56
  connections = connections !== null && connections !== void 0 ? connections : config.connections;
@@ -302,13 +301,8 @@ class Runtime {
302
301
  this._resolvedGivensPromise = undefined;
303
302
  }
304
303
  /**
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.
304
+ * Default virtual-source bindings for translation and query SQL generation.
305
+ * Falls back to config.virtualMap when no runtime map is assigned.
312
306
  */
313
307
  get virtualMap() {
314
308
  var _a, _b;
@@ -355,16 +349,9 @@ class Runtime {
355
349
  */
356
350
  loadModel(source, options) {
357
351
  const { refreshSchemaCache, noThrowOnError } = options || {};
358
- if (this.isTestRuntime) {
359
- if (options === undefined) {
360
- options = { testEnvironment: true };
361
- }
362
- else {
363
- options = { ...options, testEnvironment: true };
364
- }
365
- }
366
352
  const compilable = source instanceof URL ? { url: source } : { source };
367
353
  return new ModelMaterializer(this, async () => {
354
+ var _a;
368
355
  const m = await compile_1.Malloy.compile({
369
356
  ...compilable,
370
357
  urlReader: this.urlReader,
@@ -373,8 +360,8 @@ class Runtime {
373
360
  noThrowOnError,
374
361
  eventStream: this.eventStream,
375
362
  importBaseURL: options === null || options === void 0 ? void 0 : options.importBaseURL,
376
- testEnvironment: options === null || options === void 0 ? void 0 : options.testEnvironment,
377
363
  cacheManager: this.cacheManager,
364
+ virtualMap: (_a = options === null || options === void 0 ? void 0 : options.virtualMap) !== null && _a !== void 0 ? _a : this.virtualMap,
378
365
  });
379
366
  return this._withRuntimeContext(m);
380
367
  }, options);
@@ -634,12 +621,13 @@ class ModelMaterializer extends FluentState {
634
621
  // `restrictedMode` and `method` are named to hold them back: this entry
635
622
  // point has never honored them, and `loadRestrictedQuery` is how a caller
636
623
  // asks for restricted compilation.
637
- const { importBaseURL, testEnvironment, refreshSchemaCache, noThrowOnError, restrictedMode: _restrictedMode, method: _method, ...callQueryOptions } = options !== null && options !== void 0 ? options : {};
624
+ const { importBaseURL, refreshSchemaCache, noThrowOnError, restrictedMode: _restrictedMode, method: _method, ...callQueryOptions } = options !== null && options !== void 0 ? options : {};
638
625
  const compileQueryOptions = {
639
626
  ...this.compileQueryOptions,
640
627
  ...callQueryOptions,
641
628
  };
642
629
  return this.makeQueryMaterializer(async () => {
630
+ var _a;
643
631
  const compilable = query instanceof URL ? { url: query } : { source: query };
644
632
  const queryModel = await compile_1.Malloy.compile({
645
633
  ...compilable,
@@ -649,8 +637,8 @@ class ModelMaterializer extends FluentState {
649
637
  refreshSchemaCache,
650
638
  noThrowOnError,
651
639
  importBaseURL,
652
- testEnvironment: testEnvironment || this.runtime.isTestRuntime,
653
640
  ...compileQueryOptions,
641
+ virtualMap: (_a = compileQueryOptions.virtualMap) !== null && _a !== void 0 ? _a : this.runtime.virtualMap,
654
642
  });
655
643
  return queryModel.preparedQuery;
656
644
  }, compileQueryOptions);
@@ -702,7 +690,6 @@ class ModelMaterializer extends FluentState {
702
690
  return this.makeQueryMaterializer(async () => {
703
691
  const urlReader = this.runtime.urlReader;
704
692
  const connections = this.runtime.connections;
705
- const testEnvironment = this.runtime.isTestRuntime ? true : undefined;
706
693
  const model = await this.getModel();
707
694
  const queryModel = await compile_1.Malloy.compile({
708
695
  source: text,
@@ -711,7 +698,6 @@ class ModelMaterializer extends FluentState {
711
698
  urlReader,
712
699
  connections,
713
700
  model,
714
- testEnvironment,
715
701
  ...this.compileQueryOptions,
716
702
  });
717
703
  return queryModel.preparedQuery;
@@ -725,14 +711,8 @@ class ModelMaterializer extends FluentState {
725
711
  * or loading further related objects.
726
712
  */
727
713
  extendModel(query, options) {
728
- if (this.runtime.isTestRuntime) {
729
- if (options === undefined) {
730
- options = { testEnvironment: true };
731
- }
732
- else {
733
- options = { ...options, testEnvironment: true };
734
- }
735
- }
714
+ var _a, _b;
715
+ 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;
736
716
  return new ModelMaterializer(this.runtime, async () => {
737
717
  const urlReader = this.runtime.urlReader;
738
718
  const connections = this.runtime.connections;
@@ -747,11 +727,11 @@ class ModelMaterializer extends FluentState {
747
727
  refreshSchemaCache: options === null || options === void 0 ? void 0 : options.refreshSchemaCache,
748
728
  noThrowOnError: options === null || options === void 0 ? void 0 : options.noThrowOnError,
749
729
  importBaseURL: options === null || options === void 0 ? void 0 : options.importBaseURL,
750
- testEnvironment: options === null || options === void 0 ? void 0 : options.testEnvironment,
751
730
  ...this.compileQueryOptions,
731
+ virtualMap: virtualMap !== null && virtualMap !== void 0 ? virtualMap : this.runtime.virtualMap,
752
732
  });
753
733
  return this.runtime._withRuntimeContext(queryModel);
754
- }, options);
734
+ }, { ...options, virtualMap });
755
735
  }
756
736
  async search(sourceName, searchTerm, limit = 1000, searchField = undefined) {
757
737
  const model = await this.materialize();
@@ -1064,15 +1044,7 @@ class QueryMaterializer extends FluentState {
1064
1044
  connectionDigests[connName] = conn.getDigest();
1065
1045
  }
1066
1046
  }
1067
- // Use virtualMap from options if provided, otherwise fall back to Runtime's.
1068
1047
  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
1048
  // Per-query supply for a finalized given is rejected at API entry
1077
1049
  // — the finalized-givens set is the runtime's "this can't be
1078
1050
  // overridden by the caller" guarantee. Fail before any IO so misuse
@@ -20,7 +20,6 @@ export interface Loggable {
20
20
  }
21
21
  export interface ParseOptions {
22
22
  importBaseURL?: URL;
23
- testEnvironment?: boolean;
24
23
  /** Reject language constructs that reach outside the trusted model. */
25
24
  restrictedMode?: boolean;
26
25
  /**
@@ -43,6 +42,7 @@ export type CompileMethod = 'loadModel' | 'extendModel' | 'query';
43
42
  export interface CompileOptions {
44
43
  refreshSchemaCache?: boolean | number;
45
44
  noThrowOnError?: boolean;
45
+ virtualMap?: VirtualMap;
46
46
  }
47
47
  /** Options given to the Malloy compiler (QueryModel) */
48
48
  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
  /**
@@ -74,6 +83,7 @@ export declare function qtz(qi: QueryInfo): string | undefined;
74
83
  */
75
84
  export declare function turtleGroupSetCondition(groupSet: number | undefined, filterSQL: string | undefined): string | undefined;
76
85
  export type OrderByClauseType = 'output_name' | 'ordinal' | 'expression';
86
+ export type GroupByClauseType = 'ordinal' | 'expression';
77
87
  export type OrderByRequest = 'query' | 'turtle' | 'analytical';
78
88
  export type BooleanTypeSupport = 'supported' | 'simulated' | 'none';
79
89
  /**
@@ -106,6 +116,14 @@ export declare abstract class Dialect {
106
116
  abstract supportsSafeCast: boolean;
107
117
  abstract supportsNesting: boolean;
108
118
  abstract experimental: boolean;
119
+ /**
120
+ * What the compiler consults, not `experimental` itself. The test harness
121
+ * calls `acceptExperimental()` on the dialect under test so the shared
122
+ * suites can run on an experimental dialect without every test enabling it.
123
+ */
124
+ private experimentalAccepted;
125
+ isExperimental(): boolean;
126
+ acceptExperimental(): void;
109
127
  cantPartitionWindowFunctionsOnExpressions: boolean;
110
128
  sqlLateralJoinBag(_expressions: LateralJoinExpression[]): string;
111
129
  supportsPipelinesInViews: boolean;
@@ -114,6 +132,9 @@ export declare abstract class Dialect {
114
132
  hasTimestamptz: boolean;
115
133
  readsNestedData: boolean;
116
134
  orderByClause: OrderByClauseType;
135
+ groupByClause: GroupByClauseType;
136
+ /** The ordering and row limit which end a SELECT stage */
137
+ sqlOrderByLimit(orderTerms: string[], limit: number | undefined): string;
117
138
  nullMatchesFunctionSignature: boolean;
118
139
  supportsSelectReplace: boolean;
119
140
  supportsBigIntPrecision: boolean;
@@ -131,6 +152,7 @@ export declare abstract class Dialect {
131
152
  compoundObjectInSchema: boolean;
132
153
  booleanType: BooleanTypeSupport;
133
154
  likeEscape: boolean;
155
+ likeExtraWildcards: string[];
134
156
  /**
135
157
  * Mappings from integer value ranges to Malloy number types.
136
158
  *
@@ -188,6 +210,7 @@ export declare abstract class Dialect {
188
210
  abstract sqlAnyValue(groupSet: number, fieldName: string): string;
189
211
  abstract sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
190
212
  sqlTurtleOrderByClause(orderBy: CompiledOrderBy[]): string;
213
+ sqlCombineLastStageOrderBy(orderBy: OrderBy[] | undefined, fieldList: DialectFieldList): string;
191
214
  abstract sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
192
215
  abstract sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
193
216
  abstract sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -457,6 +480,16 @@ export declare abstract class Dialect {
457
480
  * the civil time in the specified timezone
458
481
  */
459
482
  abstract sqlTimestamptzLiteral(qi: QueryInfo, literal: string, timezone: string): string;
483
+ /**
484
+ * Reject characters that can break SQL literals or
485
+ * be changed by SQL formatting. Timezone-name validity belongs to the
486
+ * database, whose formats may include offsets and POSIX TZ strings.
487
+ * Static so the translator can apply the same rule before dialect resolution.
488
+ */
489
+ static isSafeTimezoneName(timezone: string): boolean;
490
+ /** Return an unquoted timezone for inclusion in a larger SQL literal. */
491
+ protected checkedTimezoneName(timezone: string): string;
492
+ sqlTimezoneLiteral(timezone: string): string;
460
493
  /**
461
494
  * Render a Malloy string as a SQL string literal. The escape style is
462
495
  * driven by `stringLiteralStyle`; dialects normally do not override
@@ -483,14 +516,13 @@ export declare abstract class Dialect {
483
516
  exprToSQL(qi: QueryInfo, df: Expr): string | undefined;
484
517
  sqlSumDistinct(_key: string, _value: string, _funcName: string): string;
485
518
  sqlAggDistinct(_key: string, _values: string[], _func: (valNames: string[]) => string): string;
486
- sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
519
+ sqlSampleTable(tableSQL: string, sample: Sampling | undefined, _onBaseTable?: boolean): string;
487
520
  /**
488
521
  * MySQL is NULLs first, all other dialects have a way to make NULLs last.
489
522
  * isBaseOrdering is a hack to allow the MySQL dialect to partially implement
490
523
  * NULLs last, but should go away once MySQL fully implements NULLs last.
491
524
  */
492
525
  sqlOrderBy(orderTerms: string[], _orderFor?: OrderByRequest): string;
493
- sqlTzStr(qi: QueryInfo): string;
494
526
  sqlMakeUnnestKey(key: string, rowKey: string): string;
495
527
  sqlStringAggDistinct(distinctKey: string, valueSQL: string, separatorSQL: string): string;
496
528
  abstract sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
@@ -81,6 +81,12 @@ function turtleGroupSetCondition(groupSet, filterSQL) {
81
81
  }
82
82
  class Dialect {
83
83
  constructor() {
84
+ /**
85
+ * What the compiler consults, not `experimental` itself. The test harness
86
+ * calls `acceptExperimental()` on the dialect under test so the shared
87
+ * suites can run on an experimental dialect without every test enabling it.
88
+ */
89
+ this.experimentalAccepted = false;
84
90
  // -- we should add flags with default values from now on so as to not break
85
91
  // dialects outside our repository
86
92
  //
@@ -106,6 +112,9 @@ class Dialect {
106
112
  this.readsNestedData = true;
107
113
  // ORDER BY 1 DESC
108
114
  this.orderByClause = 'ordinal';
115
+ // GROUP BY 1, or GROUP BY the dimension expressions for an engine whose
116
+ // GROUP BY takes no ordinals
117
+ this.groupByClause = 'ordinal';
109
118
  // null will match in a function signature
110
119
  this.nullMatchesFunctionSignature = true;
111
120
  // support select * replace(...)
@@ -147,6 +156,8 @@ class Dialect {
147
156
  this.booleanType = 'supported';
148
157
  // Like characters are escaped with ESCAPE clause
149
158
  this.likeEscape = true;
159
+ // Characters LIKE reads as wildcards beyond % and _, escaped like them
160
+ this.likeExtraWildcards = [];
150
161
  /**
151
162
  * Mappings from integer value ranges to Malloy number types.
152
163
  *
@@ -209,6 +220,12 @@ class Dialect {
209
220
  */
210
221
  this.identifierEscapeStyle = exports.EscapeStyle.Unset;
211
222
  }
223
+ isExperimental() {
224
+ return this.experimental && !this.experimentalAccepted;
225
+ }
226
+ acceptExperimental() {
227
+ this.experimentalAccepted = true;
228
+ }
212
229
  // Generate the lateral join bag clause for window function partitioning.
213
230
  // The expressions are dimension fields that need to be referenced by name
214
231
  // in PARTITION BY clauses. Must be overridden by any dialect that sets
@@ -219,6 +236,14 @@ class Dialect {
219
236
  }
220
237
  throw new Error('Internal error: sqlLateralJoinBag called but cantPartitionWindowFunctionsOnExpressions is false');
221
238
  }
239
+ /** The ordering and row limit which end a SELECT stage */
240
+ sqlOrderByLimit(orderTerms, limit) {
241
+ let s = orderTerms.length > 0 ? this.sqlOrderBy(orderTerms, 'query') + '\n' : '';
242
+ if (limit !== undefined) {
243
+ s += `LIMIT ${limit}\n`;
244
+ }
245
+ return s;
246
+ }
222
247
  /**
223
248
  * Determine the Malloy number type for a numeric literal.
224
249
  */
@@ -323,6 +348,20 @@ class Dialect {
323
348
  const terms = orderBy.map(o => ` ${o.field} ${o.dir.toUpperCase()}`);
324
349
  return ' ' + this.sqlOrderBy(terms, 'turtle');
325
350
  }
351
+ // Format the ordering of the last stage of a nest's pipeline into an ORDER BY
352
+ // clause for the aggregate which packs that stage's rows into an array. An
353
+ // ordinal names an output field by number, counting from one; the field list
354
+ // is a zero-based array.
355
+ sqlCombineLastStageOrderBy(orderBy, fieldList) {
356
+ if (orderBy === undefined || orderBy.length === 0) {
357
+ return '';
358
+ }
359
+ const terms = orderBy.map(o => {
360
+ const name = typeof o.field === 'string' ? o.field : fieldList[o.field - 1].rawName;
361
+ return `${this.sqlQuoteIdentifier(name)} ${o.dir || 'asc'}`;
362
+ });
363
+ return ` ORDER BY ${terms.join(', ')}`;
364
+ }
326
365
  sqlFinalStage(_lastStageName, _fields) {
327
366
  throw new Error('Dialect has no final Stage but called Anyway');
328
367
  }
@@ -482,6 +521,25 @@ class Dialect {
482
521
  }
483
522
  return sql;
484
523
  }
524
+ /**
525
+ * Reject characters that can break SQL literals or
526
+ * be changed by SQL formatting. Timezone-name validity belongs to the
527
+ * database, whose formats may include offsets and POSIX TZ strings.
528
+ * Static so the translator can apply the same rule before dialect resolution.
529
+ */
530
+ static isSafeTimezoneName(timezone) {
531
+ return !/['"`\\\r\n]/.test(timezone);
532
+ }
533
+ /** Return an unquoted timezone for inclusion in a larger SQL literal. */
534
+ checkedTimezoneName(timezone) {
535
+ if (!Dialect.isSafeTimezoneName(timezone)) {
536
+ throw new Error(`Invalid timezone ${JSON.stringify(timezone)}: quotes, backslashes, and line breaks are not allowed`);
537
+ }
538
+ return timezone;
539
+ }
540
+ sqlTimezoneLiteral(timezone) {
541
+ return `'${this.checkedTimezoneName(timezone)}'`;
542
+ }
485
543
  /**
486
544
  * Render a Malloy string as a SQL string literal. The escape style is
487
545
  * driven by `stringLiteralStyle`; dialects normally do not override
@@ -590,7 +648,9 @@ class Dialect {
590
648
  _func) {
591
649
  return 'sqlAggDistinct called but not implemented';
592
650
  }
593
- sqlSampleTable(tableSQL, sample) {
651
+ // onBaseTable: the source is a table the engine can sample in place; a
652
+ // dialect whose sampling reads the table itself ignores a sample elsewhere
653
+ sqlSampleTable(tableSQL, sample, _onBaseTable = true) {
594
654
  if (sample !== undefined) {
595
655
  throw new Error(`Sampling is not supported on dialect ${this.name}.`);
596
656
  }
@@ -604,9 +664,6 @@ class Dialect {
604
664
  sqlOrderBy(orderTerms, _orderFor) {
605
665
  return `ORDER BY ${orderTerms.join(',')}`;
606
666
  }
607
- sqlTzStr(qi) {
608
- return `"${qi.queryTimezone}"`;
609
- }
610
667
  sqlMakeUnnestKey(key, rowKey) {
611
668
  return this.concat(key, "'x'", rowKey);
612
669
  }
@@ -664,6 +721,11 @@ class Dialect {
664
721
  escapeActive = false;
665
722
  escapeClause = true;
666
723
  }
724
+ else if (this.likeEscape && this.likeExtraWildcards.includes(c)) {
725
+ escaped += '^' + c;
726
+ escapeActive = false;
727
+ escapeClause = true;
728
+ }
667
729
  else {
668
730
  if (escapeActive) {
669
731
  if (this.likeEscape) {
@@ -690,7 +752,7 @@ class Dialect {
690
752
  */
691
753
  sqlBoolean(bv) {
692
754
  if (this.booleanType === 'none') {
693
- return bv ? '(1=1)' : '(1-0)';
755
+ return bv ? '(1=1)' : '(1=0)';
694
756
  }
695
757
  return bv ? 'true' : 'false';
696
758
  }
@@ -14,6 +14,7 @@ const standardsql_1 = require("./standardsql");
14
14
  const trino_1 = require("./trino");
15
15
  const mysql_1 = require("./mysql");
16
16
  const databricks_1 = require("./databricks");
17
+ const sqlserver_1 = require("./sqlserver");
17
18
  const dialectMap = new Map();
18
19
  function getDialect(name) {
19
20
  const d = dialectMap.get(name);
@@ -36,4 +37,5 @@ registerDialect(new trino_1.TrinoDialect());
36
37
  registerDialect(new trino_1.PrestoDialect());
37
38
  registerDialect(new mysql_1.MySQLDialect());
38
39
  registerDialect(new databricks_1.DatabricksDialect());
40
+ registerDialect(new sqlserver_1.SQLServerDialect());
39
41
  //# sourceMappingURL=dialect_map.js.map