@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.
- package/dist/api/foundation/compile.d.ts +7 -6
- package/dist/api/foundation/compile.js +22 -6
- package/dist/api/foundation/config.d.ts +2 -3
- package/dist/api/foundation/config.js +23 -11
- package/dist/api/foundation/core.js +1 -1
- package/dist/api/foundation/runtime.d.ts +85 -5
- package/dist/api/foundation/runtime.js +204 -14
- package/dist/api/foundation/types.d.ts +2 -0
- package/dist/api/util.js +4 -0
- package/dist/connection/base_connection.js +6 -0
- package/dist/connection/validate_table_path.d.ts +10 -0
- package/dist/connection/validate_table_path.js +56 -0
- package/dist/dialect/databricks/databricks.d.ts +4 -4
- package/dist/dialect/databricks/databricks.js +17 -22
- package/dist/dialect/dialect.d.ts +100 -4
- package/dist/dialect/dialect.js +145 -1
- package/dist/dialect/duckdb/duckdb.d.ts +2 -3
- package/dist/dialect/duckdb/duckdb.js +12 -14
- package/dist/dialect/duckdb/table-path-parser.d.ts +2 -0
- package/dist/dialect/duckdb/table-path-parser.js +57 -0
- package/dist/dialect/index.d.ts +2 -0
- package/dist/dialect/index.js +4 -1
- package/dist/dialect/mysql/mysql.d.ts +4 -4
- package/dist/dialect/mysql/mysql.js +25 -20
- package/dist/dialect/pg_impl.d.ts +3 -1
- package/dist/dialect/pg_impl.js +6 -3
- package/dist/dialect/postgres/postgres.d.ts +1 -3
- package/dist/dialect/postgres/postgres.js +8 -16
- package/dist/dialect/snowflake/snowflake.d.ts +4 -4
- package/dist/dialect/snowflake/snowflake.js +11 -27
- package/dist/dialect/standardsql/standardsql.d.ts +6 -4
- package/dist/dialect/standardsql/standardsql.js +36 -15
- package/dist/dialect/table-path.d.ts +54 -0
- package/dist/dialect/table-path.js +144 -0
- package/dist/dialect/trino/trino.d.ts +0 -3
- package/dist/dialect/trino/trino.js +7 -20
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/lang/ast/expressions/expr-func.js +30 -11
- package/dist/lang/ast/expressions/expr-given.js +1 -0
- package/dist/lang/ast/field-space/reference-field.js +1 -1
- package/dist/lang/ast/source-elements/sql-source.js +4 -0
- package/dist/lang/ast/source-elements/table-source.d.ts +1 -7
- package/dist/lang/ast/source-elements/table-source.js +24 -19
- package/dist/lang/ast/statements/define-given.d.ts +1 -0
- package/dist/lang/ast/statements/define-given.js +7 -0
- package/dist/lang/ast/statements/import-statement.js +4 -0
- package/dist/lang/ast/types/annotation-elements.d.ts +1 -0
- package/dist/lang/ast/types/annotation-elements.js +10 -3
- package/dist/lang/ast/types/malloy-element.d.ts +1 -0
- package/dist/lang/ast/types/malloy-element.js +4 -0
- package/dist/lang/malloy-to-ast.d.ts +2 -1
- package/dist/lang/malloy-to-ast.js +11 -1
- package/dist/lang/parse-log.d.ts +2 -0
- package/dist/lang/parse-log.js +4 -0
- package/dist/lang/parse-malloy.d.ts +4 -1
- package/dist/lang/parse-malloy.js +63 -11
- package/dist/lang/parse-tree-walkers/find-external-references.d.ts +2 -15
- package/dist/lang/parse-tree-walkers/find-external-references.js +6 -23
- package/dist/lang/test/test-translator.d.ts +19 -5
- package/dist/lang/test/test-translator.js +15 -12
- package/dist/lang/translate-response.d.ts +1 -1
- package/dist/lang/zone.d.ts +2 -0
- package/dist/lang/zone.js +10 -0
- package/dist/model/constant_expression_compiler.js +14 -5
- package/dist/model/expression_compiler.js +19 -17
- package/dist/model/field_instance.js +7 -3
- package/dist/model/filter_compilers.js +1 -1
- package/dist/model/given_binding.js +26 -21
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +3 -1
- package/dist/model/malloy_compile_error.d.ts +13 -0
- package/dist/model/malloy_compile_error.js +23 -0
- package/dist/model/malloy_types.d.ts +2 -0
- package/dist/model/query_model_impl.js +9 -8
- package/dist/model/query_node.d.ts +5 -5
- package/dist/model/query_node.js +21 -16
- package/dist/model/query_query.js +60 -44
- package/dist/model/sql_compiled.d.ts +2 -4
- package/dist/model/sql_compiled.js +20 -18
- package/dist/test/test-models.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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(
|
|
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(
|
|
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
|
|
106
|
-
|
|
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
|
|
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
|
|
456
|
-
|
|
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 : {},
|
|
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).
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
917
|
-
throw new
|
|
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
|
-
|
|
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];
|