@malloydata/malloy 0.0.394 → 0.0.396

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 (84) hide show
  1. package/dist/api/foundation/compile.d.ts +7 -6
  2. package/dist/api/foundation/compile.js +22 -6
  3. package/dist/api/foundation/config.d.ts +2 -3
  4. package/dist/api/foundation/config.js +23 -11
  5. package/dist/api/foundation/core.js +1 -1
  6. package/dist/api/foundation/runtime.d.ts +85 -5
  7. package/dist/api/foundation/runtime.js +204 -14
  8. package/dist/api/foundation/types.d.ts +2 -0
  9. package/dist/api/util.js +4 -0
  10. package/dist/connection/base_connection.js +6 -0
  11. package/dist/connection/validate_table_path.d.ts +10 -0
  12. package/dist/connection/validate_table_path.js +56 -0
  13. package/dist/dialect/databricks/databricks.d.ts +4 -4
  14. package/dist/dialect/databricks/databricks.js +17 -22
  15. package/dist/dialect/dialect.d.ts +100 -4
  16. package/dist/dialect/dialect.js +145 -1
  17. package/dist/dialect/duckdb/duckdb.d.ts +2 -3
  18. package/dist/dialect/duckdb/duckdb.js +12 -14
  19. package/dist/dialect/duckdb/table-path-parser.d.ts +2 -0
  20. package/dist/dialect/duckdb/table-path-parser.js +57 -0
  21. package/dist/dialect/index.d.ts +2 -0
  22. package/dist/dialect/index.js +4 -1
  23. package/dist/dialect/mysql/mysql.d.ts +4 -4
  24. package/dist/dialect/mysql/mysql.js +25 -20
  25. package/dist/dialect/pg_impl.d.ts +3 -1
  26. package/dist/dialect/pg_impl.js +6 -3
  27. package/dist/dialect/postgres/postgres.d.ts +1 -3
  28. package/dist/dialect/postgres/postgres.js +8 -16
  29. package/dist/dialect/snowflake/snowflake.d.ts +4 -4
  30. package/dist/dialect/snowflake/snowflake.js +11 -27
  31. package/dist/dialect/standardsql/standardsql.d.ts +6 -4
  32. package/dist/dialect/standardsql/standardsql.js +36 -15
  33. package/dist/dialect/table-path.d.ts +54 -0
  34. package/dist/dialect/table-path.js +144 -0
  35. package/dist/dialect/trino/trino.d.ts +0 -3
  36. package/dist/dialect/trino/trino.js +7 -20
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +4 -2
  39. package/dist/lang/ast/expressions/expr-func.js +30 -11
  40. package/dist/lang/ast/expressions/expr-given.js +1 -0
  41. package/dist/lang/ast/field-space/reference-field.js +1 -1
  42. package/dist/lang/ast/source-elements/sql-source.js +4 -0
  43. package/dist/lang/ast/source-elements/table-source.d.ts +1 -7
  44. package/dist/lang/ast/source-elements/table-source.js +24 -19
  45. package/dist/lang/ast/statements/define-given.d.ts +1 -0
  46. package/dist/lang/ast/statements/define-given.js +7 -0
  47. package/dist/lang/ast/statements/import-statement.js +4 -0
  48. package/dist/lang/ast/types/annotation-elements.d.ts +1 -0
  49. package/dist/lang/ast/types/annotation-elements.js +10 -3
  50. package/dist/lang/ast/types/malloy-element.d.ts +1 -0
  51. package/dist/lang/ast/types/malloy-element.js +4 -0
  52. package/dist/lang/malloy-to-ast.d.ts +2 -1
  53. package/dist/lang/malloy-to-ast.js +11 -1
  54. package/dist/lang/parse-log.d.ts +2 -0
  55. package/dist/lang/parse-log.js +4 -0
  56. package/dist/lang/parse-malloy.d.ts +4 -1
  57. package/dist/lang/parse-malloy.js +63 -11
  58. package/dist/lang/parse-tree-walkers/find-external-references.d.ts +2 -15
  59. package/dist/lang/parse-tree-walkers/find-external-references.js +6 -23
  60. package/dist/lang/test/test-translator.d.ts +19 -5
  61. package/dist/lang/test/test-translator.js +15 -12
  62. package/dist/lang/translate-response.d.ts +1 -1
  63. package/dist/lang/zone.d.ts +2 -0
  64. package/dist/lang/zone.js +10 -0
  65. package/dist/model/constant_expression_compiler.js +14 -5
  66. package/dist/model/expression_compiler.js +19 -17
  67. package/dist/model/field_instance.js +7 -3
  68. package/dist/model/filter_compilers.js +1 -1
  69. package/dist/model/given_binding.js +26 -21
  70. package/dist/model/index.d.ts +1 -0
  71. package/dist/model/index.js +3 -1
  72. package/dist/model/malloy_compile_error.d.ts +13 -0
  73. package/dist/model/malloy_compile_error.js +23 -0
  74. package/dist/model/malloy_types.d.ts +2 -0
  75. package/dist/model/query_model_impl.js +9 -8
  76. package/dist/model/query_node.d.ts +5 -5
  77. package/dist/model/query_node.js +21 -16
  78. package/dist/model/query_query.js +60 -44
  79. package/dist/model/sql_compiled.d.ts +2 -4
  80. package/dist/model/sql_compiled.js +20 -18
  81. package/dist/test/test-models.js +2 -2
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +4 -4
@@ -54,6 +54,12 @@ export declare class MalloyError extends Error {
54
54
  */
55
55
  constructor(message: string, problems?: LogMessage[]);
56
56
  }
57
+ type CompileRequest = Compilable & CompileOptions & CompileQueryOptions & ParseOptions & {
58
+ urlReader: URLReader;
59
+ connections: LookupConnection<InfoConnection>;
60
+ model?: Model;
61
+ cacheManager?: CacheManager;
62
+ };
57
63
  export declare class Malloy {
58
64
  static get version(): string;
59
65
  /**
@@ -109,12 +115,7 @@ export declare class Malloy {
109
115
  * @param model A compiled model to build upon (optional).
110
116
  * @return A (promise of a) compiled `Model`.
111
117
  */
112
- static compile({ url, source, parse, urlReader, connections, model, refreshSchemaCache, noThrowOnError, eventStream, importBaseURL, cacheManager, }: {
113
- urlReader: URLReader;
114
- connections: LookupConnection<InfoConnection>;
115
- model?: Model;
116
- cacheManager?: CacheManager;
117
- } & Compilable & CompileOptions & CompileQueryOptions & ParseOptions): Promise<Model>;
118
+ static compile(req: CompileRequest): Promise<Model>;
118
119
  /**
119
120
  * A dialect must provide a response for every table, or the translator loop
120
121
  * will never exit. Because there was a time when this happened, we throw
@@ -37,9 +37,6 @@ class MalloyError extends Error {
37
37
  }
38
38
  }
39
39
  exports.MalloyError = MalloyError;
40
- // =============================================================================
41
- // Malloy Static Class
42
- // =============================================================================
43
40
  class Malloy {
44
41
  static get version() {
45
42
  return version_1.MALLOY_VERSION;
@@ -70,6 +67,7 @@ class Malloy {
70
67
  return (0, registry_1.getRegisteredConnectionTypes)();
71
68
  }
72
69
  static _parse(source, url, eventStream, options, invalidationKey) {
70
+ var _a;
73
71
  if (url === undefined) {
74
72
  url = new URL(MALLOY_INTERNAL_URL);
75
73
  }
@@ -79,7 +77,7 @@ class Malloy {
79
77
  }
80
78
  const translator = new lang_1.MalloyTranslator(url.toString(), importBaseURL.toString(), {
81
79
  urls: { [url.toString()]: source },
82
- }, eventStream);
80
+ }, eventStream, (_a = options === null || options === void 0 ? void 0 : options.restrictedMode) !== null && _a !== void 0 ? _a : false);
83
81
  if (options === null || options === void 0 ? void 0 : options.testEnvironment) {
84
82
  translator.allDialectsEnabled = true;
85
83
  }
@@ -110,8 +108,19 @@ class Malloy {
110
108
  * @param model A compiled model to build upon (optional).
111
109
  * @return A (promise of a) compiled `Model`.
112
110
  */
113
- static async compile({ url, source, parse, urlReader, connections, model, refreshSchemaCache, noThrowOnError, eventStream, importBaseURL, cacheManager, }) {
111
+ static async compile(req) {
114
112
  var _a, _b, _c, _d, _e;
113
+ let { url, source, importBaseURL, cacheManager } = req;
114
+ const { parse, urlReader, connections, model, refreshSchemaCache, noThrowOnError, eventStream, restrictedMode, } = req;
115
+ if (restrictedMode) {
116
+ // Restricted-mode compiles do not participate in the model-def
117
+ // cache. The cache key is the URL, but restricted vs. unrestricted
118
+ // produces different validation outcomes, so allowing a restricted
119
+ // compile to serve from (or write to) the same cache as
120
+ // unrestricted compiles would let restricted mode be bypassed by a
121
+ // prior unrestricted compile of the same URL.
122
+ cacheManager = undefined;
123
+ }
115
124
  let refreshTimestamp;
116
125
  if (refreshSchemaCache) {
117
126
  refreshTimestamp =
@@ -145,6 +154,13 @@ class Malloy {
145
154
  // It's not cached, so we may need to get the actual source
146
155
  const _url = url.toString();
147
156
  if (parse !== undefined) {
157
+ // A pre-parsed translator's restrictedMode was fixed at parse
158
+ // time and cannot be changed here. Loudly reject mismatched
159
+ // requests rather than silently inheriting the parse-time value.
160
+ if (restrictedMode !== undefined &&
161
+ parse._translator.restrictedMode !== restrictedMode) {
162
+ throw new Error(`Malloy.compile: restrictedMode (${restrictedMode}) does not match the pre-parsed translator's restrictedMode (${parse._translator.restrictedMode}). Set restrictedMode at parse time.`);
163
+ }
148
164
  translator = parse._translator;
149
165
  const invalidationKey = (_a = parse._invalidationKey) !== null && _a !== void 0 ? _a : (await (0, readers_1.getInvalidationKey)(urlReader, url));
150
166
  invalidationKeys[_url] = invalidationKey;
@@ -161,7 +177,7 @@ class Malloy {
161
177
  }
162
178
  translator = new lang_1.MalloyTranslator(_url, importBaseURL.toString(), {
163
179
  urls: { [_url]: source },
164
- }, eventStream);
180
+ }, eventStream, restrictedMode !== null && restrictedMode !== void 0 ? restrictedMode : false);
165
181
  }
166
182
  for (;;) {
167
183
  const result = translator.translate(model === null || model === void 0 ? void 0 : model._modelDef);
@@ -40,9 +40,7 @@ export declare class Manifest {
40
40
  */
41
41
  get strict(): boolean;
42
42
  set strict(value: boolean);
43
- /**
44
- * Add or replace a manifest entry. Also marks it as touched.
45
- */
43
+ /** Add or replace a manifest entry. Also marks it as touched. */
46
44
  update(buildId: BuildID, entry: BuildManifestEntry): void;
47
45
  /**
48
46
  * Mark an existing entry as active without changing it.
@@ -257,3 +255,4 @@ export declare class MalloyConfig {
257
255
  */
258
256
  readOverlay(overlayName: string, ...path: string[]): Promise<unknown>;
259
257
  }
258
+ export declare function isBuildManifestEntry(value: unknown): value is BuildManifestEntry;
@@ -5,10 +5,12 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.MalloyConfig = exports.Manifest = void 0;
8
+ exports.isBuildManifestEntry = isBuildManifestEntry;
8
9
  const config_compile_1 = require("./config_compile");
9
10
  const config_resolve_1 = require("./config_resolve");
10
11
  const config_lookup_1 = require("./config_lookup");
11
12
  const config_overlays_1 = require("./config_overlays");
13
+ const validate_table_path_1 = require("../../connection/validate_table_path");
12
14
  /**
13
15
  * In-memory manifest store. Reads, updates, and serializes manifest data.
14
16
  *
@@ -61,10 +63,9 @@ class Manifest {
61
63
  set strict(value) {
62
64
  this._manifest.strict = value;
63
65
  }
64
- /**
65
- * Add or replace a manifest entry. Also marks it as touched.
66
- */
66
+ /** Add or replace a manifest entry. Also marks it as touched. */
67
67
  update(buildId, entry) {
68
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(entry.tableName, `Manifest entry '${buildId}'`);
68
69
  this._manifest.entries[buildId] = entry;
69
70
  this._touched.add(buildId);
70
71
  }
@@ -102,9 +103,10 @@ class Manifest {
102
103
  // Old format: {buildId: {tableName}, ...} (flat record, no "entries" key)
103
104
  const rawEntries = isRecord(parsed['entries']) ? parsed['entries'] : parsed;
104
105
  for (const [key, val] of Object.entries(rawEntries)) {
105
- if (key !== 'strict' && isBuildManifestEntry(val)) {
106
- this._manifest.entries[key] = val;
107
- }
106
+ if (key === 'strict' || !isBuildManifestEntry(val))
107
+ continue;
108
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(val.tableName, `Manifest entry '${key}'`);
109
+ this._manifest.entries[key] = val;
108
110
  }
109
111
  }
110
112
  }
@@ -166,7 +168,7 @@ class MalloyConfig {
166
168
  this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, mergedOverlays, log);
167
169
  this._connections = this._managedLookup;
168
170
  this._overlays = mergedOverlays;
169
- this.virtualMap = toVirtualMap(prepared.virtualMap);
171
+ this.virtualMap = toVirtualMap(prepared.virtualMap, log);
170
172
  this.configURL = configURL;
171
173
  this.rootDirectory = rootDirectory;
172
174
  this.manifestPath = prepared.manifestPath;
@@ -441,9 +443,10 @@ function validateURLString(value, fieldName, log) {
441
443
  }
442
444
  /**
443
445
  * Convert the raw virtualMap POJO shape (a dict of dicts of strings) into
444
- * the runtime Map-of-Maps representation that Runtime consumes.
446
+ * the runtime Map-of-Maps representation. Invalid entries are dropped and
447
+ * logged — they'd be pasted into FROM clauses downstream otherwise.
445
448
  */
446
- function toVirtualMap(raw) {
449
+ function toVirtualMap(raw, log) {
447
450
  if (!isRecord(raw))
448
451
  return undefined;
449
452
  const outer = new Map();
@@ -452,9 +455,18 @@ function toVirtualMap(raw) {
452
455
  continue;
453
456
  const innerMap = new Map();
454
457
  for (const [virtualName, tablePath] of Object.entries(inner)) {
455
- if (typeof tablePath === 'string') {
456
- innerMap.set(virtualName, tablePath);
458
+ if (typeof tablePath !== 'string')
459
+ continue;
460
+ const invalid = (0, validate_table_path_1.validateCanonicalTablePathAnyDialect)(tablePath);
461
+ if (invalid !== undefined) {
462
+ log.push({
463
+ message: `virtualMap entry '${connName}.${virtualName}': ${invalid}`,
464
+ severity: 'error',
465
+ code: 'config-validation',
466
+ });
467
+ continue;
457
468
  }
469
+ innerMap.set(virtualName, tablePath);
458
470
  }
459
471
  if (innerMap.size > 0)
460
472
  outer.set(connName, innerMap);
@@ -1101,7 +1101,7 @@ class PersistSource {
1101
1101
  const sd = this.persistableDef;
1102
1102
  const queryModel = this.model.queryModel;
1103
1103
  if (sd.type === 'sql_select') {
1104
- return (0, model_1.getCompiledSQL)(sd, options !== null && options !== void 0 ? options : {}, path => this.dialect.quoteTablePath(path), (query, opts) => queryModel.compileQuery(query, opts).sql);
1104
+ return (0, model_1.getCompiledSQL)(sd, options !== null && options !== void 0 ? options : {}, (query, opts) => queryModel.compileQuery(query, opts).sql);
1105
1105
  }
1106
1106
  else {
1107
1107
  const compiled = queryModel.compileQuery(sd.query, options);
@@ -1,6 +1,7 @@
1
1
  import type { Connection, LookupConnection } from '../../connection/types';
2
2
  import type { URLReader, EventStream } from '../../runtime_types';
3
3
  import type { ModelDef, Query as InternalQuery, SearchIndexResult, SearchValueMapResult, QueryRunStats, BuildManifest, GivenValue, VirtualMap } from '../../model';
4
+ import type { LogMessage } from '../../lang';
4
5
  import type { Dialect } from '../../dialect';
5
6
  import type { RunSQLOptions } from '../../run_sql_options';
6
7
  import type { CacheManager } from './cache';
@@ -345,6 +346,50 @@ export declare class ModelMaterializer extends FluentState<Model> {
345
346
  * or loading further related objects.
346
347
  */
347
348
  loadQuery(query: QueryString | QueryURL, options?: ParseOptions & CompileOptions & CompileQueryOptions): QueryMaterializer;
349
+ /**
350
+ * Load a Malloy query whose text comes from an untrusted source — an
351
+ * MCP client, an LLM-authored query, a UI field, an HTTP request body
352
+ * — and should compile against this already-loaded trusted model.
353
+ * Use this in preference to `loadQuery` when the caller of your
354
+ * service is not the author of the model and may write Malloy that
355
+ * reaches past the model's curated surface.
356
+ *
357
+ * Restricted-mode compilation rejects these constructs:
358
+ *
359
+ * - `import` statements
360
+ * - `given:` declarations (restricted queries may still reference
361
+ * givens the model declared, via `$NAME`)
362
+ * - `##!` compiler-flag annotations
363
+ * - `connection.table(...)` and `connection.sql(...)` source forms
364
+ * - `name!type(args)` raw-SQL function calls
365
+ * - The `sql_*` family of built-in functions (`sql_number`,
366
+ * `sql_string`, `sql_date`, `sql_timestamp`, `sql_boolean`)
367
+ *
368
+ * The model's existing surface — sources, queries, dimensions,
369
+ * measures, functions, givens declared by the model author — is
370
+ * fully available regardless of whether the model's own definitions
371
+ * use any of the forbidden constructs.
372
+ *
373
+ * Errors reach the caller in the same way as any other Malloy
374
+ * compile: `.validate()` returns a `LogMessage[]`, and `.run()` /
375
+ * `.getPreparedResult()` / `.getSQL()` throw `MalloyError` whose
376
+ * `.problems` is the same array. The list holds every compile
377
+ * problem — ordinary translator and SQL-compile errors as well as
378
+ * restricted-mode rejections. The restricted-mode subset is
379
+ * identifiable by `code: 'restricted-construct-forbidden'` and
380
+ * `errorTag: 'restricted-mode'`; restricted-mode rejection messages
381
+ * quote the offending source text and state the rule. Multiple
382
+ * violations are reported in one compile.
383
+ *
384
+ * The input is required to be a string. Restricted text arrives from
385
+ * an untrusted caller as bytes the host already has in hand; there
386
+ * is no host-side trust mechanism for fetching it via a URL.
387
+ *
388
+ * @param text The Malloy text to compile as a restricted query.
389
+ * @return A `QueryMaterializer` capable of materializing or running
390
+ * the query.
391
+ */
392
+ loadRestrictedQuery(text: string): QueryMaterializer;
348
393
  /**
349
394
  * Extend a Malloy model by URL or contents.
350
395
  *
@@ -404,16 +449,47 @@ export declare class ModelMaterializer extends FluentState<Model> {
404
449
  * @return A promise to the compiled model that is loaded.
405
450
  */
406
451
  getModel(): Promise<Model>;
452
+ /**
453
+ * Compile this model and return any problems as structured
454
+ * `LogMessage`s; empty array means clean. Non-throwing.
455
+ *
456
+ * Only translator-time problems surface here; SQL-compile problems
457
+ * are per-query and live on `QueryMaterializer.validate()`.
458
+ *
459
+ * A subsequent `getModel()` reuses the cached materialize.
460
+ */
461
+ validate(): Promise<LogMessage[]>;
407
462
  }
408
- /**
409
- * An object representing the task of loading a `Query`, capable of
410
- * materializing the query (via `getPreparedQuery()`) or extending the task to load
411
- * prepared results or run the query (via e.g. `loadPreparedResult()` or `run()`).
412
- */
413
463
  export declare class QueryMaterializer extends FluentState<PreparedQuery> {
414
464
  protected runtime: Runtime;
415
465
  private readonly compileQueryOptions;
466
+ /**
467
+ * Memoizes the no-options compile so `validate()` + a follow-up
468
+ * `getPreparedResult()` / `run()` shares one compilation. Calls with
469
+ * explicit options bypass — options aren't hashed.
470
+ */
471
+ private _compileAttempt;
416
472
  constructor(runtime: Runtime, materialize: () => Promise<PreparedQuery>, options?: CompileQueryOptions);
473
+ /**
474
+ * `MalloyError` (translator) is unwrapped into its `problems` array.
475
+ * `MalloyCompileError` (SQL compiler) becomes one structured problem.
476
+ * Any other `Error` is treated as an invariant violation and surfaces
477
+ * as one `code: 'compiler-bug'` problem.
478
+ */
479
+ private _compileAndCollect;
480
+ private compileAttempt;
481
+ /**
482
+ * Compile this query and return any problems as structured
483
+ * `LogMessage`s; empty array means clean. Non-throwing.
484
+ *
485
+ * Surfaces translator-time errors (may be several) and compile-time
486
+ * errors (at most one — the compiler is fail-fast). LogMessages carry
487
+ * `code` and, where the IR has it, `at: DocumentLocation`.
488
+ *
489
+ * A subsequent no-options `getPreparedResult()` / `getSQL()` / `run()`
490
+ * reuses the cached compile.
491
+ */
492
+ validate(options?: CompileQueryOptions): Promise<LogMessage[]>;
417
493
  /**
418
494
  * Run this loaded `Query`.
419
495
  *
@@ -431,6 +507,10 @@ export declare class QueryMaterializer extends FluentState<PreparedQuery> {
431
507
  /**
432
508
  * Materialize the prepared result of this loaded query.
433
509
  *
510
+ * Throws `MalloyError` on failure, with `.problems` populated for both
511
+ * translator and compiler errors. For non-throwing access to the same
512
+ * problem list, use `validate()`.
513
+ *
434
514
  * @return A promise of the prepared result of this loaded query.
435
515
  */
436
516
  getPreparedResult(options?: CompileQueryOptions): Promise<PreparedResult>;
@@ -7,6 +7,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.ExploreMaterializer = exports.PreparedResultMaterializer = exports.QueryMaterializer = exports.ModelMaterializer = exports.SingleConnectionRuntime = exports.ConnectionRuntime = exports.Runtime = void 0;
8
8
  const model_1 = require("../../model");
9
9
  const dialect_1 = require("../../dialect");
10
+ const validate_table_path_1 = require("../../connection/validate_table_path");
11
+ const config_1 = require("./config");
10
12
  const row_data_utils_1 = require("../../api/row_data_utils");
11
13
  const readers_1 = require("./readers");
12
14
  const core_1 = require("./core");
@@ -221,6 +223,12 @@ class Runtime {
221
223
  if (!isBuildManifestShape(parsed)) {
222
224
  throw new Error('manifest is not an object with an "entries" map');
223
225
  }
226
+ for (const [buildId, entry] of Object.entries(parsed.entries)) {
227
+ if (!(0, config_1.isBuildManifestEntry)(entry)) {
228
+ throw new Error(`Manifest entry '${buildId}' is missing a string tableName`);
229
+ }
230
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(entry.tableName, `Manifest entry '${buildId}'`);
231
+ }
224
232
  return parsed;
225
233
  }
226
234
  catch (e) {
@@ -502,7 +510,7 @@ class SingleConnectionRuntime extends Runtime {
502
510
  }
503
511
  // quote a column name
504
512
  quote(column) {
505
- return (0, dialect_1.getDialect)(this.connection.dialectName).sqlMaybeQuoteIdentifier(column);
513
+ return (0, dialect_1.getDialect)(this.connection.dialectName).sqlQuoteIdentifier(column);
506
514
  }
507
515
  get dialect() {
508
516
  return (0, dialect_1.getDialect)(this.connection.dialectName);
@@ -606,6 +614,67 @@ class ModelMaterializer extends FluentState {
606
614
  return queryModel.preparedQuery;
607
615
  });
608
616
  }
617
+ /**
618
+ * Load a Malloy query whose text comes from an untrusted source — an
619
+ * MCP client, an LLM-authored query, a UI field, an HTTP request body
620
+ * — and should compile against this already-loaded trusted model.
621
+ * Use this in preference to `loadQuery` when the caller of your
622
+ * service is not the author of the model and may write Malloy that
623
+ * reaches past the model's curated surface.
624
+ *
625
+ * Restricted-mode compilation rejects these constructs:
626
+ *
627
+ * - `import` statements
628
+ * - `given:` declarations (restricted queries may still reference
629
+ * givens the model declared, via `$NAME`)
630
+ * - `##!` compiler-flag annotations
631
+ * - `connection.table(...)` and `connection.sql(...)` source forms
632
+ * - `name!type(args)` raw-SQL function calls
633
+ * - The `sql_*` family of built-in functions (`sql_number`,
634
+ * `sql_string`, `sql_date`, `sql_timestamp`, `sql_boolean`)
635
+ *
636
+ * The model's existing surface — sources, queries, dimensions,
637
+ * measures, functions, givens declared by the model author — is
638
+ * fully available regardless of whether the model's own definitions
639
+ * use any of the forbidden constructs.
640
+ *
641
+ * Errors reach the caller in the same way as any other Malloy
642
+ * compile: `.validate()` returns a `LogMessage[]`, and `.run()` /
643
+ * `.getPreparedResult()` / `.getSQL()` throw `MalloyError` whose
644
+ * `.problems` is the same array. The list holds every compile
645
+ * problem — ordinary translator and SQL-compile errors as well as
646
+ * restricted-mode rejections. The restricted-mode subset is
647
+ * identifiable by `code: 'restricted-construct-forbidden'` and
648
+ * `errorTag: 'restricted-mode'`; restricted-mode rejection messages
649
+ * quote the offending source text and state the rule. Multiple
650
+ * violations are reported in one compile.
651
+ *
652
+ * The input is required to be a string. Restricted text arrives from
653
+ * an untrusted caller as bytes the host already has in hand; there
654
+ * is no host-side trust mechanism for fetching it via a URL.
655
+ *
656
+ * @param text The Malloy text to compile as a restricted query.
657
+ * @return A `QueryMaterializer` capable of materializing or running
658
+ * the query.
659
+ */
660
+ loadRestrictedQuery(text) {
661
+ return this.makeQueryMaterializer(async () => {
662
+ const urlReader = this.runtime.urlReader;
663
+ const connections = this.runtime.connections;
664
+ const testEnvironment = this.runtime.isTestRuntime ? true : undefined;
665
+ const model = await this.getModel();
666
+ const queryModel = await compile_1.Malloy.compile({
667
+ source: text,
668
+ restrictedMode: true,
669
+ urlReader,
670
+ connections,
671
+ model,
672
+ testEnvironment,
673
+ ...this.compileQueryOptions,
674
+ });
675
+ return queryModel.preparedQuery;
676
+ });
677
+ }
609
678
  /**
610
679
  * Extend a Malloy model by URL or contents.
611
680
  *
@@ -767,6 +836,36 @@ class ModelMaterializer extends FluentState {
767
836
  getModel() {
768
837
  return this.materialize();
769
838
  }
839
+ /**
840
+ * Compile this model and return any problems as structured
841
+ * `LogMessage`s; empty array means clean. Non-throwing.
842
+ *
843
+ * Only translator-time problems surface here; SQL-compile problems
844
+ * are per-query and live on `QueryMaterializer.validate()`.
845
+ *
846
+ * A subsequent `getModel()` reuses the cached materialize.
847
+ */
848
+ async validate() {
849
+ try {
850
+ await this.materialize();
851
+ return [];
852
+ }
853
+ catch (e) {
854
+ if (e instanceof compile_1.MalloyError)
855
+ return e.problems;
856
+ if (e instanceof Error) {
857
+ return [
858
+ {
859
+ message: 'Internal compiler error (likely a Malloy bug — please file ' +
860
+ `an issue): ${e.message}`,
861
+ severity: 'error',
862
+ code: 'compiler-bug',
863
+ },
864
+ ];
865
+ }
866
+ throw e;
867
+ }
868
+ }
770
869
  }
771
870
  exports.ModelMaterializer = ModelMaterializer;
772
871
  // =============================================================================
@@ -779,17 +878,78 @@ function runSQLOptionsWithAnnotations(preparedResult, givenOptions) {
779
878
  ...givenOptions,
780
879
  };
781
880
  }
782
- /**
783
- * An object representing the task of loading a `Query`, capable of
784
- * materializing the query (via `getPreparedQuery()`) or extending the task to load
785
- * prepared results or run the query (via e.g. `loadPreparedResult()` or `run()`).
786
- */
787
881
  class QueryMaterializer extends FluentState {
788
882
  constructor(runtime, materialize, options) {
789
883
  super(runtime, materialize);
790
884
  this.runtime = runtime;
791
885
  this.compileQueryOptions = options;
792
886
  }
887
+ /**
888
+ * `MalloyError` (translator) is unwrapped into its `problems` array.
889
+ * `MalloyCompileError` (SQL compiler) becomes one structured problem.
890
+ * Any other `Error` is treated as an invariant violation and surfaces
891
+ * as one `code: 'compiler-bug'` problem.
892
+ */
893
+ async _compileAndCollect(options) {
894
+ try {
895
+ const preparedResult = await this.loadPreparedResult(options).getPreparedResult();
896
+ return { preparedResult, problems: [] };
897
+ }
898
+ catch (e) {
899
+ if (e instanceof compile_1.MalloyError) {
900
+ return { problems: e.problems };
901
+ }
902
+ if (e instanceof model_1.MalloyCompileError) {
903
+ return {
904
+ problems: [
905
+ {
906
+ message: e.message,
907
+ severity: 'error',
908
+ code: e.code,
909
+ at: e.at,
910
+ },
911
+ ],
912
+ };
913
+ }
914
+ if (e instanceof Error) {
915
+ return {
916
+ problems: [
917
+ {
918
+ message: 'Internal compiler error (likely a Malloy bug — please file ' +
919
+ `an issue): ${e.message}`,
920
+ severity: 'error',
921
+ code: 'compiler-bug',
922
+ },
923
+ ],
924
+ };
925
+ }
926
+ throw e;
927
+ }
928
+ }
929
+ compileAttempt(options) {
930
+ if (options === undefined && this._compileAttempt) {
931
+ return this._compileAttempt;
932
+ }
933
+ const p = this._compileAndCollect(options);
934
+ if (options === undefined) {
935
+ this._compileAttempt = p;
936
+ }
937
+ return p;
938
+ }
939
+ /**
940
+ * Compile this query and return any problems as structured
941
+ * `LogMessage`s; empty array means clean. Non-throwing.
942
+ *
943
+ * Surfaces translator-time errors (may be several) and compile-time
944
+ * errors (at most one — the compiler is fail-fast). LogMessages carry
945
+ * `code` and, where the IR has it, `at: DocumentLocation`.
946
+ *
947
+ * A subsequent no-options `getPreparedResult()` / `getSQL()` / `run()`
948
+ * reuses the cached compile.
949
+ */
950
+ async validate(options) {
951
+ return (await this.compileAttempt(options)).problems;
952
+ }
793
953
  /**
794
954
  * Run this loaded `Query`.
795
955
  *
@@ -834,6 +994,11 @@ class QueryMaterializer extends FluentState {
834
994
  // Pass EMPTY_BUILD_MANIFEST in options to explicitly suppress manifest substitution.
835
995
  const explicitManifest = mergedOptions.buildManifest !== undefined;
836
996
  let buildManifest = (_a = mergedOptions.buildManifest) !== null && _a !== void 0 ? _a : (await this.runtime._resolveBuildManifest());
997
+ if (buildManifest) {
998
+ for (const [buildId, entry] of Object.entries(buildManifest.entries)) {
999
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(entry.tableName, `Manifest entry '${buildId}'`);
1000
+ }
1001
+ }
837
1002
  // If we have a manifest with entries, compute connectionDigests for lookups.
838
1003
  // TODO: This is inefficient - we call getBuildPlan just to find connection names.
839
1004
  // Consider adding a listConnections() method to LookupConnection, or caching this.
@@ -849,7 +1014,9 @@ class QueryMaterializer extends FluentState {
849
1014
  if (!modelTag.has('experimental', 'persistence')) {
850
1015
  if (explicitManifest) {
851
1016
  // Explicitly passed non-empty manifest requires persistence support
852
- throw new Error('Model must have ##! experimental.persistence to use buildManifest');
1017
+ throw new model_1.MalloyCompileError('A non-empty `buildManifest` was supplied, but the model is ' +
1018
+ 'missing `##! experimental.persistence`. Add the flag to the ' +
1019
+ 'model or pass `EMPTY_BUILD_MANIFEST` to suppress substitution.', 'runtime-manifest-needs-persistence-flag', undefined);
853
1020
  }
854
1021
  // Runtime-level manifest (e.g. from config): silently ignore
855
1022
  buildManifest = undefined;
@@ -866,6 +1033,13 @@ class QueryMaterializer extends FluentState {
866
1033
  }
867
1034
  // Use virtualMap from options if provided, otherwise fall back to Runtime's.
868
1035
  const virtualMap = (_b = mergedOptions.virtualMap) !== null && _b !== void 0 ? _b : this.runtime.virtualMap;
1036
+ if (virtualMap) {
1037
+ for (const [connName, inner] of virtualMap) {
1038
+ for (const [virtualName, tablePath] of inner) {
1039
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(tablePath, `virtualMap entry '${connName}.${virtualName}'`);
1040
+ }
1041
+ }
1042
+ }
869
1043
  // Per-query supply for a finalized given is rejected at API entry
870
1044
  // — the finalized-givens set is the runtime's "this can't be
871
1045
  // overridden by the caller" guarantee. Fail before any IO so misuse
@@ -874,7 +1048,8 @@ class QueryMaterializer extends FluentState {
874
1048
  if (finalizedSet.size > 0 && mergedOptions.givens) {
875
1049
  for (const name of Object.keys(mergedOptions.givens)) {
876
1050
  if (finalizedSet.has(name)) {
877
- throw new Error(`Cannot supply '${name}' per-query: it is finalized at the runtime layer (config.finalizeGivens).`);
1051
+ throw new model_1.MalloyCompileError(`Cannot supply given '${name}' per-query: it is finalized at ` +
1052
+ 'the runtime layer via `config.finalizeGivens`.', 'runtime-given-finalized', undefined);
878
1053
  }
879
1054
  }
880
1055
  }
@@ -901,7 +1076,7 @@ class QueryMaterializer extends FluentState {
901
1076
  const queryGivenUsage = (_c = preparedQuery._query.givenUsage) !== null && _c !== void 0 ? _c : [];
902
1077
  if (finalizedSet.size > 0 && queryGivenUsage.length > 0) {
903
1078
  const referencedIds = new Set(queryGivenUsage.map(g => g.id));
904
- const missing = [];
1079
+ const missingProblems = [];
905
1080
  for (const [surfaceName, entry] of Object.entries(preparedQuery._modelDef.contents)) {
906
1081
  if (entry.type !== 'given')
907
1082
  continue;
@@ -911,10 +1086,18 @@ class QueryMaterializer extends FluentState {
911
1086
  continue;
912
1087
  if (mergedGivens && surfaceName in mergedGivens)
913
1088
  continue;
914
- missing.push(surfaceName);
1089
+ const firstUse = queryGivenUsage.find(u => u.id === entry.id);
1090
+ missingProblems.push({
1091
+ message: `Finalized given '${surfaceName}' has no resolved value. ` +
1092
+ 'It must be supplied in `givensPath` or in the Runtime ' +
1093
+ "constructor's `givens`.",
1094
+ severity: 'error',
1095
+ code: 'runtime-given-finalized-missing',
1096
+ at: firstUse === null || firstUse === void 0 ? void 0 : firstUse.at,
1097
+ });
915
1098
  }
916
- if (missing.length > 0) {
917
- throw new Error(`Query references finalized given(s) with no resolved value: ${missing.join(', ')}. Each finalized given the query needs must have a value in givensPath or in the Runtime constructor's \`givens\`.`);
1099
+ if (missingProblems.length > 0) {
1100
+ throw new compile_1.MalloyError(missingProblems.map(p => p.message).join('\n'), missingProblems);
918
1101
  }
919
1102
  }
920
1103
  // Build PrepareResultOptions from CompileQueryOptions + connectionDigests.
@@ -934,10 +1117,17 @@ class QueryMaterializer extends FluentState {
934
1117
  /**
935
1118
  * Materialize the prepared result of this loaded query.
936
1119
  *
1120
+ * Throws `MalloyError` on failure, with `.problems` populated for both
1121
+ * translator and compiler errors. For non-throwing access to the same
1122
+ * problem list, use `validate()`.
1123
+ *
937
1124
  * @return A promise of the prepared result of this loaded query.
938
1125
  */
939
- getPreparedResult(options) {
940
- return this.loadPreparedResult(options).getPreparedResult();
1126
+ async getPreparedResult(options) {
1127
+ const attempt = await this.compileAttempt(options);
1128
+ if (attempt.preparedResult)
1129
+ return attempt.preparedResult;
1130
+ throw new compile_1.MalloyError(attempt.problems.map(p => p.message).join('\n') || 'Compilation failed.', attempt.problems);
941
1131
  }
942
1132
  /**
943
1133
  * Materialize the SQL of this loaded query.
@@ -19,6 +19,8 @@ export interface Loggable {
19
19
  export interface ParseOptions {
20
20
  importBaseURL?: URL;
21
21
  testEnvironment?: boolean;
22
+ /** Reject language constructs that reach outside the trusted model. */
23
+ restrictedMode?: boolean;
22
24
  }
23
25
  /** Options for how to run the Malloy semantic checker/translator */
24
26
  export interface CompileOptions {
package/dist/api/util.js CHANGED
@@ -13,6 +13,7 @@ exports.mapData = mapData;
13
13
  exports.wrapResult = wrapResult;
14
14
  exports.nodeToLiteralValue = nodeToLiteralValue;
15
15
  exports.mapLogs = mapLogs;
16
+ const validate_table_path_1 = require("../connection/validate_table_path");
16
17
  const to_stable_1 = require("../to_stable");
17
18
  const row_data_utils_1 = require("./row_data_utils");
18
19
  function wrapLegacyInfoConnection(connection) {
@@ -31,6 +32,9 @@ function wrapLegacyInfoConnection(connection) {
31
32
  };
32
33
  },
33
34
  async fetchSchemaForTable(tableName) {
35
+ const invalid = (0, validate_table_path_1.validateCanonicalTablePath)(connection.dialectName, tableName);
36
+ if (invalid !== undefined)
37
+ throw new Error(invalid);
34
38
  const key = `${connection.name}:${tableName}`;
35
39
  const result = await connection.fetchSchemaForTables({ [key]: tableName }, {});
36
40
  const table = result.schemas[key];