@malloy-publisher/server 0.9.10 → 0.9.12
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/app/api-doc.yaml +121 -45
- package/dist/app/assets/{EnvironmentPage-DfL-2Fy5.js → EnvironmentPage-BFcCRfvE.js} +1 -1
- package/dist/app/assets/{HomePage-Cc5z85GD.js → HomePage-C0LnSonK.js} +1 -1
- package/dist/app/assets/MainPage-D3EM60u0.js +1 -0
- package/dist/app/assets/ModelPage-B1wvZV7r.js +2 -0
- package/dist/app/assets/PackagePage-QZ59dwip.js +1 -0
- package/dist/app/assets/RenderedResult-CDiXJAh9.es-C9BYYEfH.js +153 -0
- package/dist/app/assets/{RouteError-Bc7-ukP7.js → RouteError-v8LP_Q9x.js} +1 -1
- package/dist/app/assets/ThemeEditorPage-B5l2nzAq.js +1 -0
- package/dist/app/assets/ToggleButtonGroup-C6Lespxy.js +1 -0
- package/dist/app/assets/{documentRoutes-CvCc_EZs.js → documentRoutes-xv0fP4cS.js} +1 -1
- package/dist/app/assets/editPageMounted-COiBq84S.js +2 -0
- package/dist/app/assets/index-5On9Gkpl.js +480 -0
- package/dist/app/assets/{index-DugQbYOk.js → index-BG7MWz9o.js} +1 -1
- package/dist/app/assets/{index-DnRXl1Nu.js → index-Dynd6zeI.js} +1 -1
- package/dist/app/assets/{index-Ddy-NxB_.js → index-mIetEOsB.js} +1 -1
- package/dist/app/assets/index.es-FZZk_rEa.js +143 -0
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +128 -17
- package/dist/server.mjs +966 -647
- package/dist/sshcrypto-cqb52edz.node +0 -0
- package/package.json +2 -1
- package/dist/app/assets/MainPage-Cv63Ip_P.js +0 -1
- package/dist/app/assets/ModelPage-DvyYSgri.js +0 -2
- package/dist/app/assets/PackagePage-BDzBa7qB.js +0 -1
- package/dist/app/assets/RenderedResult-Dp1Qdajz.es-woQDtiv_.js +0 -153
- package/dist/app/assets/ThemeEditorPage-DShPXmlS.js +0 -1
- package/dist/app/assets/ToggleButtonGroup-WO-9ZUmS.js +0 -1
- package/dist/app/assets/index-BvSbJtsT.js +0 -483
- package/dist/app/assets/index.es-BhMC9nt1.js +0 -195
package/dist/server.mjs
CHANGED
|
@@ -205154,15 +205154,36 @@ function filesystemAccessFailure(error2) {
|
|
|
205154
205154
|
function isMysqlConfigFault(code) {
|
|
205155
205155
|
return typeof code === "string" && (MYSQL_CONFIG_CODES.has(code) || code.startsWith("HANDSHAKE_"));
|
|
205156
205156
|
}
|
|
205157
|
-
function
|
|
205158
|
-
|
|
205157
|
+
function isSqlserverConnectTimeout(error2, code) {
|
|
205158
|
+
return code === "ETIMEOUT" && error2.name === "ConnectionError";
|
|
205159
|
+
}
|
|
205160
|
+
function wrappedError(error2) {
|
|
205161
|
+
if (error2.cause instanceof Error)
|
|
205162
|
+
return error2.cause;
|
|
205163
|
+
return error2.originalError;
|
|
205164
|
+
}
|
|
205165
|
+
function causeChainHasTlsVerificationFailure(error2) {
|
|
205166
|
+
let current = wrappedError(error2);
|
|
205159
205167
|
for (let depth = 0;depth < 8 && current instanceof Error; depth++) {
|
|
205168
|
+
const { code } = current;
|
|
205169
|
+
if (typeof code === "string" && TLS_VERIFICATION_CODES.has(code)) {
|
|
205170
|
+
return true;
|
|
205171
|
+
}
|
|
205172
|
+
current = wrappedError(current);
|
|
205173
|
+
}
|
|
205174
|
+
return false;
|
|
205175
|
+
}
|
|
205176
|
+
function isConnectionFailure(error2) {
|
|
205177
|
+
for (const current of causeChain(error2)) {
|
|
205160
205178
|
const { code, fatal } = current;
|
|
205161
205179
|
if (typeof code === "string") {
|
|
205162
205180
|
if (NODE_CONNECTION_CODES.has(code))
|
|
205163
205181
|
return true;
|
|
205164
205182
|
if (POSTGRES_CONNECTION_SQLSTATE.test(code))
|
|
205165
205183
|
return true;
|
|
205184
|
+
if (SQLSERVER_CONNECTION_CODES.has(code) || isSqlserverConnectTimeout(current, code)) {
|
|
205185
|
+
return !causeChainHasTlsVerificationFailure(current);
|
|
205186
|
+
}
|
|
205166
205187
|
}
|
|
205167
205188
|
if (fatal === true && !isMysqlConfigFault(code))
|
|
205168
205189
|
return true;
|
|
@@ -205170,13 +205191,18 @@ function isConnectionFailure(error2) {
|
|
|
205170
205191
|
if (CODELESS_CONNECTION_MESSAGES.some((re) => re.test(message))) {
|
|
205171
205192
|
return true;
|
|
205172
205193
|
}
|
|
205173
|
-
current = current.cause;
|
|
205174
205194
|
}
|
|
205175
205195
|
return false;
|
|
205176
205196
|
}
|
|
205177
|
-
function
|
|
205197
|
+
function* causeChain(error2) {
|
|
205178
205198
|
let current = error2;
|
|
205179
205199
|
for (let depth = 0;depth < 8 && current instanceof Error; depth++) {
|
|
205200
|
+
yield current;
|
|
205201
|
+
current = wrappedError(current);
|
|
205202
|
+
}
|
|
205203
|
+
}
|
|
205204
|
+
function isCredentialRejection(error2) {
|
|
205205
|
+
for (const current of causeChain(error2)) {
|
|
205180
205206
|
const { code } = current;
|
|
205181
205207
|
if (typeof code === "string" || typeof code === "number") {
|
|
205182
205208
|
const value = String(code);
|
|
@@ -205184,12 +205210,13 @@ function isCredentialRejection(error2) {
|
|
|
205184
205210
|
return true;
|
|
205185
205211
|
if (MYSQL_AUTH_CODES.has(value))
|
|
205186
205212
|
return true;
|
|
205213
|
+
if (SQLSERVER_AUTH_CODES.has(value))
|
|
205214
|
+
return true;
|
|
205187
205215
|
if (SNOWFLAKE_AUTH_CODES.has(value))
|
|
205188
205216
|
return true;
|
|
205189
205217
|
}
|
|
205190
205218
|
if (current.name === "BigQueryAuthenticationError")
|
|
205191
205219
|
return true;
|
|
205192
|
-
current = current.cause;
|
|
205193
205220
|
}
|
|
205194
205221
|
return false;
|
|
205195
205222
|
}
|
|
@@ -205204,6 +205231,19 @@ function databaseAccessFailure(error2) {
|
|
|
205204
205231
|
return new ConnectionFailedError(message);
|
|
205205
205232
|
return;
|
|
205206
205233
|
}
|
|
205234
|
+
function statementRejection(error2) {
|
|
205235
|
+
for (const current of causeChain(error2)) {
|
|
205236
|
+
const { code, severity } = current;
|
|
205237
|
+
if (severity === "FATAL" || severity === "PANIC")
|
|
205238
|
+
return;
|
|
205239
|
+
if (typeof code !== "string" || !POSTGRES_STATEMENT_SQLSTATES.has(code)) {
|
|
205240
|
+
continue;
|
|
205241
|
+
}
|
|
205242
|
+
const message = current.message;
|
|
205243
|
+
return code === POSTGRES_UNDEFINED_TABLE && POSTGRES_RELATION_MISSING.test(message) ? new TableNotFoundError(message) : new InvalidArgumentError(message);
|
|
205244
|
+
}
|
|
205245
|
+
return;
|
|
205246
|
+
}
|
|
205207
205247
|
function describeFilesystemAccessFailure(errno) {
|
|
205208
205248
|
const code = errno.code ?? "";
|
|
205209
205249
|
const target = errno.path ? ` '${errno.path}'` : "";
|
|
@@ -205343,7 +205383,7 @@ function toQueryTextProblem(problem) {
|
|
|
205343
205383
|
...problem.at ? { at: { range: problem.at.range } } : {}
|
|
205344
205384
|
};
|
|
205345
205385
|
}
|
|
205346
|
-
var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", GENERIC_UNREACHABLE_MESSAGE = "The database connection is down: the database could not be reached, so the query did not run.", GENERIC_AUTH_MESSAGE = "The database rejected the connection's credentials. Check the connection's user, password, key or token.", MAX_LOGGED_DETAIL_CHARS = 2000, FILESYSTEM_ACCESS_DESCRIPTIONS, NODE_CONNECTION_CODES, POSTGRES_CONNECTION_SQLSTATE, CODELESS_CONNECTION_MESSAGES, MYSQL_CONFIG_CODES, POSTGRES_AUTH_SQLSTATE, MYSQL_AUTH_CODES, SNOWFLAKE_AUTH_CODES, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, InvalidModelPathError, RenderTagRefusedError, UnparseableTextError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, SkillNotFoundError, AgentNotFoundError, ConflictError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, ConnectionFailedError, UnconfiguredConnectionError, ConnectionPoolExhaustedError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, ChainedUpstreamMissingError, ChainedShapeNotCarriedError, PublisherConfigError, ServerConfigurationError, FrozenConfigError, AccessDeniedError, QueryCompileError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, WriteRollbackFailedError, WriteVerifyError, InvalidStateTransitionError, ServiceUnavailableError, PackageAdmissionRefusedError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
|
|
205386
|
+
var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", GENERIC_UNREACHABLE_MESSAGE = "The database connection is down: the database could not be reached, so the query did not run.", GENERIC_AUTH_MESSAGE = "The database rejected the connection's credentials. Check the connection's user, password, key or token.", MAX_LOGGED_DETAIL_CHARS = 2000, FILESYSTEM_ACCESS_DESCRIPTIONS, NODE_CONNECTION_CODES, POSTGRES_CONNECTION_SQLSTATE, CODELESS_CONNECTION_MESSAGES, MYSQL_CONFIG_CODES, SQLSERVER_CONNECTION_CODES, TLS_VERIFICATION_CODES, POSTGRES_AUTH_SQLSTATE, MYSQL_AUTH_CODES, SQLSERVER_AUTH_CODES, SNOWFLAKE_AUTH_CODES, POSTGRES_STATEMENT_SQLSTATES, POSTGRES_UNDEFINED_TABLE = "42P01", POSTGRES_RELATION_MISSING, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, InvalidModelPathError, RenderTagRefusedError, UnparseableTextError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, SkillNotFoundError, AgentNotFoundError, ConflictError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, ConnectionFailedError, UnconfiguredConnectionError, ConnectionPoolExhaustedError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, ChainedUpstreamMissingError, ChainedShapeNotCarriedError, PublisherConfigError, ServerConfigurationError, FrozenConfigError, AccessDeniedError, QueryCompileError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, WriteRollbackFailedError, WriteVerifyError, InvalidStateTransitionError, ServiceUnavailableError, PackageAdmissionRefusedError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
|
|
205347
205387
|
var init_errors = __esm(() => {
|
|
205348
205388
|
init_constants();
|
|
205349
205389
|
init_logger();
|
|
@@ -205369,7 +205409,8 @@ var init_errors = __esm(() => {
|
|
|
205369
205409
|
/^(Error: )?Connection lost: The server closed the connection\.$/,
|
|
205370
205410
|
new RegExp(`^(Error: )?(connect|read|write|getaddrinfo) (${[
|
|
205371
205411
|
...NODE_CONNECTION_CODES
|
|
205372
|
-
].join("|")})\\b`)
|
|
205412
|
+
].join("|")})\\b`),
|
|
205413
|
+
/^Failed to connect to \S+( \(redirected from \S+\))?( in \d+ms| - (?!.*certificate).+)$/i
|
|
205373
205414
|
];
|
|
205374
205415
|
MYSQL_CONFIG_CODES = new Set([
|
|
205375
205416
|
"ER_ACCESS_DENIED_ERROR",
|
|
@@ -205378,9 +205419,46 @@ var init_errors = __esm(() => {
|
|
|
205378
205419
|
"AUTH_SWITCH_PLUGIN_ERROR",
|
|
205379
205420
|
"MYSQL_CLEAR_PASSWORD_NOT_ENABLED"
|
|
205380
205421
|
]);
|
|
205422
|
+
SQLSERVER_CONNECTION_CODES = new Set(["ESOCKET", "EINSTLOOKUP"]);
|
|
205423
|
+
TLS_VERIFICATION_CODES = new Set([
|
|
205424
|
+
"CERT_HAS_EXPIRED",
|
|
205425
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
205426
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
205427
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
205428
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
205429
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
205430
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE"
|
|
205431
|
+
]);
|
|
205381
205432
|
POSTGRES_AUTH_SQLSTATE = /^28[0-9A-Z]{3}$/;
|
|
205382
205433
|
MYSQL_AUTH_CODES = new Set(["ER_ACCESS_DENIED_ERROR"]);
|
|
205434
|
+
SQLSERVER_AUTH_CODES = new Set(["ELOGIN"]);
|
|
205383
205435
|
SNOWFLAKE_AUTH_CODES = new Set(["390100", "390144", "390195", "390318"]);
|
|
205436
|
+
POSTGRES_STATEMENT_SQLSTATES = new Set([
|
|
205437
|
+
"42601",
|
|
205438
|
+
"42703",
|
|
205439
|
+
"42P01",
|
|
205440
|
+
"42883",
|
|
205441
|
+
"42704",
|
|
205442
|
+
"42P02",
|
|
205443
|
+
"42702",
|
|
205444
|
+
"42725",
|
|
205445
|
+
"42P09",
|
|
205446
|
+
"42P10",
|
|
205447
|
+
"42804",
|
|
205448
|
+
"42846",
|
|
205449
|
+
"42803",
|
|
205450
|
+
"42P20",
|
|
205451
|
+
"42P18",
|
|
205452
|
+
"42P19",
|
|
205453
|
+
"42712",
|
|
205454
|
+
"42701",
|
|
205455
|
+
"42602",
|
|
205456
|
+
"42622",
|
|
205457
|
+
"42809",
|
|
205458
|
+
"42P21",
|
|
205459
|
+
"42P22"
|
|
205460
|
+
]);
|
|
205461
|
+
POSTGRES_RELATION_MISSING = /^relation ".*" does not exist$/;
|
|
205384
205462
|
NotImplementedError = class NotImplementedError extends Error {
|
|
205385
205463
|
constructor(message) {
|
|
205386
205464
|
super(message);
|
|
@@ -262101,6 +262179,11 @@ var require_utils93 = __commonJS((exports, module) => {
|
|
|
262101
262179
|
};
|
|
262102
262180
|
});
|
|
262103
262181
|
|
|
262182
|
+
// ../../node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
|
|
262183
|
+
var require_sshcrypto = __commonJS((exports, module) => {
|
|
262184
|
+
module.exports = __require("./sshcrypto-cqb52edz.node");
|
|
262185
|
+
});
|
|
262186
|
+
|
|
262104
262187
|
// ../../node_modules/ssh2/lib/protocol/crypto/poly1305.js
|
|
262105
262188
|
var require_poly1305 = __commonJS((exports, module) => {
|
|
262106
262189
|
var __dirname = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto", __filename = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto/poly1305.js";
|
|
@@ -262587,7 +262670,7 @@ var require_crypto = __commonJS((exports, module) => {
|
|
|
262587
262670
|
var ChaChaPolyDecipher;
|
|
262588
262671
|
var GenericDecipher;
|
|
262589
262672
|
try {
|
|
262590
|
-
binding = (
|
|
262673
|
+
binding = require_sshcrypto();
|
|
262591
262674
|
({
|
|
262592
262675
|
AESGCMCipher,
|
|
262593
262676
|
ChaChaPolyCipher,
|
|
@@ -337127,7 +337210,7 @@ ${body}`;
|
|
|
337127
337210
|
if (artifactAt < 0) {
|
|
337128
337211
|
return refuse({
|
|
337129
337212
|
line: 1,
|
|
337130
|
-
message: "Line 1: this notebook has no ## artifact note, so its header cannot be told from its cells. Fix: add `## artifact { layout=linear }` after the ##! line."
|
|
337213
|
+
message: "Line 1: this notebook has no ## artifact note, so its header cannot be told from its cells. Fix: add `## artifact { layout=linear tiles=[…] }` after the ##! line."
|
|
337131
337214
|
});
|
|
337132
337215
|
}
|
|
337133
337216
|
const runCount = items.filter((item) => item.kind === "statement" && item.run).length;
|
|
@@ -337327,12 +337410,14 @@ function readReportTag(modelNotes) {
|
|
|
337327
337410
|
const kind = tagText(artifact, "kind");
|
|
337328
337411
|
if (kind !== undefined)
|
|
337329
337412
|
return { status: "kind-removed", kind };
|
|
337330
|
-
const
|
|
337331
|
-
if (
|
|
337332
|
-
return { status: "
|
|
337333
|
-
|
|
337334
|
-
|
|
337335
|
-
|
|
337413
|
+
const hasTiles = artifact.has("tiles");
|
|
337414
|
+
if (hasTiles && artifact.array("tiles") === undefined)
|
|
337415
|
+
return { status: "unparsed" };
|
|
337416
|
+
const declared = tagText(artifact, "layout");
|
|
337417
|
+
const layout = parseLayoutValue(declared) ?? (hasTiles || tags?.tag("dashboard") !== undefined ? "grid" : "linear");
|
|
337418
|
+
if (layout === "linear" && !hasTiles)
|
|
337419
|
+
return declared !== undefined && parseLayoutValue(declared) === undefined ? { status: "cells-removed", unknownLayout: declared } : { status: "cells-removed" };
|
|
337420
|
+
return { status: "report", layout };
|
|
337336
337421
|
}
|
|
337337
337422
|
function readReportTagOfText(text) {
|
|
337338
337423
|
return readReportTag(modelNotesOfText(text));
|
|
@@ -338981,6 +339066,7 @@ import {
|
|
|
338981
339066
|
buildPoolOptions,
|
|
338982
339067
|
SnowflakeConnection
|
|
338983
339068
|
} from "@malloydata/db-snowflake";
|
|
339069
|
+
import"@malloydata/db-sqlserver";
|
|
338984
339070
|
import"@malloydata/db-trino";
|
|
338985
339071
|
import {
|
|
338986
339072
|
contextOverlay,
|
|
@@ -344265,6 +344351,7 @@ var DOUBLED_ESCAPE_DIALECTS = new Set([
|
|
|
344265
344351
|
"motherduck",
|
|
344266
344352
|
"postgres",
|
|
344267
344353
|
"publisher",
|
|
344354
|
+
"sqlserver",
|
|
344268
344355
|
"trino"
|
|
344269
344356
|
]);
|
|
344270
344357
|
function sqlLiteral(value, connectionType) {
|
|
@@ -345117,6 +345204,13 @@ function getStaticConnectionAttributes(type) {
|
|
|
345117
345204
|
canPersist: true,
|
|
345118
345205
|
canStream: false
|
|
345119
345206
|
};
|
|
345207
|
+
case "sqlserver":
|
|
345208
|
+
return {
|
|
345209
|
+
dialectName: "sqlserver",
|
|
345210
|
+
isPool: true,
|
|
345211
|
+
canPersist: false,
|
|
345212
|
+
canStream: true
|
|
345213
|
+
};
|
|
345120
345214
|
case "duckdb":
|
|
345121
345215
|
case "motherduck":
|
|
345122
345216
|
case "ducklake":
|
|
@@ -345263,6 +345357,20 @@ function validateConnectionShape(connection) {
|
|
|
345263
345357
|
case "postgres":
|
|
345264
345358
|
case "mysql":
|
|
345265
345359
|
break;
|
|
345360
|
+
case "sqlserver": {
|
|
345361
|
+
const sqlserver = connection.sqlserverConnection;
|
|
345362
|
+
if (!sqlserver) {
|
|
345363
|
+
throw new Error("SQL Server connection configuration is missing.");
|
|
345364
|
+
}
|
|
345365
|
+
const port = sqlserver.port;
|
|
345366
|
+
if (port !== undefined && port !== null && (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)) {
|
|
345367
|
+
throw new Error(`Connection '${connection.name}' has an invalid sqlserverConnection.port ` + `${JSON.stringify(port)} (expected an integer from 1 to 65535).`);
|
|
345368
|
+
}
|
|
345369
|
+
if (sqlserver.instanceName && sqlserver.port) {
|
|
345370
|
+
logger.warn(`Connection '${connection.name}' sets both sqlserverConnection.instanceName ` + `and port; the instance is located through SQL Browser and the port is not used.`);
|
|
345371
|
+
}
|
|
345372
|
+
break;
|
|
345373
|
+
}
|
|
345266
345374
|
case "bigquery": {
|
|
345267
345375
|
const bigquery = connection.bigqueryConnection;
|
|
345268
345376
|
if (bigquery?.impersonateServiceAccount) {
|
|
@@ -345510,6 +345618,9 @@ function canonicalizeJsonValue(value) {
|
|
|
345510
345618
|
}
|
|
345511
345619
|
return sorted;
|
|
345512
345620
|
}
|
|
345621
|
+
function blankAsUnset(value) {
|
|
345622
|
+
return value === "" ? undefined : value;
|
|
345623
|
+
}
|
|
345513
345624
|
function assembleEnvironmentConnections(connections = [], environmentPath = "") {
|
|
345514
345625
|
const pojo = { connections: {} };
|
|
345515
345626
|
const metadata = new Map;
|
|
@@ -345572,6 +345683,22 @@ function assembleEnvironmentConnections(connections = [], environmentPath = "")
|
|
|
345572
345683
|
};
|
|
345573
345684
|
break;
|
|
345574
345685
|
}
|
|
345686
|
+
case "sqlserver": {
|
|
345687
|
+
const sqlserver = connection.sqlserverConnection;
|
|
345688
|
+
pojo.connections[connection.name] = {
|
|
345689
|
+
is: "sqlserver",
|
|
345690
|
+
server: sqlserver?.server,
|
|
345691
|
+
port: sqlserver?.port,
|
|
345692
|
+
database: sqlserver?.database,
|
|
345693
|
+
user: sqlserver?.user,
|
|
345694
|
+
password: sqlserver?.password,
|
|
345695
|
+
encrypt: sqlserver?.encrypt,
|
|
345696
|
+
trustServerCertificate: sqlserver?.trustServerCertificate,
|
|
345697
|
+
hostNameInCertificate: blankAsUnset(sqlserver?.hostNameInCertificate),
|
|
345698
|
+
instanceName: blankAsUnset(sqlserver?.instanceName)
|
|
345699
|
+
};
|
|
345700
|
+
break;
|
|
345701
|
+
}
|
|
345575
345702
|
case "bigquery": {
|
|
345576
345703
|
const serviceAccountKey = parseServiceAccountKey(connection.bigqueryConnection?.serviceAccountKeyJson);
|
|
345577
345704
|
const impersonateServiceAccount = connection.bigqueryConnection?.impersonateServiceAccount;
|
|
@@ -346036,7 +346163,7 @@ function mergeStatementTimeoutOption(existing, ms) {
|
|
|
346036
346163
|
}
|
|
346037
346164
|
function conninfoPairs(conninfo) {
|
|
346038
346165
|
const pairs = [];
|
|
346039
|
-
const isSpace = (c) => c !== undefined &&
|
|
346166
|
+
const isSpace = (c) => c !== undefined && /[ \t\n\v\f\r]/.test(c);
|
|
346040
346167
|
let i2 = 0;
|
|
346041
346168
|
while (i2 < conninfo.length) {
|
|
346042
346169
|
while (isSpace(conninfo[i2]))
|
|
@@ -346082,7 +346209,66 @@ function decodeUriComponentOrRaw(value) {
|
|
|
346082
346209
|
return value;
|
|
346083
346210
|
}
|
|
346084
346211
|
}
|
|
346085
|
-
|
|
346212
|
+
var LIBPQ_NO_VERIFY_SSLMODE = "require";
|
|
346213
|
+
var isUriConnectionString = (connectionString) => /^postgres(ql)?:\/\//i.test(connectionString);
|
|
346214
|
+
function mapUriQuery(uri, fn) {
|
|
346215
|
+
const queryStart = uri.indexOf("?");
|
|
346216
|
+
const base = queryStart === -1 ? uri : uri.slice(0, queryStart);
|
|
346217
|
+
const params = fn(queryStart === -1 ? [] : uri.slice(queryStart + 1).split("&"));
|
|
346218
|
+
return params.length ? `${base}?${params.join("&")}` : base;
|
|
346219
|
+
}
|
|
346220
|
+
function libpqSslmodeInRawString(connectionString) {
|
|
346221
|
+
const envNoVerify = process.env.PGSSLMODE?.toLowerCase() === "no-verify" && !process.env.PGSERVICE;
|
|
346222
|
+
const rewritten = `sslmode=${LIBPQ_NO_VERIFY_SSLMODE}`;
|
|
346223
|
+
if (isUriConnectionString(connectionString)) {
|
|
346224
|
+
return mapUriQuery(connectionString, (params) => {
|
|
346225
|
+
const keys = params.map((param) => decodeUriComponentOrRaw(param.split("=")[0]));
|
|
346226
|
+
const hasSslmode = keys.includes("sslmode");
|
|
346227
|
+
const hasCert = keys.some((key) => ["sslrootcert", "sslcert", "sslkey"].includes(key));
|
|
346228
|
+
let seen = false;
|
|
346229
|
+
const mapped = params.flatMap((param, i2) => {
|
|
346230
|
+
const [, ...rest] = param.split("=");
|
|
346231
|
+
const value = decodeUriComponentOrRaw(rest.join("="));
|
|
346232
|
+
if (keys[i2] === "service")
|
|
346233
|
+
seen = true;
|
|
346234
|
+
if (keys[i2] === "uselibpqcompat")
|
|
346235
|
+
return [];
|
|
346236
|
+
if (keys[i2] === "ssl") {
|
|
346237
|
+
if (hasSslmode)
|
|
346238
|
+
return [];
|
|
346239
|
+
if (value === "no-verify") {
|
|
346240
|
+
seen = true;
|
|
346241
|
+
return [rewritten];
|
|
346242
|
+
}
|
|
346243
|
+
if (value === "1")
|
|
346244
|
+
return ["ssl=true"];
|
|
346245
|
+
if (value === "0") {
|
|
346246
|
+
if (hasCert)
|
|
346247
|
+
return [];
|
|
346248
|
+
seen = true;
|
|
346249
|
+
return ["sslmode=disable"];
|
|
346250
|
+
}
|
|
346251
|
+
return [param];
|
|
346252
|
+
}
|
|
346253
|
+
if (keys[i2] !== "sslmode")
|
|
346254
|
+
return [param];
|
|
346255
|
+
seen = true;
|
|
346256
|
+
return [value === "no-verify" ? rewritten : param];
|
|
346257
|
+
});
|
|
346258
|
+
return !seen && envNoVerify ? [...mapped, rewritten] : mapped;
|
|
346259
|
+
});
|
|
346260
|
+
}
|
|
346261
|
+
const pairs = conninfoPairs(connectionString);
|
|
346262
|
+
let result = connectionString;
|
|
346263
|
+
for (const pair of [...pairs].reverse()) {
|
|
346264
|
+
if (pair.key === "sslmode" && pair.value === "no-verify") {
|
|
346265
|
+
result = result.slice(0, pair.start) + rewritten + result.slice(pair.end);
|
|
346266
|
+
}
|
|
346267
|
+
}
|
|
346268
|
+
return !pairs.some((pair) => pair.key === "sslmode" || pair.key === "service") && envNoVerify ? `${result} ${rewritten}` : result;
|
|
346269
|
+
}
|
|
346270
|
+
function libpqConnectionString(name, rawConnectionString, pg) {
|
|
346271
|
+
const connectionString = libpqSslmodeInRawString(rawConnectionString);
|
|
346086
346272
|
const ms = postgresStatementTimeoutMs(name, pg);
|
|
346087
346273
|
if (ms === undefined)
|
|
346088
346274
|
return connectionString;
|
|
@@ -346095,21 +346281,21 @@ function withLibpqStatementTimeout(name, connectionString, pg) {
|
|
|
346095
346281
|
}
|
|
346096
346282
|
return value;
|
|
346097
346283
|
};
|
|
346098
|
-
if (
|
|
346099
|
-
const queryStart = connectionString.indexOf("?");
|
|
346100
|
-
if (queryStart === -1) {
|
|
346101
|
-
return `${connectionString}?options=${encodeURIComponent(merged(undefined))}`;
|
|
346102
|
-
}
|
|
346103
|
-
const params = connectionString.slice(queryStart + 1).split("&");
|
|
346284
|
+
if (isUriConnectionString(connectionString)) {
|
|
346104
346285
|
const isOptions = (param) => decodeUriComponentOrRaw(param.split("=", 1)[0]) === "options";
|
|
346105
|
-
|
|
346106
|
-
|
|
346107
|
-
|
|
346108
|
-
|
|
346286
|
+
return mapUriQuery(connectionString, (params) => {
|
|
346287
|
+
const last = params.map(isOptions).lastIndexOf(true);
|
|
346288
|
+
if (last === -1) {
|
|
346289
|
+
return [
|
|
346290
|
+
...params,
|
|
346291
|
+
`options=${encodeURIComponent(merged(undefined))}`
|
|
346292
|
+
];
|
|
346293
|
+
}
|
|
346109
346294
|
const raw = params[last].slice(params[last].indexOf("=") + 1);
|
|
346110
|
-
|
|
346111
|
-
|
|
346112
|
-
|
|
346295
|
+
const out = [...params];
|
|
346296
|
+
out[last] = `options=${encodeURIComponent(merged(decodeUriComponentOrRaw(raw)))}`;
|
|
346297
|
+
return out;
|
|
346298
|
+
});
|
|
346113
346299
|
}
|
|
346114
346300
|
const existing = conninfoPairs(connectionString).filter((pair) => pair.key === "options").pop();
|
|
346115
346301
|
if (!existing) {
|
|
@@ -346122,12 +346308,13 @@ function buildPgConnectionString(pg, {
|
|
|
346122
346308
|
applyConnectionOptions
|
|
346123
346309
|
}) {
|
|
346124
346310
|
if (pg.connectionString) {
|
|
346125
|
-
if (!applyConnectionOptions)
|
|
346126
|
-
return pg.connectionString;
|
|
346311
|
+
if (!applyConnectionOptions) {
|
|
346312
|
+
return libpqSslmodeInRawString(pg.connectionString);
|
|
346313
|
+
}
|
|
346127
346314
|
if (pg.sslmode != null) {
|
|
346128
346315
|
logger.warn(`Connection '${name}' sets both postgresConnection.sslmode and ` + `connectionString; sslmode is ignored and the connectionString's own sslmode applies.`);
|
|
346129
346316
|
}
|
|
346130
|
-
return
|
|
346317
|
+
return libpqConnectionString(name, pg.connectionString, pg);
|
|
346131
346318
|
}
|
|
346132
346319
|
const parts = [];
|
|
346133
346320
|
if (pg.host)
|
|
@@ -346150,7 +346337,7 @@ function buildPgConnectionString(pg, {
|
|
|
346150
346337
|
const pgSSLMode = process.env.PGSSLMODE;
|
|
346151
346338
|
if (pgSSLMode) {
|
|
346152
346339
|
const mapping = {
|
|
346153
|
-
"no-verify":
|
|
346340
|
+
"no-verify": LIBPQ_NO_VERIFY_SSLMODE,
|
|
346154
346341
|
disable: "disable",
|
|
346155
346342
|
allow: "allow",
|
|
346156
346343
|
prefer: "prefer",
|
|
@@ -346519,7 +346706,7 @@ function libpqSslmodeParts(subject, mode, trust) {
|
|
|
346519
346706
|
case "disable":
|
|
346520
346707
|
return ["sslmode=disable"];
|
|
346521
346708
|
case "no-verify":
|
|
346522
|
-
return [
|
|
346709
|
+
return [`sslmode=${LIBPQ_NO_VERIFY_SSLMODE}`];
|
|
346523
346710
|
case "verify-ca": {
|
|
346524
346711
|
if (!trust.caBundle) {
|
|
346525
346712
|
throw new Error(`${subject} uses sslmode 'verify-ca' but no trusted CA bundle is available (NODE_EXTRA_CA_CERTS is unset).`);
|
|
@@ -347445,6 +347632,16 @@ var MYSQL = {
|
|
|
347445
347632
|
database: "scalar",
|
|
347446
347633
|
user: "scalar"
|
|
347447
347634
|
};
|
|
347635
|
+
var SQLSERVER = {
|
|
347636
|
+
server: "scalar",
|
|
347637
|
+
port: "scalar",
|
|
347638
|
+
database: "scalar",
|
|
347639
|
+
user: "scalar",
|
|
347640
|
+
encrypt: "scalar",
|
|
347641
|
+
trustServerCertificate: "scalar",
|
|
347642
|
+
hostNameInCertificate: "scalar",
|
|
347643
|
+
instanceName: "scalar"
|
|
347644
|
+
};
|
|
347448
347645
|
var MOTHERDUCK = { database: "scalar" };
|
|
347449
347646
|
var PUBLISHER = { connectionUri: "scalar" };
|
|
347450
347647
|
var S3 = {
|
|
@@ -347510,6 +347707,7 @@ var PUBLIC_CONNECTION = {
|
|
|
347510
347707
|
trinoConnection: TRINO,
|
|
347511
347708
|
databricksConnection: DATABRICKS,
|
|
347512
347709
|
mysqlConnection: MYSQL,
|
|
347710
|
+
sqlserverConnection: SQLSERVER,
|
|
347513
347711
|
duckdbConnection: DUCKDB,
|
|
347514
347712
|
motherduckConnection: MOTHERDUCK,
|
|
347515
347713
|
ducklakeConnection: DUCKLAKE,
|
|
@@ -347523,6 +347721,7 @@ var PUBLIC_FIELDS_BY_SCHEMA = {
|
|
|
347523
347721
|
TrinoConnection: Object.keys(TRINO),
|
|
347524
347722
|
DatabricksConnection: Object.keys(DATABRICKS),
|
|
347525
347723
|
MysqlConnection: Object.keys(MYSQL),
|
|
347724
|
+
SqlserverConnection: Object.keys(SQLSERVER),
|
|
347526
347725
|
DuckdbConnection: Object.keys(DUCKDB),
|
|
347527
347726
|
MotherDuckConnection: Object.keys(MOTHERDUCK),
|
|
347528
347727
|
DucklakeConnection: Object.keys(DUCKLAKE),
|
|
@@ -348015,6 +348214,27 @@ async function getSchemasForPostgres(connection, malloyConnection) {
|
|
|
348015
348214
|
throw new Error(`Failed to get schemas for Postgres connection ${connection.name}: ${error2.message}`, { cause: error2 });
|
|
348016
348215
|
}
|
|
348017
348216
|
}
|
|
348217
|
+
async function getSchemasForSQLServer(connection, malloyConnection) {
|
|
348218
|
+
if (!connection.sqlserverConnection) {
|
|
348219
|
+
throw new Error("SQL Server connection is required");
|
|
348220
|
+
}
|
|
348221
|
+
try {
|
|
348222
|
+
const result = await runIntrospectionSQL(malloyConnection, "SELECT DISTINCT table_schema AS schema_name FROM information_schema.tables ORDER BY table_schema");
|
|
348223
|
+
const rows = standardizeRunSQLResult2(result);
|
|
348224
|
+
return rows.map((row) => {
|
|
348225
|
+
const typedRow = row;
|
|
348226
|
+
const schemaName = String(typedRow.schema_name ?? typedRow.SCHEMA_NAME ?? "");
|
|
348227
|
+
return {
|
|
348228
|
+
name: schemaName,
|
|
348229
|
+
isHidden: false,
|
|
348230
|
+
isDefault: schemaName === "dbo"
|
|
348231
|
+
};
|
|
348232
|
+
});
|
|
348233
|
+
} catch (error2) {
|
|
348234
|
+
logger.error(`Error getting schemas for SQL Server connection ${connection.name}`, { error: error2 });
|
|
348235
|
+
throw new Error(`Failed to get schemas for SQL Server connection ${connection.name}: ${error2.message}`, { cause: error2 });
|
|
348236
|
+
}
|
|
348237
|
+
}
|
|
348018
348238
|
async function getSchemasForMySQL(connection) {
|
|
348019
348239
|
if (!connection.mysqlConnection) {
|
|
348020
348240
|
throw new Error("Mysql connection is required");
|
|
@@ -348364,6 +348584,8 @@ async function getSchemasForConnection(connection, malloyConnection) {
|
|
|
348364
348584
|
return getSchemasForPostgres(connection, malloyConnection);
|
|
348365
348585
|
case "mysql":
|
|
348366
348586
|
return getSchemasForMySQL(connection);
|
|
348587
|
+
case "sqlserver":
|
|
348588
|
+
return getSchemasForSQLServer(connection, malloyConnection);
|
|
348367
348589
|
case "snowflake":
|
|
348368
348590
|
return getSchemasForSnowflake(connection, malloyConnection);
|
|
348369
348591
|
case "trino":
|
|
@@ -348551,6 +348773,8 @@ async function listTablesForSchema(connection, schemaName, malloyConnection, tab
|
|
|
348551
348773
|
return listTablesForMySQL(connection, schemaName, malloyConnection, tableNames);
|
|
348552
348774
|
case "postgres":
|
|
348553
348775
|
return listTablesForPostgres(connection, schemaName, malloyConnection, tableNames);
|
|
348776
|
+
case "sqlserver":
|
|
348777
|
+
return listTablesForSQLServer(connection, schemaName, malloyConnection, tableNames);
|
|
348554
348778
|
case "snowflake":
|
|
348555
348779
|
return listTablesForSnowflake(connection, schemaName, malloyConnection, tableNames);
|
|
348556
348780
|
case "trino":
|
|
@@ -348619,6 +348843,27 @@ async function listTablesForMySQL(connection, schemaName, malloyConnection, tabl
|
|
|
348619
348843
|
throw new Error(`Failed to get tables for MySQL schema ${schemaName} in connection ${connection.name}: ${error2.message}`, { cause: error2 });
|
|
348620
348844
|
}
|
|
348621
348845
|
}
|
|
348846
|
+
function sqlserverNLiteral(value) {
|
|
348847
|
+
return `N'${sqlLiteral(value, "sqlserver")}'`;
|
|
348848
|
+
}
|
|
348849
|
+
function sqlserverInFilter(columnName, values) {
|
|
348850
|
+
if (!values || values.length === 0)
|
|
348851
|
+
return "";
|
|
348852
|
+
return `AND ${columnName} IN (${values.map(sqlserverNLiteral).join(", ")})`;
|
|
348853
|
+
}
|
|
348854
|
+
async function listTablesForSQLServer(connection, schemaName, malloyConnection, tableNames) {
|
|
348855
|
+
if (!connection.sqlserverConnection) {
|
|
348856
|
+
throw new Error("SQL Server connection is required");
|
|
348857
|
+
}
|
|
348858
|
+
try {
|
|
348859
|
+
const result = await runIntrospectionSQL(malloyConnection, `SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = ${sqlserverNLiteral(schemaName)} ${sqlserverInFilter("table_name", tableNames)} ORDER BY table_name, ordinal_position`);
|
|
348860
|
+
const rows = standardizeRunSQLResult2(result);
|
|
348861
|
+
return groupColumnRowsIntoTables(rows, (t2) => `${schemaName}.${t2}`);
|
|
348862
|
+
} catch (error2) {
|
|
348863
|
+
logger.error(`Error getting tables for SQL Server schema ${schemaName} in connection ${connection.name}`, { error: error2 });
|
|
348864
|
+
throw new Error(`Failed to get tables for SQL Server schema ${schemaName} in connection ${connection.name}: ${error2.message}`, { cause: error2 });
|
|
348865
|
+
}
|
|
348866
|
+
}
|
|
348622
348867
|
async function listTablesForPostgres(connection, schemaName, malloyConnection, tableNames) {
|
|
348623
348868
|
if (!connection.postgresConnection) {
|
|
348624
348869
|
throw new Error("Postgres connection is required");
|
|
@@ -348939,12 +349184,12 @@ function classifyDriverFailure(error2) {
|
|
|
348939
349184
|
return error2;
|
|
348940
349185
|
}
|
|
348941
349186
|
const message = error2 instanceof Error ? error2.message : typeof error2 === "string" ? error2 : JSON.stringify(error2);
|
|
348942
|
-
return databaseAccessFailure(error2) ?? driverErrorToPublisherError(message);
|
|
349187
|
+
return databaseAccessFailure(error2) ?? statementRejection(error2) ?? driverErrorToPublisherError(message);
|
|
348943
349188
|
}
|
|
348944
349189
|
function sqlRunFailure(error2) {
|
|
348945
349190
|
if (error2 instanceof ConnectionError)
|
|
348946
349191
|
return error2;
|
|
348947
|
-
return databaseAccessFailure(error2) ?? new ConnectionError(error2.message);
|
|
349192
|
+
return databaseAccessFailure(error2) ?? statementRejection(error2) ?? new ConnectionError(error2.message);
|
|
348948
349193
|
}
|
|
348949
349194
|
async function withDatabaseAccessClassified(list) {
|
|
348950
349195
|
try {
|
|
@@ -349034,8 +349279,11 @@ class ConnectionController {
|
|
|
349034
349279
|
throw new TableNotFoundError(`Table ${tablePath} not found`);
|
|
349035
349280
|
}
|
|
349036
349281
|
if (typeof source === "string") {
|
|
349037
|
-
|
|
349038
|
-
|
|
349282
|
+
if (source === `Table ${tablePath} not found`) {
|
|
349283
|
+
throw new TableNotFoundError(source);
|
|
349284
|
+
}
|
|
349285
|
+
const driverMessage = lookupFailureDriverMessage(source, tablePath);
|
|
349286
|
+
const accessFailure = driverMessage === undefined ? undefined : databaseAccessFailure(new Error(driverMessage));
|
|
349039
349287
|
throw accessFailure ?? driverErrorToPublisherError(source);
|
|
349040
349288
|
}
|
|
349041
349289
|
return {
|
|
@@ -349314,6 +349562,16 @@ class ConnectionController {
|
|
|
349314
349562
|
};
|
|
349315
349563
|
}
|
|
349316
349564
|
}
|
|
349565
|
+
function lookupFailureDriverMessage(source, tablePath) {
|
|
349566
|
+
const prefix = `Error fetching schema for ${tablePath}: `;
|
|
349567
|
+
if (source.startsWith(prefix))
|
|
349568
|
+
return source.slice(prefix.length);
|
|
349569
|
+
const sqlserverPrefix = `Error fetching schema for table ${tablePath}: `;
|
|
349570
|
+
if (source.startsWith(sqlserverPrefix)) {
|
|
349571
|
+
return source.slice(sqlserverPrefix.length).replace(/^\d+: /, "");
|
|
349572
|
+
}
|
|
349573
|
+
return;
|
|
349574
|
+
}
|
|
349317
349575
|
|
|
349318
349576
|
// src/controller/report.controller.ts
|
|
349319
349577
|
init_errors();
|
|
@@ -349368,6 +349626,19 @@ function recordPackageLoadPhases(timings, status) {
|
|
|
349368
349626
|
schemaFetchDuration?.record(timings.schemaFetchDurationMs, attrs);
|
|
349369
349627
|
schemaFetches?.record(timings.schemaFetchCount, attrs);
|
|
349370
349628
|
}
|
|
349629
|
+
var phaseDuration = null;
|
|
349630
|
+
function recordPackageLoadPhaseDurations(durations) {
|
|
349631
|
+
phaseDuration ??= publisherMeter().createHistogram("publisher_package_load_phase_duration_ms", {
|
|
349632
|
+
description: "Per-load wall-clock time of each stage of a successful package load, main-thread stages included. Label: phase (worker, hydrate, render_validation, discovery, lint, build_plan, total).",
|
|
349633
|
+
unit: "ms",
|
|
349634
|
+
advice: { explicitBucketBoundaries: LOAD_DURATION_BUCKETS_MS }
|
|
349635
|
+
});
|
|
349636
|
+
for (const [phase, ms] of Object.entries(durations)) {
|
|
349637
|
+
if (ms === undefined)
|
|
349638
|
+
continue;
|
|
349639
|
+
phaseDuration.record(ms, { phase });
|
|
349640
|
+
}
|
|
349641
|
+
}
|
|
349371
349642
|
|
|
349372
349643
|
// src/report_metrics.ts
|
|
349373
349644
|
var resetHooks2 = [];
|
|
@@ -351424,7 +351695,7 @@ with both queries. Otherwise it is still \`NOT-RETURNED\` / \`LOW-RANK\`.
|
|
|
351424
351695
|
Use when the entity was found and used, and the definition or the data behind it
|
|
351425
351696
|
is wrong (bad grain, wrong join key, inverted filter). Owner: model. A doc whose
|
|
351426
351697
|
factual claim the data contradicts (a population statement, a grain claim) is
|
|
351427
|
-
also model-definition: the
|
|
351698
|
+
also model-definition: the field may compute correctly while its doc is false,
|
|
351428
351699
|
and an agent that trusts the doc answers wrongly without ever failing a query.
|
|
351429
351700
|
Probe the claim before writing the issue.
|
|
351430
351701
|
|
|
@@ -352138,7 +352409,7 @@ trusting a name that looks familiar, and write down the mapping you used in
|
|
|
352138
352409
|
|---|---|---|
|
|
352139
352410
|
| \`prompt\`, \`query\`, \`input\`, \`nl_question\` | the question | \`query\` is the trap: in half of these it is the SQL or Malloy they ran, not the question |
|
|
352140
352411
|
| \`answer\`, \`expected\`, \`output\`, \`ground_truth\` | a golden value, prose, or both mixed in one string | a paragraph under \`answer\` is criteria, not a value. Classify it, do not store it as a number |
|
|
352141
|
-
| \`sql\`, \`malloy\`, \`gold_query\` | their derivation | run it (\`SKILL.md\` step 3).
|
|
352412
|
+
| \`sql\`, \`malloy\`, \`gold_query\` | their derivation | Malloy: run it (\`SKILL.md\` step 3). SQL: keep it as evidence of how they got the number, but do not run it; derive the number in Malloy over the truth package instead. Either way, this is the field that upgrades a typed number into something checkable |
|
|
352142
352413
|
| \`criteria\`, \`rubric\`, \`assertions\`, \`must_include\` | prose criteria | apply the query test in \`SKILL.md\` step 4 to EACH one, not to the field |
|
|
352143
352414
|
| \`id\`, \`case_id\`, \`uuid\` | their id | keep it, prefixed. Two sets from one customer collide otherwise |
|
|
352144
352415
|
| \`difficulty\`, \`category\`, \`tags\` | their own labels | keep as-is on the case for slicing. Never let one become \`split\` |
|
|
@@ -352301,7 +352572,7 @@ data with no question in front of them?* Reject:
|
|
|
352301
352572
|
|
|
352302
352573
|
- a field that hard-codes this question's filter and serves no other question
|
|
352303
352574
|
- this question's text, qid, or expected numbers in a doc, comment, or name
|
|
352304
|
-
- a join copied from
|
|
352575
|
+
- a join copied from a golden's query that you have not probed as a real relationship
|
|
352305
352576
|
|
|
352306
352577
|
The golden is a hypothesis source. The data is still the verifier.
|
|
352307
352578
|
|
|
@@ -352310,12 +352581,16 @@ The golden is a hypothesis source. The data is still the verifier.
|
|
|
352310
352581
|
Every structural claim needs a query you ran: join key, primary key, filter
|
|
352311
352582
|
value existence, snapshot assumption, value space, cardinality.
|
|
352312
352583
|
|
|
352313
|
-
\`\`\`
|
|
352314
|
-
|
|
352315
|
-
|
|
352316
|
-
|
|
352584
|
+
\`\`\`malloy
|
|
352585
|
+
run: t -> { aggregate: row_count is count(), distinct_keys is count(col) }
|
|
352586
|
+
run: b -> { group_by: k; aggregate: n is count(); order_by: n desc; limit: 5 }
|
|
352587
|
+
run: t -> { group_by: col; aggregate: n is count(); order_by: n desc; limit: 5 }
|
|
352317
352588
|
\`\`\`
|
|
352318
352589
|
|
|
352590
|
+
Run probes in Malloy, against the raw tables (the truth package, or a bare
|
|
352591
|
+
\`connection.table()\` source), never as SQL. The second line is the join probe:
|
|
352592
|
+
any \`n\` above 1 means \`k\` is not a key on \`b\`, so a \`join_one\` on it fans out.
|
|
352593
|
+
|
|
352319
352594
|
A false \`primary_key\` compiles and silently corrupts every aggregate. Of one
|
|
352320
352595
|
pilot's 11 accepted edits, 4 of 5 wrong ones died to a single
|
|
352321
352596
|
\`COUNT(*)\` vs \`COUNT(DISTINCT …)\` probe that was never run.
|
|
@@ -352660,7 +352935,7 @@ Output, exactly this shape:
|
|
|
352660
352935
|
plainly the answer explained itself, and however the rubric is worded. Say
|
|
352661
352936
|
in \`why\` that you are overriding a rubric clause, so the clause gets fixed.
|
|
352662
352937
|
This rule exists because a set shipped one: a question asked for sales over
|
|
352663
|
-
the company's season, the answer gave the meteorological window
|
|
352938
|
+
the company's season, the answer gave the meteorological window 20% lower
|
|
352664
352939
|
and said which window it used, and a clause granting \`near_match\` for a
|
|
352665
352940
|
stated window kept a materially wrong answer out of the pass rate
|
|
352666
352941
|
entirely -- the arm reported 100%.
|
|
@@ -354724,7 +354999,7 @@ End with a short **Next steps**: one or two specific deeper analyses the data co
|
|
|
354724
354999
|
{
|
|
354725
355000
|
name: "malloy-analysis-report",
|
|
354726
355001
|
description: 'Combine validated Malloy queries into a report (a linear report, read top to bottom). Use when the user asks to "create a report", "combine these into a report", or wants a persistent multi-query artifact.',
|
|
354727
|
-
body: "# Creating Reports\n\nAn ad-hoc report is a Publisher **report** with `layout=linear`: a `.malloy` file at the package root, `<slug>.malloy` (unless the user asks for another location), that combines markdown narrative with live Malloy queries
|
|
355002
|
+
body: "# Creating Reports\n\nAn ad-hoc report is a Publisher **report** with `layout=linear`: a `.malloy` file at the package root, `<slug>.malloy` (unless the user asks for another location), that combines markdown narrative with live Malloy queries, as text tiles and query tiles read top to bottom. Load `skill:malloy-reports` for the full format and authoring rules; this skill covers when to build one and how to design good report content (tiles, chart annotations, narrative structure). Never write a new `.malloynb`, never write a report as bare `run:` statements (that older form is no longer read), and do not create a `notebooks/` or `dashboards/` directory for it.\n\n## Before building a report\n\n1. **Run each query first** via `execute_query` to verify it works and returns expected results.\n2. **Explain the results** to the user as you go: walk through the analysis step by step.\n3. **Then assemble the report** once the analysis is validated.\n\nDo NOT build the report in the same turn as `execute_query`. Explain first, then build.\n\n## Filters belong to the report, never to the source\n\nAn ad-hoc report usually needs no filters: it records an analysis already run. If the user wants one a reader can change (a manufacturer, a date range), declare the given in the report and bind it on its tiles, as `skill:malloy-reports` describes. Do not add a `given:` to the source for it: a model-level `where:` filters every read of the source, chat and every other report included, and the report builder can show such a filter but not change or remove it. A given the source already reads (a gate, a value every query must supply) shows in the report with no work, and stays in the model.\n\nNever add a `#(filter)` annotation: it is deprecated, and every use has a `given:` form (`malloy-model` covers the migration).\n\n## What goes in the report\n\nDo NOT add an H1 heading in any markdown (use H2 and below for sections); the `title` in the `## artifact` tag serves as the title. To redo the structure rather than tweak one tile, rewrite the report file end-to-end.\n\nText tiles own narrative; query tiles own a single Malloy query whose chart annotation tells the renderer how to display the result. Markdown supports H2 headings, lists, bold, and inline code. Keep text tiles short, one idea per tile, so the rendered output reads as a story instead of a wall of text.\n\nThe file has four parts, in this order:\n\n1. **The tag**, `##| artifact { layout=linear title=\"...\" tiles=[…] }`, listing every tile in reading order. A text tile is `name { kind=text }`; a query tile is the quoted string `\"<extension> -> <view>\"`.\n2. **The `import`** of the model file, relative to the report's own location (`\"order_analysis.malloy\"` from the package root).\n3. **The text tiles**, each a `##|(markdown) name` block: the name on the opener line, the body on the lines below, `|##` closing it.\n4. **One extension of the source**, `source: <source>_tiles is <source> extend { … }`, holding a `view:` per query tile. The view's chart tag and `# label=\"…\"` (the tile's heading) go directly above it.\n\nCompile the file with `/compile` (`\"scope\": \"file\"`, at the path `<slug>.malloy`) before saving; a report compiles as a model, so its errors come back there. After saving and reloading, read the package warnings: a tile that names a missing view is reported there, not by the compile. A complete report:\n\n```malloy\n##| artifact { layout=linear title=\"Sales report\"\n tiles=[\n overview { kind=text },\n \"order_analysis_tiles -> headline\",\n trend { kind=text },\n \"order_analysis_tiles -> revenue_by_month\",\n breakdown { kind=text },\n \"order_analysis_tiles -> top_categories\",\n takeaways { kind=text }\n ]\n}\n|##\nimport \"order_analysis.malloy\"\n\n##|(markdown) overview\n## Overview\nWhat is driving sales, and which categories carry it? The tiles below cover every order in the model.\n|##\n\n##|(markdown) trend\n## Trend\nHow does revenue move over time?\n|##\n\n##|(markdown) breakdown\n## Breakdown\nWhich categories account for the most revenue?\n|##\n\n##|(markdown) takeaways\n## Key takeaways\nWhat to look at next.\n|##\n\nsource: order_analysis_tiles is order_analysis extend {\n # big_value\n # label=\"Headline\"\n view: headline is {\n aggregate:\n # label=\"Revenue\"\n # currency\n total_revenue\n\n # label=\"Orders\"\n # number=auto\n order_count\n }\n\n # line_chart\n # label=\"Revenue by month\"\n view: revenue_by_month is {\n group_by: order_date.month\n aggregate: total_revenue\n order_by: 1\n }\n\n # bar_chart\n # label=\"Top ten categories by revenue\"\n view: top_categories is {\n group_by: category\n aggregate: total_revenue\n order_by: total_revenue desc\n limit: 10\n }\n}\n```\n\nWhen the model already has the view you want, the tile view refines it rather than repeating it: `view: top_categories is by_category + { limit: 10 }`.\n\nA well-structured report typically follows this pattern:\n\n```\n[Text] ## Overview: what question are we answering, what data is in scope (date range, entity count)\n[Query] KPI tile: headline numbers (e.g., # big_value, or # dashboard with nested # big_value views)\n[Text] ## Trend: describe what we should look for over time\n[Query] Time-series tile (e.g., # line_chart on a date dimension)\n[Text] ## Breakdown: where the signal is\n[Query] Categorical tile (e.g., # bar_chart on a categorical dimension)\n[Text] ## Key takeaways: what the user should walk away with\n```\n\nUse this as a default; deviate when the analysis warrants. A grounded report names the time range and entity count up front so every number that follows has context.\n\n## Choosing chart types and annotations\n\nRead `skill:malloy-charts` before picking visualizations: it owns chart-type selection, properties, and the placement rules for chart annotations. `skill:malloy-queries` covers Malloy query patterns and the critical placement rules for chart-annotation tags.\n\nWhen in doubt:\n\n- KPIs / single numbers -> `# big_value`, often nested inside `# dashboard`.\n- Trend over time -> `# line_chart`, usually on the primary date dimension.\n- Category comparisons -> `# bar_chart`, ordered by the metric.\n- Tabular data with many columns -> a plain table tile with `# table.size=fill`.\n- Multiple coordinated charts -> `# dashboard` with `nest:` blocks.\n\nAnnotations go **directly above** the tile's `view:`, never inside its curly braces:\n\n```malloy\n# bar_chart\nview: top_categories is {\n group_by: category\n aggregate: revenue\n order_by: revenue desc\n limit: 10\n}\n```\n\nA `# dashboard` tile composes nested views, useful for KPIs alongside a trend in a single tile. Each `nest:` is a tile; any top-level `aggregate:` measures render as KPI cards. For a fixed grid, use `# dashboard { columns=N }` with `# colspan` on each tile (see `skill:malloy-charts`):\n\n```malloy\n# dashboard { columns=2 }\nview: summary is {\n nest:\n # colspan=2\n # big_value\n kpis is {\n aggregate:\n # label=\"Revenue\"\n # currency\n total_revenue\n\n # label=\"Orders\"\n # number=auto\n order_count\n }\n nest:\n # line_chart\n trend is {\n group_by: order_date.month\n aggregate: total_revenue\n order_by: 1\n }\n}\n```\n\nKey rendering rules to keep in mind when shaping a tile:\n\n- FIRST `group_by` = x-axis, FIRST `aggregate` = y-axis.\n- Override field roles with `# x`, `# y`, `# series` on individual fields.\n- For multiple measure series, place `# y` above the `aggregate:` keyword.\n- One aggregate per chart view: use `# dashboard` with nested views for multiple charts.\n- Use `# table.size=fill` for standalone table queries.\n\n## Editing an existing report\n\nFor small targeted changes (fix one view, insert one tile), edit that view or text block in place and add its entry to `tiles=[…]`, rather than recreating the whole report. For structural rewrites (reordering many tiles, changing the narrative arc), rewrite the report file. An existing `.malloynb` is read, not edited: to change its story, write a `.malloy` report. A tag that sets `kind=notebook` is no longer read, so the file is not a report (`report-kind-removed`): replace it with `layout=linear` and list the tiles. A linear file with no `tiles=[…]`, written as bare `run:` statements, is no longer a report: rewrite it as tiles (`skill:malloy-reports`, `reference/linear.md`).\n\n## IMPORTANT\n\nYou CANNOT see the rendered output of report tiles. Do not claim to see charts, values, or patterns from report tiles you haven't explicitly executed via `execute_query`. If you need to analyze results, run the query via `execute_query` first."
|
|
354728
355003
|
},
|
|
354729
355004
|
{
|
|
354730
355005
|
name: "malloy-charts",
|
|
@@ -354909,7 +355184,7 @@ The user will:
|
|
|
354909
355184
|
- **Remove** fields they don't need.
|
|
354910
355185
|
- **Change** priorities.
|
|
354911
355186
|
|
|
354912
|
-
Once the definitions are confirmed, write them into the \`.malloy\` model (see your modeling workflow). Use \`#(doc)\` annotations to document sources and fields, and \`given:\`
|
|
355187
|
+
Once the definitions are confirmed, write them into the \`.malloy\` model (see your modeling workflow). Use \`#(doc)\` annotations to document sources and fields, and a \`given:\` only where the data's meaning or access depends on a runtime value (a gate, a value every query must supply). A filter a reader picks on a dashboard belongs to the report, not the model. \`#(filter)\` is deprecated. Never add a \`#(filter)\` annotation: every use, including \`required\`, \`implicit\`, and date/number ranges, has a \`given:\` form. See \`skill:malloy-model\` § Parameterizing sources with \`given:\`. Keep the confirmed definitions in the conversation; there is no separate plan-file store.
|
|
354913
355188
|
|
|
354914
355189
|
## Data-driven proposals
|
|
354915
355190
|
|
|
@@ -357447,31 +357722,39 @@ gets unwieldy.
|
|
|
357447
357722
|
|
|
357448
357723
|
## Parameterizing sources with \`given:\` (preferred)
|
|
357449
357724
|
|
|
357450
|
-
Native Malloy **\`given:\` parameters** are
|
|
357725
|
+
Native Malloy **\`given:\` parameters** are runtime values a model reads in its own logic; callers supply them at query time. \`#(filter)\` is deprecated: never add one. Enable givens with \`##! experimental.givens\` at the top of the model.
|
|
357726
|
+
|
|
357727
|
+
**A given belongs in the model only when the data's meaning or access depends on it:**
|
|
357728
|
+
|
|
357729
|
+
- a row-level gate: \`#(authorize)\`, \`#(access_filter)\`, a \`#(secure)\` given, \`$GROUPS\`;
|
|
357730
|
+
- a value every query must supply (the no-default row in the table below);
|
|
357731
|
+
- a parameter a measure, dimension or view is defined with;
|
|
357732
|
+
- scoping the author means to apply to every consumer of the source: chat, MCP and every report.
|
|
357733
|
+
|
|
357734
|
+
**A filter a reader picks on a dashboard or notebook is not modeling.** Manufacturer, a date range, region, a "major recalls only" toggle: don't add it to a source. Leave the source unfiltered, and let the report declare the given and bind it on its tiles (\`skill:malloy-reports\`). A model-level \`where:\` applies to every read of the source, chat and every other report included, and a report cannot take it over, because binding is per declaration, not per name. The report builder shows such a filter but cannot remove or reconfigure it.
|
|
357451
357735
|
|
|
357452
357736
|
\`\`\`malloy
|
|
357453
357737
|
##! experimental.givens
|
|
357454
357738
|
|
|
357455
|
-
|
|
357456
|
-
|
|
357457
|
-
subject_filter :: filter<string> is f''
|
|
357739
|
+
# A value every query must supply: the table is partitioned by day.
|
|
357740
|
+
given: EVENT_DATE :: date
|
|
357458
357741
|
|
|
357459
|
-
source:
|
|
357460
|
-
where:
|
|
357461
|
-
measure:
|
|
357742
|
+
source: events is duckdb.table('data/events.parquet') extend {
|
|
357743
|
+
where: event_date = $EVENT_DATE
|
|
357744
|
+
measure: event_count is count()
|
|
357462
357745
|
}
|
|
357463
357746
|
\`\`\`
|
|
357464
357747
|
|
|
357465
|
-
A given is **declared bare** but **referenced with a \`$\` sigil** in expressions (\`$
|
|
357748
|
+
A given is **declared bare** but **referenced with a \`$\` sigil** in expressions (\`$EVENT_DATE\`), as above.
|
|
357466
357749
|
|
|
357467
|
-
- **
|
|
357750
|
+
- **An optional given in the model gets a neutral, match-all default** (a \`filter<>\` given defaulting to \`f''\`), so an unsupplied value returns unfiltered rows. Because it bakes an always-on \`where:\` into the source, a non-neutral default (a date floor, say) applies to *every* read of the source. Defaults must be Malloy literals.
|
|
357468
357751
|
- **Givens don't auto-inject a \`where:\`.** Unlike \`#(filter)\`, you write the filter expression that references the given yourself (e.g. \`where: dimension ~ $given_name\`).
|
|
357469
357752
|
- **Every \`#(filter)\` use has a \`given:\` form.** Never add a \`#(filter)\` annotation to a model, not even for \`required\`, \`implicit\`, or a date/number range:
|
|
357470
357753
|
|
|
357471
357754
|
| You want | Write this | Not this |
|
|
357472
357755
|
|---|---|---|
|
|
357473
|
-
| An optional value or list filter | \`given: REGION :: filter<string> is f''\`
|
|
357474
|
-
| A date or number range | \`given: MIN_SALE :: filter<number> is f''\` (or \`filter<date>\`)
|
|
357756
|
+
| An optional value or list filter a reader picks | In the **report**, not the model: \`given: REGION :: filter<string> is f''\` there, bound on its tiles with \`+ { where: region ~ $REGION }\` (\`skill:malloy-reports\`). In the model only for one of the cases above. | \`#(filter) dimension=region type=in\` |
|
|
357757
|
+
| A date or number range a reader picks | In the **report** the same way: \`given: MIN_SALE :: filter<number> is f''\` (or \`filter<date>\`), bound with \`+ { where: sale_price ~ $MIN_SALE }\`. The caller sends a filter expression such as \`>= 50\`. | two \`#(filter)\` lines, \`greater_than\` and \`less_than\` |
|
|
357475
357758
|
| A value every query must supply (the primary key is only unique within it, or the table is too big to scan whole) | a given with **no default**, e.g. \`given: EVENT_DATE :: date\` and \`where: event_date = $EVENT_DATE\`. A query that omits it fails with "Given 'EVENT_DATE' has no value and no default". | \`#(filter) ... required\` |
|
|
357476
357759
|
| A row filter the system applies, not the caller | \`#(access_filter) org_id in $ORG_IDS\`, with \`ORG_IDS\` set by a trusted tier (see the trust caveat below) | \`#(filter) ... implicit\` |
|
|
357477
357760
|
|
|
@@ -357501,7 +357784,7 @@ Publisher formats the value from the dimension's type (\`'value'\`, bare \`true\
|
|
|
357501
357784
|
|
|
357502
357785
|
\`#(filter)\` is not a security boundary. A caller skips it with \`bypass_filters=true\` (REST) or \`bypassFilters: true\` (POST body), or by writing their own query text. Use givens with \`#(authorize)\` / \`#(access_filter)\` for access control.
|
|
357503
357786
|
|
|
357504
|
-
**To migrate** a source, replace each annotation using the table above, then delete it. \`type=greater_than\` and \`type=less_than\` are exclusive, so a plain comparison you convert one to is \`>\` or \`<\`, not \`>=\` or \`<=\`. \`docs/givens.md\` § "Coming from \`#(filter)\`" has a worked conversion.
|
|
357787
|
+
**To migrate** a source, replace each annotation using the table above, then delete it. An optional filter (\`type=in\`, \`like\`, \`equal\`, a range) moves to the reports that offer it, declared and bound there, and the source is left unfiltered; a \`required\` one stays in the model as a no-default given. \`type=greater_than\` and \`type=less_than\` are exclusive, so a plain comparison you convert one to is \`>\` or \`<\`, not \`>=\` or \`<=\`. \`docs/givens.md\` § "Coming from \`#(filter)\`" has a worked conversion.
|
|
357505
357788
|
|
|
357506
357789
|
## Access Control: \`#(authorize)\` and \`#(access_filter)\`
|
|
357507
357790
|
|
|
@@ -357932,7 +358215,7 @@ When you've been doing analysis, writing queries, building views, creating repor
|
|
|
357932
358215
|
|
|
357933
358216
|
## Step 1: IDENTIFY, what belongs in a model?
|
|
357934
358217
|
|
|
357935
|
-
Scan your analysis \`.malloy\` files and reports (any \`.malloy\` with an \`## artifact\` tag, wherever it sits, or an existing \`.malloynb\`). A report (a \`tiles=[…]\` layout
|
|
358218
|
+
Scan your analysis \`.malloy\` files and reports (any \`.malloy\` with an \`## artifact\` tag, wherever it sits, or an existing \`.malloynb\`). A report (a \`tiles=[…]\` layout, of either layout) is not a model file to restructure: lift its reusable definitions into a model and leave the report to import them. For each dimension, measure, join, or calculation, ask: **is this a reusable building block?**
|
|
357936
358219
|
|
|
357937
358220
|
A measure or dimension belongs in a model if it meets ANY of these criteria:
|
|
357938
358221
|
|
|
@@ -360351,7 +360634,7 @@ A flat layout at the package root is the simplest default, for models and report
|
|
|
360351
360634
|
|
|
360352
360635
|
Publishable contents:
|
|
360353
360636
|
- \`.malloy\` files - Semantic model definitions (base sources + joined sources)
|
|
360354
|
-
- Reports: \`.malloy\` files with an \`## artifact { layout=linear … }\` or \`## artifact { layout=grid … }\` tag, anywhere in the package (outside \`public/\`), written as tiles
|
|
360637
|
+
- Reports: \`.malloy\` files with an \`## artifact { layout=linear … }\` or \`## artifact { layout=grid … }\` tag, anywhere in the package (outside \`public/\`), written as tiles (see \`skill:malloy-reports\`). A linear tag with no tiles, the retired \`run:\`-cell form, is no report. The tag alone makes a file a report and sets its layout; the folder means nothing. An existing \`.malloynb\` is still served as a linear report; never write a new one.
|
|
360355
360638
|
- Data files (CSV/Parquet/XLSX) - Embedded data published with package
|
|
360356
360639
|
|
|
360357
360640
|
## Version Management
|
|
@@ -360720,7 +361003,7 @@ For anything not covered here, call \`search_malloy_docs\` with the topic (for e
|
|
|
360720
361003
|
{
|
|
360721
361004
|
name: "malloy-reports",
|
|
360722
361005
|
description: "Build or modify a Malloy Publisher report: a .malloy file with an ## artifact tag, written at the package root unless asked otherwise, laid out as a grid (a dashboard, scanned at a glance) or linear (a notebook or data story, read top to bottom), with filter controls rendered from givens and # drill click-through. Use when the user asks for a dashboard, a notebook, a report, a data story, a filterable view, or drill-through between views, and no code is wanted. Legacy .malloynb files are read, not authored.",
|
|
360723
|
-
body: "# Publisher Reports\n\n> A `.malloy` file with an `## artifact { layout=… tiles=[…] }` tag **is** a report. It imports the model, names the views to show, binds the filtering on those views, and says how to lay them out. Publisher discovers it at package load, renders the filter controls from the givens its tiles reference, offers `# drill` click-through, and serves it at `/<env>/<pkg>/<path>`, its package path without `.malloy`. No code, no build step.\n\nWhat other tools call a dashboard and a notebook are both reports here. The **layout** is the only difference:\n\n- **`layout=grid`**: tiles across a grid behind one filter row, scanned at a glance (a dashboard). Read `reference/grid.md` before you lay one out.\n- **`layout=linear`**: tiles and prose top to bottom, read in order (a notebook, a data story). Read `reference/linear.md` before you write one, and before you edit a report written as `run:` cells.\n\nEverything else (filters, text tiles, drill, the builder, the lint, the write path) is the same for both, and switching layout later is a one-property edit.\n\n## Where the file goes\n\n- **Write a new report at the package root**, as `<slug>.malloy`, unless the author asks for another location. No directory is required, and none is a convention: a report can live anywhere in the package except `public/`, and the root `index.malloy` is never a report. Do not create a `dashboards/` or `notebooks/` directory for it.\n- **Always write `layout=grid` or `layout=linear`** in the tag. A tag without one is read by a fallback rule, and a linear report that lists tiles without `layout=linear` is read as a grid. `kind=` is no longer read: a tag that sets it makes no report (`report-kind-removed`), so replace it with `layout=`.\n- **The package path is the report's identity.** `sales/q3.malloy` is the report `sales/q3`: its URL, its `GET …/reports/{path}` address, and its `# drill` target. Two reports may share a basename in different directories.\n- **Imports are relative to the report's own location.** At the root: `import { order_items } from \"storefront.malloy\"`. In a subdirectory: `\"../storefront.malloy\"`.\n- **An untagged `.malloy` file is an ordinary model**, wherever it lives. Shared definitions several reports import go in one.\n\n## When this is the right tool\n\n| The user wants | Use |\n| ---------------------------------------------------- | ------------------------------------------------------------ |\n| A recurring, at-a-glance view behind shared filters | this skill, `layout=grid` |\n| A narrative, with prose between the numbers | this skill, `layout=linear` |\n| A quick write-up of queries you already ran | `skill:malloy-analysis-report`, which writes a linear report |\n| Custom design, branding, or interactions beyond tags | an HTML data app |\n| The model itself: sources, measures, joins | `malloy-model` |\n\nInteractivity is not the axis: both layouts get filter controls, URL-addressable state, Apply batching, and `# drill`. Pick on the shape of the document: scanned at a glance is grid, read top to bottom is linear. When in doubt, linear.\n\n## Build sequence\n\nTool names below are bare (`get_context`); match each against the tools you actually have, since a host may prefix or rename them. A host names its compile and reload tools its own way. Where you have none, use the REST endpoint named beside the step.\n\n1. **READ THE MODEL FIRST.** Get the real source, view, dimension, and given names from the package: `get_context` if you have it, otherwise the REST model endpoint for `index.malloy` (it answers 404 for a file off the surface) or the `.malloy` files. Never guess a name. A guessed field in a query fails the whole package load, not just that one report; a guessed tile or suggest source is quieter, and only shows up in the package warnings. If the package root holds an `index.malloy` (every scaffolded package does), a tile reads only the sources that file exports.\n2. **PICK THE LAYOUT AND THE TILES.** Which views, in what order, and on a grid how wide each sits. A query tile is a `view:` you declare in the report, named as `\"source -> view\"`.\n3. **DECLARE THE GIVENS** the report will filter by, in the report file itself, with their control tags (see \"Filter controls\"). The builder edits the report file and nothing else, so a filter it can add, change or remove is a declaration in that file. A given the model already declares and reads is imported by name, never declared again (see \"Givens that stay in the model\").\n4. **COMPOSE THE FILE** at its path (the package root unless asked otherwise), following the template below, but do not save it yet. Name the sources you need in the import. Each tile binds its controls with a refinement, `view: t is v + { where: category ~ $CATEGORY }`, one clause per given it answers to; a `filter<...>` binds with `~`, a plain `date` or `number` given is a value and binds with `>=`, `<=` or `=`. Only the givens some tile references become controls. Then import every source or query any referenced given names in a `suggest`; **a `suggest` naming a `query=` needs that query's own source imported as well**, because an import is not transitive.\n5. **COMPILE IT** with your compile tool (`POST …/models/<path>/compile` over REST) at the path the file will have, with **`\"scope\": \"file\"`**: a report opens with an `import`, which the default `append` scope refuses, and an existing file compiled at `append` collides with itself. **`\"scope\": \"file\"` does not check tiles, drills or suggest queries.** To see those before you save, compile the same text at `\"scope\": \"package\"` with `modelPath` set to the report's path: it runs the lint step 6 reads and returns its findings with code `dashboard-lint` (or `render-tag`). Editing an untagged model several reports import also wants `\"scope\": \"package\"`, which recompiles every file as saved.\n6. **SAVE IT, RELOAD, AND READ THE MANIFEST AND THE WARNINGS.** Save by writing the file, or with `PUT …/models/<path>` over REST (omit `expectedHash` to create; it creates at any new path and replaces only a file the package already serves as a report). Then your reload tool, or `GET …/packages/<pkg>?reload=true`. A 424 means the package did not load and your edit is not live. **The `warnings` key is absent when there are none.** Then read `GET …/packages/<pkg>/reports/<path>`: its `givens` are exactly the controls that will render, and its `layout` is the layout Publisher read. See \"Read the lint\".\n7. **OPEN IT AND LOOK.** Not optional; see \"What done means\".\n\n## The form\n\n```malloy\n##! experimental.givens\n##\" One line saying what this report is for.\n##| artifact { layout=grid title=\"Storefront overview\"\n tiles=[\n intro { kind=text colspan=12 },\n \"overview -> kpis\",\n \"overview -> revenue_trend\",\n \"overview -> revenue_by_state\"\n ]\n} dashboard { columns=12 }\n|##\nimport { order_items, products } from \"storefront.malloy\"\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n\n##|(markdown) intro\nRevenue and margin for the selected category.\n|##\n\nsource: overview is order_items extend {\n # colspan=12\n # label=\"Key figures\"\n # big_value\n view: kpis is key_figures + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=8\n # break\n # label=\"Revenue by month\"\n view: revenue_trend is sales_by_month + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=4\n # label=\"Revenue by state\"\n view: revenue_by_state is sales_by_state + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n}\n```\n\nThe same file with `layout=linear` and no `dashboard { … }` or `colspan` is a linear report.\n\n- **The tag is model-level**, one line (`## artifact { … }`) or a `##|` … `|##` block with the same text inside, the form to use once the list is long: one tile per line, `}` on its own line, `|##` closing at the opener's column. A one-line `##` tag must stay on one line: wrapping it is a compile error, or worse, compiles clean and quietly stops being a report.\n- **The header** above the tag holds only `##!` flags, `//` comments and the unnamed `\"` notes that are the report's description. A statement or a `#` tag above `## artifact` is an error.\n- **Quote every query entry.** `\"overview -> kpis\"` is a string; unquoted, the tag does not parse (`notebook-artifact-unparsed`). A bare quoted name (`\"top_products\"`) names a model-level `query:` in the file.\n- **A query tile is a thin `view:` on an extension the report declares** (`source: overview is order_items extend { … }`): the modelled view keeps its chart tag, the wrapper carries the tile's `# label`, its width on a grid, and its binding.\n- **A text tile is a `##|(markdown) name` block** listed as `name { kind=text }`. The name is one bare word on the opener line, the body starts on the next line (a heading goes inside the block), and `|##` closes it at the opener's column. A bare `intro` without `{ kind=text }` is the query-tile form for a `query:`, so the block is not shown. On a grid the entry also takes `colspan` and `break`, and nothing else.\n- **File order:** header, imports and givens, the prose blocks in tile order, then the extension that defines the tile views. That is the order the builder writes.\n- **A `run:` in a report that lists `tiles` is never shown** (`notebook-layout-run`). Define a view, list it, delete the `run:`.\n- **A tile expression is a string, so the compiler never checks it.** Rename a view and the file still compiles; the tile fails at package load, where the lint names it.\n- **Chart tags go on the view** (`# line_chart`, `# bar_chart`, `# big_value`, `# label=\"…\"`). `skill:malloy-charts` covers choosing them.\n\n## Filter controls\n\nControls come from the `given:` declarations the tiles reference. The tags on the declaration are the control contract:\n\n```malloy\n##! experimental.givens\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n\n# label=\"Brand\" control=multiselect suggest { query=brand_suggest dimension=brand }\ngiven: BRAND :: filter<string> is f''\n\n# label=\"Minimum line total\" range_min=0 range_max=250\ngiven: MIN_SALE :: filter<number> is f''\n\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n```\n\n`control=select`/`multiselect` with a `suggest` renders a picker filled from the data; `range_min`/`range_max` on a `filter<number>` renders a two-handled range slider; a `filter<date>` or `filter<timestamp>` renders a time-range control with preset windows and a custom day range; a bare `date` or `timestamp` renders a date picker; a `filter<string>` with no tag renders a text box taking filter syntax. Which controls appear is per report, decided by which givens its tiles reference.\n\n- `#(description=\"…\")` renders as helper text under the control and draws a cosmetic \"not a well-formed route\" compile diagnostic for a multi-word value; `# description=\"…\"` publishes to the API but the UI does not render it.\n- `dimension=` can name a field of a directly joined source, quoted: `suggest { source=sales dimension=\"products.department\" }`. On the `query=` form, `dimension` names a column of that query's output; match it to the query's `group_by`.\n- **A suggest's source or query has to resolve in the report file**, so import it there. Getting it wrong does not error: the control still looks like a picker and says \"Could not load the options for this control\"; the package warnings name it.\n- Two options on the artifact tag, either layout: `autorun=false` batches control changes behind an Apply button; `givens { CATEGORY=f'Outerwear' }` sets starting values (a URL parameter wins). A file-level `## autorun=false` does nothing on a `.malloy` report.\n\n### Binding, and givens that stay in the model\n\n**Binding is per declaration, not per name.** A report declaring its own `CATEGORY` over a source whose model-level `where:` reads the model's `CATEGORY` compiles, shows the control, and filters nothing. So bind on the tiles, with `+ { where: … }`. The other design, model-level scoping, still works: put a scoped source in an untagged model that imports the givens it reads, and import that model and those givens into the report. Do not mix the two on one given.\n\nA given stays in the model, declared once there, when a model source, view or measure, an `#(authorize)` or `#(access_filter)` gate, or an HTML data app reads it. The report imports it by name (`import { CATEGORY } from \"givens.malloy\"`) and never re-declares it:\n\n- **Import a name and also declare it: a compile error.** Drop the local declaration.\n- **Declare a name the model already reads, without importing it: no error at all.** The control renders and moves, and the model's own `where:` or gate never sees the value.\n\nA given has to be in the report file's own scope to be bindable: declared there or imported. Never add a `#(filter)` annotation: it is deprecated, a report renders no control for it, and every use has a `given:` form (`malloy-model` covers the migration). `malloy-model` and `docs/givens.md` cover givens themselves.\n\n## Drill\n\n`# drill` goes on a model **dimension**, never on a report:\n\n```malloy\n# drill { to=[\"category\", \"self\"] given=CATEGORY }\ndimension: category is products.category\n\n# drill { to=\"regions/west\" given=REGION }\ndimension: region is regions.region\n```\n\n- **`to=<path>`** navigates to a report, of either layout, by its package path without `.malloy`, read from the package root: `to=category` is `category.malloy` at the root, `to=\"regions/west\"` is `regions/west.malloy`. Quote a path with a `/`. **Always write the path from the package root**: a bare name is a root file, never a report of that name elsewhere.\n- **`to=self`** filters in place; two or more destinations pop a menu.\n- **Always write `given=`.** Without it the given is the dimension name verbatim, so `dimension: category` seeds `category`, not `CATEGORY`. `to=self` survives that (it folds case); `to=<path>` does not, and lands on an unfiltered page with nothing erroring.\n- **The destination needs a control for the given**, declared as `filter<T>`. No lint checks that half, so click it and look.\n- **Only table cells are marked drillable.** A report meant to be drilled wants at least one untagged (table) tile, grouped by the tagged dimension. A column whose rendered header another, non-drillable tile also shows loses its marking: give the two different `# label=`s.\n\n## Read the lint\n\nPackage warnings after a reload are the report's test suite. Fix all of them, and read the text rather than the severity or the count. **\"Dashboard lint stopped early, so this list is incomplete\"** means the rest of the list is partial.\n\n| Finding | Fix |\n| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `# drill … targets \"x\", which is not a report in this package` | A dead click. Write the target's path |\n| `to=self, but no model in this package declares a given \"X\"` | The clicked value has nowhere to go. Declare the given, or fix `given=` |\n| `given \"X\" suggests options from source \"y\", which this file does not define` | Import `y` into the report |\n| `filters by given \"X\", which this file does not import` | No control is shown for it. Import the given |\n| A tile that does not resolve to a real view | Fix the view name, or declare the view |\n| `notebook-artifact-unparsed` | The tag does not parse, usually an unquoted `source -> view` entry |\n| `notebook-layout-run` | A `run:` beside `tiles`. Make it a view, list it, delete the `run:` |\n| `notebook-tile-layout-ignored` | `colspan` or `break` on a linear report. Remove them, or switch to `layout=grid` |\n| `report-kind-removed` | The tag sets `kind=`, which is no longer read, so the file is no report. Write the `layout=` the finding gives |\n| `report-layout-implicit` | A tag with no tiles and no `layout=`. Add `layout=` |\n| `report-on-surface` | A model `index.malloy` imports (or `explores` lists) is tagged, so its sources left the surface. Untag it, or move the report out |\n| `index-is-not-a-report` | The root `index.malloy` carries a tag. Remove it; put the report in its own file |\n| `report-name-ambiguous` | Two grid reports share a file name, so the deprecated by-name dashboards route answers 409 for it. Rename one of the files |\n| `Tile … reads <source>, which index.malloy doesn't export` | Add the source to the `export { ... }` in `index.malloy`. Never add an `explores` to `publisher.json` |\n| A grid's `columns`, `colspan`, or `dashboard_columns=` findings | `reference/grid.md` |\n| A statement or tag above the artifact tag; prose the reader cannot place | `reference/linear.md`, the cell rules |\n\n**Read the status the reload itself returns, not the listing.** One report that fails to compile fails the whole package load (424), and a package already serving keeps its previous version, so the listing still looks healthy. `GET /api/v0/status` still knows: a package serving an older model than its files appears in `loadErrors` with `stale: true`. If the reload is 200 and your report is not listed, discovery skipped it: usually a missing or misspelled artifact tag, or a file under `public/`.\n\n**A clean reload is not proof the tags are right.** The parse warning is syntax only and carries no position. That is why the last step is opening the page.\n\n## Template\n\n```malloy\n##! experimental.givens\n##\" [One-line description of the report.]\n##| artifact { layout=linear title=\"[Title]\"\n tiles=[\n intro { kind=text },\n \"main_source_tiles -> broad_tile\",\n \"main_source_tiles -> drill_tile\"\n ]\n}\n|##\nimport { main_source } from \"model.malloy\"\n\n##|(markdown) intro\n[Framing question: what are we trying to understand?]\n|##\n\nsource: main_source_tiles is main_source extend {\n view: broad_tile is [broad view]\n view: drill_tile is [drill view]\n}\n```\n\nFor a grid, write `layout=grid`, add `dashboard { columns=12 }` after the tag's closing `}`, and give every tile view a `# colspan` (`reference/grid.md`).\n\n## Editing in the Console\n\nA person can open any report in the Console's builder (**Edit** on the report page; **New report** on the package page writes `<slug>.malloy` at the package root). The builder splices the file rather than regenerating it, so comments and anything it does not model survive; its layout switch rewrites `layout=` and nothing else. A report written as `run:` cells opens converted to tiles, and the first Save asks before it rewrites the file (`reference/linear.md`). A save whose text declares a real `#(authorize)` or `#(access_filter)` gate outside prose is refused with a 400: gates live in a model file the report imports.\n\n## What \"done\" means\n\n- The file is at the package root (or where the author asked), and its tag says `layout=`.\n- Every source, view, and field name came from the model you read in step 1.\n- The reload returned **200**, not 424, with **zero** report warnings, and your report is in `GET …/reports`.\n- You opened the page and every tile shows real numbers: not stuck loading, not an error, not an empty state you did not intend.\n- Each control renders as the widget you intended, and changing one changes the numbers.\n- If you added a `# drill`, its `to=` is a path, and you clicked it and landed where you meant to, with the given seeded.\n- On a grid: every tile carries a colspan, each row sums to `columns`, rows end flush, nothing is clipped, and no legend or card label ends in `…`.\n\n## Reference\n\n- `reference/grid.md`: the grid recipe, its traps, and one query as the whole page.\n- `reference/linear.md`: linear reports, `run:` cells, legacy `.malloynb`, style, and common mistakes.\n- `docs/reports.md`: the full guide this skill condenses.\n- `docs/givens.md`: the givens the controls are generated from.\n\n## Reference files over MCP\n\nThis skill's `reference/` files are served as separate prompts, one per file, fetched only when you ask for them. Where the text above says to read `reference/<name>.md`, get the prompt named `malloy-reports/<name>` instead.\n\nAvailable: grid, linear."
|
|
361006
|
+
body: "# Publisher Reports\n\n> A `.malloy` file with an `## artifact { layout=… tiles=[…] }` tag **is** a report. It imports the model, names the views to show, binds the filtering on those views, and says how to lay them out. Publisher discovers it at package load, renders the filter controls from the givens its tiles reference, offers `# drill` click-through, and serves it at `/<env>/<pkg>/<path>`, its package path without `.malloy`. No code, no build step.\n\nWhat other tools call a dashboard and a notebook are both reports here. The **layout** is the only difference:\n\n- **`layout=grid`**: tiles across a grid behind one filter row, scanned at a glance (a dashboard). Read `reference/grid.md` before you lay one out.\n- **`layout=linear`**: tiles and prose top to bottom, read in order (a notebook, a data story). Read `reference/linear.md` before you write one.\n\nEverything else (filters, text tiles, drill, the builder, the lint, the write path) is the same for both, and switching layout later is a one-property edit.\n\n## Where the file goes\n\n- **Write a new report at the package root**, as `<slug>.malloy`, unless the author asks for another location. No directory is required, and none is a convention: a report can live anywhere in the package except `public/`, and the root `index.malloy` is never a report. Do not create a `dashboards/` or `notebooks/` directory for it.\n- **Always write `layout=grid` or `layout=linear`** in the tag. A tag without one is read by a fallback rule, and a linear report that lists tiles without `layout=linear` is read as a grid. `kind=` is no longer read: a tag that sets it makes no report (`report-kind-removed`), so replace it with `layout=`.\n- **The package path is the report's identity.** `sales/q3.malloy` is the report `sales/q3`: its URL, its `GET …/reports/{path}` address, and its `# drill` target. Two reports may share a basename in different directories.\n- **Imports are relative to the report's own location.** At the root: `import { order_items } from \"storefront.malloy\"`. In a subdirectory: `\"../storefront.malloy\"`.\n- **An untagged `.malloy` file is an ordinary model**, wherever it lives. Shared definitions several reports import go in one.\n\n## When this is the right tool\n\n| The user wants | Use |\n| ---------------------------------------------------- | ------------------------------------------------------------ |\n| A recurring, at-a-glance view behind shared filters | this skill, `layout=grid` |\n| A narrative, with prose between the numbers | this skill, `layout=linear` |\n| A quick write-up of queries you already ran | `skill:malloy-analysis-report`, which writes a linear report |\n| Custom design, branding, or interactions beyond tags | an HTML data app |\n| The model itself: sources, measures, joins | `malloy-model` |\n\nInteractivity is not the axis: both layouts get filter controls, URL-addressable state, Apply batching, and `# drill`. Pick on the shape of the document: scanned at a glance is grid, read top to bottom is linear. When in doubt, linear.\n\n## Build sequence\n\nTool names below are bare (`get_context`); match each against the tools you actually have, since a host may prefix or rename them. A host names its compile and reload tools its own way. Where you have none, use the REST endpoint named beside the step.\n\n1. **READ THE MODEL FIRST.** Get the real source, view, dimension, and given names from the package: `get_context` if you have it, otherwise the REST model endpoint for `index.malloy` (it answers 404 for a file off the surface) or the `.malloy` files. Never guess a name. A guessed field in a query fails the whole package load, not just that one report; a guessed tile or suggest source is quieter, and only shows up in the package warnings. If the package root holds an `index.malloy` (every scaffolded package does), a tile reads only the sources that file exports.\n2. **PICK THE LAYOUT AND THE TILES.** Which views, in what order, and on a grid how wide each sits. A query tile is a `view:` you declare in the report, named as `\"source -> view\"`.\n3. **DECLARE THE GIVENS** the report will filter by, in the report file itself, with their control tags (see \"Filter controls\"). A report holds its own filters: the builder edits the report file and nothing else, so a filter it can add, change or remove is a declaration in that file. A given the model already declares and reads is imported by name, never declared again; if it is only a reader's filter, move it into the report instead (see \"Givens that stay in the model\").\n4. **COMPOSE THE FILE** at its path (the package root unless asked otherwise), following the template below, but do not save it yet. Name the sources you need in the import. Each tile binds its controls with a refinement, `view: t is v + { where: category ~ $CATEGORY }`, one clause per given it answers to; a `filter<...>` binds with `~`, a plain `date` or `number` given is a value and binds with `>=`, `<=` or `=`. Only the givens some tile references become controls. Then import every source or query any referenced given names in a `suggest`; **a `suggest` naming a `query=` needs that query's own source imported as well**, because an import is not transitive.\n5. **COMPILE IT** with your compile tool (`POST …/models/<path>/compile` over REST) at the path the file will have, with **`\"scope\": \"file\"`**: a report opens with an `import`, which the default `append` scope refuses, and an existing file compiled at `append` collides with itself. **`\"scope\": \"file\"` does not check tiles, drills or suggest queries.** To see those before you save, compile the same text at `\"scope\": \"package\"` with `modelPath` set to the report's path: it runs the lint step 6 reads and returns its findings with code `dashboard-lint` (or `render-tag`). Editing an untagged model several reports import also wants `\"scope\": \"package\"`, which recompiles every file as saved.\n6. **SAVE IT, RELOAD, AND READ THE MANIFEST AND THE WARNINGS.** Save by writing the file, or with `PUT …/models/<path>` over REST (omit `expectedHash` to create; it creates at any new path and replaces only a file the package already serves as a report). Then your reload tool, or `GET …/packages/<pkg>?reload=true`. A 424 means the package did not load and your edit is not live. **The `warnings` key is absent when there are none.** Then read `GET …/packages/<pkg>/reports/<path>`: its `givens` are exactly the controls that will render, and its `layout` is the layout Publisher read. See \"Read the lint\".\n7. **OPEN IT AND LOOK.** Not optional; see \"What done means\".\n\n## The form\n\n```malloy\n##! experimental.givens\n##\" One line saying what this report is for.\n##| artifact { layout=grid title=\"Storefront overview\"\n tiles=[\n intro { kind=text colspan=12 },\n \"overview -> kpis\",\n \"overview -> revenue_trend\",\n \"overview -> revenue_by_state\"\n ]\n} dashboard { columns=12 }\n|##\nimport { order_items, products } from \"storefront.malloy\"\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n\n##|(markdown) intro\nRevenue and margin for the selected category.\n|##\n\nsource: overview is order_items extend {\n # colspan=12\n # label=\"Key figures\"\n # big_value\n view: kpis is key_figures + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=8\n # break\n # label=\"Revenue by month\"\n view: revenue_trend is sales_by_month + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n\n # colspan=4\n # label=\"Revenue by state\"\n view: revenue_by_state is sales_by_state + { where: category ~ $CATEGORY, where: created_at >= $SINCE }\n}\n```\n\nThe same file with `layout=linear` and no `dashboard { … }` or `colspan` is a linear report.\n\n- **The tag is model-level**, one line (`## artifact { … }`) or a `##|` … `|##` block with the same text inside, the form to use once the list is long: one tile per line, `}` on its own line, `|##` closing at the opener's column. A one-line `##` tag must stay on one line: wrapping it is a compile error, or worse, compiles clean and quietly stops being a report.\n- **The header** above the tag holds only `##!` flags, `//` comments and the unnamed `\"` notes that are the report's description. Keep statements and `#` tags below `## artifact`: nothing refuses one above it, but the header is where readers and the builder look.\n- **Quote every query entry.** `\"overview -> kpis\"` is a string; unquoted, the tag does not parse (`notebook-artifact-unparsed`). A bare quoted name (`\"top_products\"`) names a model-level `query:` in the file.\n- **A query tile is a thin `view:` on an extension the report declares** (`source: overview is order_items extend { … }`): the modelled view keeps its chart tag, the wrapper carries the tile's `# label`, its width on a grid, and its binding.\n- **A text tile is a `##|(markdown) name` block** listed as `name { kind=text }`. The name is one bare word on the opener line, the body starts on the next line (a heading goes inside the block), and `|##` closes it at the opener's column. A bare `intro` without `{ kind=text }` is the query-tile form for a `query:`, so the block is not shown. On a grid the entry also takes `colspan` and `break`, and nothing else.\n- **File order:** header, imports and givens, the prose blocks in tile order, then the extension that defines the tile views. That is the order the builder writes.\n- **A `run:` in a report that lists `tiles` is never shown** (`notebook-layout-run`). Define a view, list it, delete the `run:`.\n- **A tile expression is a string, so the compiler never checks it.** Rename a view and the file still compiles; the tile fails at package load, where the lint names it.\n- **Chart tags go on the view** (`# line_chart`, `# bar_chart`, `# big_value`, `# label=\"…\"`). `skill:malloy-charts` covers choosing them.\n\n## Filter controls\n\nControls come from the `given:` declarations the tiles reference. The tags on the declaration are the control contract:\n\n```malloy\n##! experimental.givens\n\n# label=\"Category\" control=select suggest { source=products dimension=category }\ngiven: CATEGORY :: filter<string> is f''\n\n# label=\"Brand\" control=multiselect suggest { query=brand_suggest dimension=brand }\ngiven: BRAND :: filter<string> is f''\n\n# label=\"Minimum line total\" range_min=0 range_max=250\ngiven: MIN_SALE :: filter<number> is f''\n\n# label=\"Ordered since\"\ngiven: SINCE :: date is @2023-01-01\n```\n\n`control=select`/`multiselect` with a `suggest` renders a picker filled from the data; `range_min`/`range_max` on a `filter<number>` renders a two-handled range slider; a `filter<date>` or `filter<timestamp>` renders a time-range control with preset windows and a custom day range; a bare `date` or `timestamp` renders a date picker; a `filter<string>` with no tag renders a text box taking filter syntax. Which controls appear is per report, decided by which givens its tiles reference.\n\n- `#(description=\"…\")` renders as helper text under the control and draws a cosmetic \"not a well-formed route\" compile diagnostic for a multi-word value; `# description=\"…\"` publishes to the API but the UI does not render it.\n- `dimension=` can name a field of a directly joined source, quoted: `suggest { source=sales dimension=\"products.department\" }`. On the `query=` form, `dimension` names a column of that query's output; match it to the query's `group_by`.\n- **A suggest's source or query has to resolve in the report file**, so import it there. Getting it wrong does not error: the control still looks like a picker and says \"Could not load the options for this control\"; the package warnings name it.\n- Two options on the artifact tag, either layout: `autorun=false` batches control changes behind an Apply button; `givens { CATEGORY=f'Outerwear' }` sets starting values (a URL parameter wins). A file-level `## autorun=false` does nothing on a `.malloy` report.\n\n### Binding, and givens that stay in the model\n\n**Binding is per declaration, not per name.** A report declaring its own `CATEGORY` over a source whose model-level `where:` reads the model's `CATEGORY` compiles, shows the control, and filters nothing. So bind on the tiles, with `+ { where: … }`. The other design, model-level scoping, still works: put a scoped source in an untagged model that imports the givens it reads, and import that model and those givens into the report. Do not mix the two on one given.\n\nA given stays in the model, declared once there, when a model source, view or measure, an `#(authorize)` or `#(access_filter)` gate, or an HTML data app reads it. The report imports it by name (`import { CATEGORY } from \"givens.malloy\"`) and never re-declares it:\n\n- **Import a name and also declare it: a compile error.** Drop the local declaration.\n- **Declare a name the model already reads, without importing it: no error at all.** The control renders and moves, and the model's own `where:` or gate never sees the value.\n\n**A model that already scopes a source by an optional filter given** (`where: Manufacturer ~ $manufacturer_filter` in the model) needs a decision:\n\n- **It is a reader's filter and you may change the model: move it.** Delete the given and its `where:` clause from the model, declare it in the report with its control tags, and bind it on each tile that should answer to it. A reader's filter is anything a person picks on the page; a given stays in the model only for the reasons `malloy-model` lists (a gate, a value every query must supply, a parameter a field is defined with, scoping meant for every consumer).\n- **The model can't change, or the given is semantic or governance: import it** as above. Say so plainly: the filter shows in the reader and in the builder, and can be set in both, but the builder cannot remove or reconfigure it. It is the model's.\n\nBefore, in the model:\n\n```malloy\n##! experimental.givens\ngiven:\n manufacturer_filter :: filter<string> is f''\n subject_filter :: filter<string> is f''\n major_recall_filter :: filter<boolean> is f''\n recall_date_filter :: filter<date> is f''\n\nsource: recalls is duckdb.table('data/auto_recalls.csv') extend {\n where:\n Manufacturer ~ $manufacturer_filter,\n Subject ~ $subject_filter,\n `Major Recall` ~ $major_recall_filter,\n `Report Received Date` ~ $recall_date_filter\n dimension: `Major Recall` is ifnull(`Potentially Affected`, 0) > 100000\n}\n```\n\nAfter: the model declares no givens and `recalls` has no `where:`; the report declares the four and binds each tile:\n\n```malloy\n##! experimental.givens\n##| artifact { layout=grid title=\"Recalls\"\n tiles=[\"recalls_tiles -> recalls_by_year\", \"recalls_tiles -> recalls_by_manufacturer\"]\n} dashboard { columns=12 }\n|##\nimport { recalls } from \"auto_recalls.malloy\"\n\n# label=\"Manufacturer\" control=multiselect suggest { source=recalls dimension=Manufacturer }\ngiven: manufacturer_filter :: filter<string> is f''\n# label=\"Subject\"\ngiven: subject_filter :: filter<string> is f''\n# label=\"Major recalls only\"\ngiven: major_recall_filter :: filter<boolean> is f''\n# label=\"Filed\"\ngiven: recall_date_filter :: filter<date> is f''\n\nsource: recalls_tiles is recalls extend {\n # colspan=6\n view: recalls_by_year is by_year + {\n where:\n Manufacturer ~ $manufacturer_filter,\n Subject ~ $subject_filter,\n `Major Recall` ~ $major_recall_filter,\n `Report Received Date` ~ $recall_date_filter\n }\n # colspan=6\n view: recalls_by_manufacturer is by_manufacturer + {\n where:\n Subject ~ $subject_filter,\n `Major Recall` ~ $major_recall_filter,\n `Report Received Date` ~ $recall_date_filter\n }\n}\n```\n\nA tile view takes a name of its own: `view: by_year is by_year + { … }` is \"Cannot redefine 'by_year'\". Each tile binds only the givens it should answer to: `recalls_by_manufacturer` leaves the manufacturer filter off, since it is the breakdown by manufacturer. Every given here is a `filter<…>`, so each binds with `~`; a plain `date` or `number` given binds with `>=`, `<=` or `=` (step 4).\n\nA given has to be in the report file's own scope to be bindable: declared there or imported. Never add a `#(filter)` annotation: it is deprecated, a report renders no control for it, and every use has a `given:` form (`malloy-model` covers the migration). `malloy-model` and `docs/givens.md` cover givens themselves.\n\n## Drill\n\n`# drill` goes on a model **dimension**, never on a report:\n\n```malloy\n# drill { to=[\"category\", \"self\"] given=CATEGORY }\ndimension: category is products.category\n\n# drill { to=\"regions/west\" given=REGION }\ndimension: region is regions.region\n```\n\n- **`to=<path>`** navigates to a report, of either layout, by its package path without `.malloy`, read from the package root: `to=category` is `category.malloy` at the root, `to=\"regions/west\"` is `regions/west.malloy`. Quote a path with a `/`. **Always write the path from the package root**: a bare name is a root file, never a report of that name elsewhere.\n- **`to=self`** filters in place; two or more destinations pop a menu.\n- **Always write `given=`.** Without it the given is the dimension name verbatim, so `dimension: category` seeds `category`, not `CATEGORY`. `to=self` survives that (it folds case); `to=<path>` does not, and lands on an unfiltered page with nothing erroring.\n- **The destination needs a control for the given**, declared as `filter<T>`. No lint checks that half, so click it and look.\n- **Only table cells are marked drillable.** A report meant to be drilled wants at least one untagged (table) tile, grouped by the tagged dimension. A column whose rendered header another, non-drillable tile also shows loses its marking: give the two different `# label=`s.\n\n## Read the lint\n\nPackage warnings after a reload are the report's test suite. Fix all of them, and read the text rather than the severity or the count. **\"Dashboard lint stopped early, so this list is incomplete\"** means the rest of the list is partial.\n\n| Finding | Fix |\n| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `# drill … targets \"x\", which is not a report in this package` | A dead click. Write the target's path |\n| `to=self, but no model in this package declares a given \"X\"` | The clicked value has nowhere to go. Declare the given, or fix `given=` |\n| `given \"X\" suggests options from source \"y\", which this file does not define` | Import `y` into the report |\n| `filters by given \"X\", which this file does not import` | No control is shown for it. Import the given |\n| A tile that does not resolve to a real view | Fix the view name, or declare the view |\n| `notebook-artifact-unparsed` | The tag does not parse, usually an unquoted `source -> view` entry |\n| `notebook-layout-run` | A `run:` beside `tiles`. Make it a view, list it, delete the `run:` |\n| `notebook-tile-layout-ignored` | `colspan` or `break` on a linear report. Remove them, or switch to `layout=grid` |\n| `report-kind-removed` | The tag sets `kind=`, which is no longer read, so the file is no report. Write the `layout=` the finding gives |\n| `report-cells-removed` | A linear tag with no `tiles`, the retired cell format, so the file is no report. List its tiles (`reference/linear.md`) |\n| `report-on-surface` | A model `index.malloy` imports (or `explores` lists) is tagged, so its sources left the surface. Untag it, or move the report out |\n| `index-is-not-a-report` | The root `index.malloy` carries a tag. Remove it; put the report in its own file |\n| `report-name-ambiguous` | Two grid reports share a file name, so the deprecated by-name dashboards route answers 409 for it. Rename one of the files |\n| `Tile … reads <source>, which index.malloy doesn't export` | Add the source to the `export { ... }` in `index.malloy`. Never add an `explores` to `publisher.json` |\n| A grid's `columns`, `colspan`, or `dashboard_columns=` findings | `reference/grid.md` |\n| Prose the reader cannot place (`notebook-markdown-attached-nowhere`, `notebook-markdown-block-unnamed`) | `reference/linear.md` |\n\n**Read the status the reload itself returns, not the listing.** One report that fails to compile fails the whole package load (424), and a package already serving keeps its previous version, so the listing still looks healthy. `GET /api/v0/status` still knows: a package serving an older model than its files appears in `loadErrors` with `stale: true`. If the reload is 200 and your report is not listed, discovery skipped it: usually a missing or misspelled artifact tag, or a file under `public/`.\n\n**A clean reload is not proof the tags are right.** The parse warning is syntax only and carries no position. That is why the last step is opening the page.\n\n## Template\n\n```malloy\n##! experimental.givens\n##\" [One-line description of the report.]\n##| artifact { layout=linear title=\"[Title]\"\n tiles=[\n intro { kind=text },\n \"main_source_tiles -> broad_tile\",\n \"main_source_tiles -> drill_tile\"\n ]\n}\n|##\nimport { main_source } from \"model.malloy\"\n\n##|(markdown) intro\n[Framing question: what are we trying to understand?]\n|##\n\nsource: main_source_tiles is main_source extend {\n view: broad_tile is [broad view]\n view: drill_tile is [drill view]\n}\n```\n\nFor a grid, write `layout=grid`, add `dashboard { columns=12 }` after the tag's closing `}`, and give every tile view a `# colspan` (`reference/grid.md`).\n\n## Editing in the Console\n\nA person can open any report in the Console's builder (**Edit** on the report page; **+ Report** on the package page writes `<slug>.malloy` at the package root). The builder splices the file rather than regenerating it, so comments and anything it does not model survive; its layout switch rewrites `layout=` and nothing else. A file written as `run:` cells, the retired linear form, is no report and does not open (`reference/linear.md`). A save whose text declares a real `#(authorize)` or `#(access_filter)` gate outside prose is refused with a 400: gates live in a model file the report imports.\n\n## What \"done\" means\n\n- The file is at the package root (or where the author asked), and its tag says `layout=`.\n- Every source, view, and field name came from the model you read in step 1.\n- The reload returned **200**, not 424, with **zero** report warnings, and your report is in `GET …/reports`.\n- You opened the page and every tile shows real numbers: not stuck loading, not an error, not an empty state you did not intend.\n- Each control renders as the widget you intended, and changing one changes the numbers.\n- If you added a `# drill`, its `to=` is a path, and you clicked it and landed where you meant to, with the given seeded.\n- On a grid: every tile carries a colspan, each row sums to `columns`, rows end flush, nothing is clipped, and no legend or card label ends in `…`.\n\n## Reference\n\n- `reference/grid.md`: the grid recipe, its traps, and one query as the whole page.\n- `reference/linear.md`: linear reports, rewriting a retired `run:`-cell file, legacy `.malloynb`, style, and common mistakes.\n- `docs/reports.md`: the full guide this skill condenses.\n- `docs/givens.md`: the givens the controls are generated from.\n\n## Reference files over MCP\n\nThis skill's `reference/` files are served as separate prompts, one per file, fetched only when you ask for them. Where the text above says to read `reference/<name>.md`, get the prompt named `malloy-reports/<name>` instead.\n\nAvailable: grid, linear."
|
|
360724
361007
|
},
|
|
360725
361008
|
{
|
|
360726
361009
|
name: "malloy-reports/grid",
|
|
@@ -360772,94 +361055,21 @@ source: category_tiles is order_items extend {
|
|
|
360772
361055
|
|
|
360773
361056
|
- **\`layout=linear\` is required** when the tag lists tiles; without it the file is read as a grid.
|
|
360774
361057
|
- \`dashboard { columns }\` has no effect, and a \`colspan\` or \`break\` on a tile entry is ignored with a warning (\`notebook-tile-layout-ignored\`).
|
|
360775
|
-
- **Tiles run through the model query endpoint**,
|
|
360776
|
-
|
|
360777
|
-
## Reports written as \`run:\` cells (the older form)
|
|
360778
|
-
|
|
360779
|
-
A linear report can also be a file of \`run:\` statements with \`(markdown)\` notes between them. Publisher still reads and serves it, and a person saving it in the Console converts it to tiles. Write new reports as tiles; maintain a cell report in its own form until someone converts it. \`skill:malloy-analysis-report\` writes this form for a quick write-up.
|
|
361058
|
+
- **Tiles run through the model query endpoint**, against the report's own path, like a grid's.
|
|
360780
361059
|
|
|
360781
|
-
|
|
361060
|
+
## Files written as \`run:\` cells (retired)
|
|
360782
361061
|
|
|
360783
|
-
|
|
361062
|
+
An older linear form was a file of \`run:\` statements with \`(markdown)\` notes between them, under a tag with no \`tiles\`. Publisher no longer reads it: such a file is not a report, and the package warnings name it (\`report-cells-removed\`). Never write one. To bring one back, rewrite it as tiles:
|
|
360784
361063
|
|
|
360785
|
-
1.
|
|
360786
|
-
2.
|
|
360787
|
-
3.
|
|
360788
|
-
4. **\`given:\` uses \`NAME :: filter<T> is f''\`, bound with \`~\`, declared before first use.** Put \`##! experimental.givens\` at the top of the file.
|
|
360789
|
-
5. **Trailing prose is \`##(markdown)\`, never \`#(markdown)\` or \`#"\`.** Both belong to the statement below them, and at the end of a file there is none. An \`import\` or an \`export\` takes no annotation either, so prose above one is \`##|(markdown)\`.
|
|
360790
|
-
|
|
360791
|
-
### How the file becomes cells
|
|
360792
|
-
|
|
360793
|
-
Cells come from the file's own notes and statements, in file order (imported files add none):
|
|
360794
|
-
|
|
360795
|
-
| In the file | Cell |
|
|
360796
|
-
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
360797
|
-
| \`##!\` lines, \`## artifact\`, and every \`"\` note above the tag | the header: no cell |
|
|
360798
|
-
| \`##(markdown)\` lines (contiguous) or a \`## | (markdown)\` block, after the tag | one markdown cell |
|
|
360799
|
-
| \`#(markdown)\` or a \`# | (markdown)\`block above a\`run:\`, \`source:\`, \`query:\`, \`given:\`or\`type:\` | no cell of its own: it renders with that statement's cell |
|
|
360800
|
-
| \`run:\` with its contiguous \`#\` tag block, including \`run: <named query>\` | one query cell |
|
|
360801
|
-
| \`import\`, \`source:\`, \`query:\`, \`given:\`, \`export { … }\`, \`type:\` | one definition cell **per statement** |
|
|
360802
|
-
| a non-\`"\` \`##\` note after the tag (\`## title=…\`) | not a cell |
|
|
360803
|
-
|
|
360804
|
-
Definition cells render folded; only query cells run. Earlier spellings are still read: \`##"\`, \`##|"\`, \`##(text)\` and \`##|(text)\` notes below the tag are markdown cells, like \`(markdown)\`. Write \`(markdown)\`. A query cell runs as a _restricted query_ against the already-compiled file, so an \`import\` inside a \`run:\` is rejected (\`file imports are not permitted in a restricted query\`).
|
|
360805
|
-
|
|
360806
|
-
### A complete cell report
|
|
360807
|
-
|
|
360808
|
-
\`\`\`malloy
|
|
360809
|
-
##! experimental.givens
|
|
360810
|
-
##" One category at a time: how it sells across the year and which brands carry it.
|
|
360811
|
-
## artifact { layout=linear title="Category review" }
|
|
360812
|
-
import { order_items, products } from "storefront.malloy"
|
|
360813
|
-
|
|
360814
|
-
##|(markdown)
|
|
360815
|
-
## Category review
|
|
360816
|
-
|
|
360817
|
-
Pick a **Category** in the controls above and every chart below re-runs for it.
|
|
360818
|
-
Leave the control empty to read the whole catalog.
|
|
360819
|
-
|##
|
|
360820
|
-
|
|
360821
|
-
#(description="Narrow to one product category")
|
|
360822
|
-
# label="Category" control=select suggest { source=products dimension=category }
|
|
360823
|
-
given: CATEGORY :: filter<string> is f''
|
|
360824
|
-
|
|
360825
|
-
##(markdown) How is revenue trending for the selected category?
|
|
360826
|
-
|
|
360827
|
-
#(markdown) ### Revenue by month
|
|
360828
|
-
#" Revenue by month for the selected category
|
|
360829
|
-
# line_chart
|
|
360830
|
-
# label="Revenue by month"
|
|
360831
|
-
run: order_items -> sales_by_month + { where: category ~ $CATEGORY }
|
|
360832
|
-
|
|
360833
|
-
##(markdown) Which brands are behind those numbers?
|
|
360834
|
-
|
|
360835
|
-
#" The eight brands with the most revenue in the selected category
|
|
360836
|
-
# bar_chart
|
|
360837
|
-
# label="Top brands"
|
|
360838
|
-
run: order_items -> top_brands + { where: category ~ $CATEGORY }
|
|
360839
|
-
|
|
360840
|
-
##(markdown) Last, the best-selling products, defined once as a named query and then run.
|
|
360841
|
-
|
|
360842
|
-
query: top_products_in_category is order_items -> top_products + { where: category ~ $CATEGORY }
|
|
360843
|
-
|
|
360844
|
-
#(markdown) ### Best sellers
|
|
360845
|
-
run: top_products_in_category
|
|
360846
|
-
\`\`\`
|
|
360847
|
-
|
|
360848
|
-
The \`##|(markdown)\` block and each \`##(markdown)\` line are floating markdown cells. The two \`#(markdown)\` lines are attached: each renders as a header with the \`run:\` below it. The \`given:\` and the \`query:\` are definition cells, and each \`run:\` is a query cell.
|
|
360849
|
-
|
|
360850
|
-
### When the Console converts it
|
|
360851
|
-
|
|
360852
|
-
Saving a cell report in the builder turns each \`run:\` into a \`view:\` on a \`<source>_tiles\` extension appended to the file (a \`query:\` used by exactly one run is folded into its view), each prose note into a \`##|(markdown) text_N\` block, and leaves definitions, imports, givens and comments where they were. The first Save asks before it rewrites the file ("Convert this notebook?"); Cancel writes nothing, and the file's history in its repository is the way back. Write a cell report so it converts:
|
|
360853
|
-
|
|
360854
|
-
- Every \`run:\` is \`<source> -> <view or query body>\` with a named source. An inline \`extend\` before the arrow, a source that is not a name, or a refinement (\`q + { … }\`) of a multi-stage query is refused, and so is a run that resolves through more than ten named queries.
|
|
360855
|
-
- A chart line the editor does not model (for example \`# bar_chart { size=spark }\`) is kept, and the Viz type picker is disabled for that tile with the reason shown.
|
|
360856
|
-
- The editor opens a report **read-only**, saying why, when it cannot place cells one by one: two statements or notes on one line, text after a block closer, a comment straddling two cells, a lone carriage return (use LF or CRLF), a statement above the \`## artifact\` tag, a statement no cell can hold, or a tag value Malloy cannot read, such as a malformed date literal (\`@2024-13-01\`).
|
|
361064
|
+
1. Each floating \`##(markdown)\` note or \`##|(markdown)\` block becomes a named \`##|(markdown) name\` block, listed as \`name { kind=text }\`.
|
|
361065
|
+
2. Each \`run: <source> -> <query>\` becomes a \`view:\` on a \`<source>_tiles\` extension the report declares, listed as \`"<source>_tiles -> <view>"\`. The render tags above the \`run:\` move to the view; a \`#"\` caption becomes \`# label="…"\`. A \`run:\` of a named \`query:\` is listed by the query's name.
|
|
361066
|
+
3. List the entries in \`tiles=[…]\` in reading order, keep \`layout=linear\`, and delete every \`run:\`.
|
|
360857
361067
|
|
|
360858
361068
|
## Multiple models and cross-model joins
|
|
360859
361069
|
|
|
360860
|
-
A report compiles with **all** its \`import\`s in scope, so it can import several \`.malloy\` models, and a tile's source
|
|
361070
|
+
A report compiles with **all** its \`import\`s in scope, so it can import several \`.malloy\` models, and a tile's source can reference, and **join**, sources from different imported files. Import each model you need at the top; no re-export wrapper is required.
|
|
360861
361071
|
|
|
360862
|
-
**When the package root holds an \`index.malloy\`, tiles
|
|
361072
|
+
**When the package root holds an \`index.malloy\`, tiles read only what it exports.** A tile over a source \`index.malloy\` does not export answers 404, even when the report imports that source's file. A source the report derives from an exported one (\`source: mine is customers extend { ... }\`) still works. A tile's own source over a raw table (\`duckdb.table(...)\`) is refused. To use a source, add it to the \`export { ... }\` in \`index.malloy\`.
|
|
360863
361073
|
|
|
360864
361074
|
## Style
|
|
360865
361075
|
|
|
@@ -360868,14 +361078,20 @@ A report compiles with **all** its \`import\`s in scope, so it can import severa
|
|
|
360868
361078
|
**No data-specific insights in markdown.** Data refreshes will make them stale.
|
|
360869
361079
|
|
|
360870
361080
|
\`\`\`malloy
|
|
360871
|
-
|
|
361081
|
+
##|(markdown) spike_note
|
|
361082
|
+
WRONG: will become stale when data refreshes. Revenue spiked 23% in March.
|
|
361083
|
+
|##
|
|
360872
361084
|
|
|
360873
|
-
|
|
361085
|
+
##|(markdown) trend_q
|
|
361086
|
+
RIGHT: frames the question and lets the query answer it. How is revenue trending?
|
|
361087
|
+
|##
|
|
360874
361088
|
\`\`\`
|
|
360875
361089
|
|
|
361090
|
+
Each is a text tile, listed in \`tiles=[…]\` by name, for example \`trend_q { kind=text }\`.
|
|
361091
|
+
|
|
360876
361092
|
To add \`limit\`, \`where\`, or other options to an existing view, refine it with \`+\`: \`my_view + { limit: 15 }\`.
|
|
360877
361093
|
|
|
360878
|
-
One query per tile
|
|
361094
|
+
One query per tile, and remember a chart renders only the **first** aggregate in its result.
|
|
360879
361095
|
|
|
360880
361096
|
## Existing \`.malloynb\` files
|
|
360881
361097
|
|
|
@@ -360883,28 +361099,21 @@ A \`.malloynb\` (cells delimited by \`>>>markdown\` and \`>>>malloy\`) is a depr
|
|
|
360883
361099
|
|
|
360884
361100
|
## Common mistakes
|
|
360885
361101
|
|
|
360886
|
-
| Mistake | Fix
|
|
360887
|
-
| -------------------------------------------------------------------------- |
|
|
360888
|
-
| A tile list without \`layout=linear\` | Read as a grid. Write \`layout=linear\`.
|
|
360889
|
-
|
|
|
360890
|
-
|
|
|
360891
|
-
| \`##
|
|
360892
|
-
| \`##
|
|
360893
|
-
|
|
|
360894
|
-
| Text on the opener line after
|
|
360895
|
-
|
|
|
360896
|
-
|
|
|
360897
|
-
|
|
|
360898
|
-
| \`
|
|
360899
|
-
|
|
|
360900
|
-
|
|
|
360901
|
-
| \`tiles=[intro { kind=text }, orders_tiles -> headline]\` (entry not quoted) | Every \`source -> view\` entry is a quoted string. Unquoted, the tag does not parse. |
|
|
360902
|
-
| A bare \`intro\` in \`tiles\` for a \`## | (markdown) intro\` block | A bare name is the query-tile form for a \`query:\`. Write \`intro { kind=text }\`. |
|
|
360903
|
-
| \`colspan\` or \`break\` on a linear report's tile entry | Ignored and reported (\`notebook-tile-layout-ignored\`). Remove them, or switch to \`layout=grid\`. |
|
|
360904
|
-
| \`#(filter)\` in any form | Deprecated. Declare a \`given:\` on the model's source instead. |
|
|
360905
|
-
| \`import\` inside a \`run:\` cell | Imports are file-wide; put them at the top. |
|
|
360906
|
-
| Data-specific insights in markdown | Frame questions instead. |
|
|
360907
|
-
| A cross-model join in a quick \`skill:malloy-analysis-report\` cell | That cell renders against a single model. Write it as a report tile over a source the report declares with the join, since the report compiles with all its imports. |`
|
|
361102
|
+
| Mistake | Fix |
|
|
361103
|
+
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
361104
|
+
| A tile list without \`layout=linear\` | Read as a grid. Write \`layout=linear\`. |
|
|
361105
|
+
| A linear tag with no \`tiles=[…]\` | The retired cell format: no report (\`report-cells-removed\`). List the tiles. |
|
|
361106
|
+
| \`view_name { limit: 10 }\` | Use \`+\`: \`view_name + { limit: 10 }\` |
|
|
361107
|
+
| Floating \`##(markdown)\` prose in a tiled report | Never shown (\`notebook-markdown-block-unnamed\`). Make it a named text tile and list it. |
|
|
361108
|
+
| \`## Heading\` for a section title | That line is model tags, not prose. Write the heading inside a \`##\\|(markdown) name\` text tile. |
|
|
361109
|
+
| \`##\\| markdown\` or \`##\\|markdown\` (no parentheses) | Write \`##\\|(markdown) name\`. |
|
|
361110
|
+
| Text on the opener line after \`##\\|(markdown)\` | Only the tile's name may follow it. Put the text on the next line. |
|
|
361111
|
+
| \`run:\` in a report that lists \`tiles=[…]\` | Never shown. Define a \`view:\`, list \`"source -> view"\` in \`tiles\`, delete the \`run:\`. |
|
|
361112
|
+
| \`tiles=[intro { kind=text }, orders_tiles -> headline]\` (entry not quoted) | Every \`source -> view\` entry is a quoted string. Unquoted, the tag does not parse. |
|
|
361113
|
+
| A bare \`intro\` in \`tiles\` for a \`##\\|(markdown) intro\` block | A bare name is the query-tile form for a \`query:\`. Write \`intro { kind=text }\`. |
|
|
361114
|
+
| \`colspan\` or \`break\` on a linear report's tile entry | Ignored and reported (\`notebook-tile-layout-ignored\`). Remove them, or switch to \`layout=grid\`. |
|
|
361115
|
+
| \`#(filter)\` in any form | Deprecated. Declare a \`given:\` on the model's source instead. |
|
|
361116
|
+
| Data-specific insights in a text tile | Frame questions instead. |`
|
|
360908
361117
|
},
|
|
360909
361118
|
{
|
|
360910
361119
|
name: "malloy-review",
|
|
@@ -361107,7 +361316,7 @@ This review covers 10,173 lines across 47 files. Only ~1,500 lines were deep-rev
|
|
|
361107
361316
|
{
|
|
361108
361317
|
name: "malloy-review/rubric-structure",
|
|
361109
361318
|
description: "Rubric: Structure. Reference detail for the malloy-review skill.",
|
|
361110
|
-
body: "# Rubric: Structure\n\n**Dimension:** `structure`\n**Rules:** 5\n\nApplies to the organization of `.malloy` files within a package. Skip a file for structural review if it's a report (a `.malloy` file anywhere in the package whose model-level notes carry an `## artifact` tag, of either layout, `layout=grid` or `layout=linear` or the deprecated `kind=`, written as `tiles=[…]
|
|
361319
|
+
body: "# Rubric: Structure\n\n**Dimension:** `structure`\n**Rules:** 5\n\nApplies to the organization of `.malloy` files within a package. Skip a file for structural review if it's a report (a `.malloy` file anywhere in the package whose model-level notes carry an `## artifact` tag, of either layout, `layout=grid` or `layout=linear` or the deprecated `kind=`, written as `tiles=[…]`; a legacy query-level `# artifact` on a `query:`; or a `.malloynb`), which has its own conventions (the `malloy-reports` skill), or a one-off sandbox script. Do not flag a report for where it sits: no directory is required for one, and the package root is the default.\n\nFor every rule, the linked instruction-skill section is the canonical source for rationale and examples.\n\n---\n\n## S-01: Base vs joined source separation\n\n- **Severity:** major (non-blocking) · **Category:** structure · LLM-judgment\n- **Detection:** LLM heuristic, a source named after a base table (`customers`, `orders`) that includes cross-table joins, or a file with multiple sources where one clearly joins the others, is a candidate for splitting\n- **Fix:** move the joined source into its own file; leave the base source clean\n- **See:** `skill:malloy-model` § Base vs Joined Sources · `skill:malloy-model` § Joined Source File Template\n\n---\n\n## S-02: Sources should declare `primary_key:` when one exists\n\n- **Severity:** minor (non-blocking) · **Category:** structure · machine-checkable\n- **Why this isn't a correctness rule.** The Malloy compiler auto-synthesizes a UUID-based `__distinct_key` for any `join_one:` target without a declared PK (`packages/malloy/src/model/field_instance.ts:644-670`, `query_query.ts:946-951`), so symmetric aggregation is correct either way, see the C-07 entry in `rubric-correctness.md`'s \"Rules we dropped\" section. The `with` shortcut, which DOES require a declared PK, errors at compile time and surfaces as a diagnostic. The actual silent-correctness hazard (\"declared PK isn't actually unique in the data\") is `rubric-correctness.md` § C-12.\n- **What this rule catches.** A discoverability / hygiene gap: when a source has a natural primary key, declaring it lets downstream code use the `with` shortcut, makes grain explicit in the model, and gives tooling a stable identifier per row. Treat as a recommendation, not a merge gate.\n- **Detection:** for every `source:` declaration, check whether its body contains a `primary_key:` clause. Skip flagging when:\n - The source is a query-based / computed source (`source: x is t -> {...}` or `source: x is (t -> {...}) extend {...}`) where grain is determined by the `group_by` columns, declaring a `primary_key:` on the result is fine but not required.\n - The source represents an event/log table or a denormalized analytical source where no natural PK exists. Both situations are legitimate; the LLM should recognize them and skip the finding.\n- **Fix (when the source does have a natural PK):** declare it, `primary_key: <col>` inside `extend {}`. When there isn't a natural PK, leave it undeclared; if the absence is non-obvious, a one-line `#(doc)` on the source explaining the grain helps future readers.\n- **See:** `skill:malloy-model` § Key Rules · `rubric-correctness.md` § C-12 (the related correctness rule that checks whether a declared PK is actually unique in the data) · `rubric-style.md` § Y-03 (`join_one:` style consistency, which is the other consequence of declared-vs-undeclared PKs)\n\n---\n\n## S-03: Joined tables defined before the source that references them\n\n- **Severity:** blocker (blocking) · **Category:** structure · machine-checkable\n- **Detection:** AST, within a single file, for each source reference inside a `join_*:` or `source is <name>`, verify the target source is declared earlier\n- **Fix:** reorder source declarations so dependencies come first. If two sources reference each other, split into separate files with explicit `import` (cross-file declarations don't have this ordering constraint).\n- **See:** `skill:malloy-gotchas-modeling` § Source Order, Define Joined Tables First\n\n---\n\n## S-04: Base sources curate their column surface\n\n- **Severity:** major (non-blocking) · **Category:** structure · machine-checkable\n- **Detection:** AST, for each `source: ... is conn.table(...)` declaration (a direct-table base source), check whether the source uses **any** of the three column-curation mechanisms below. The finding fires only when the source uses none of them, every column from the underlying table is implicitly public. Query-based sources and sources derived from other sources are exempt.\n- **Fix:** pick whichever option matches the source's needs. Curation options, lightest to heaviest:\n 1. `extend { except: a, b, c }`, drop a small named set of columns. Compatible with `rename:`. No experimental flag.\n 2. `extend { accept: a, b, c }`, keep only a small named set. Compatible with `rename:`. No experimental flag.\n 3. `##! experimental.access_modifiers` + `include { public: …, internal: …, private: … }`, full per-column visibility tiers. **Composes with `rename:` in one order only** (the `extend { rename: }` before the `include {}`, which then names the field by its new name) and disallows measures/dimensions whose names shadow `internal:` columns. Use only when the visibility distinction matters (e.g., shared sources joined into multiple consumers).\n\n Most files only need option 1 or 2, for the small set of columns that shouldn't be public, prefer `except:` over the heavier `include {}` machinery. Reach for `include {}` only when per-column tiers are genuinely worth the constraints.\n- **See:** `skill:malloy-gotchas-modeling` § Field Management: `extend {}` and `include {}`, in that order · `skill:malloy-model` § Base Source Templates · `malloy-model/reference/access-modifiers.md`\n\n---\n\n## S-05: One file per table (base) / one per analytical domain (joined)\n\n- **Severity:** minor (non-blocking) · **Category:** structure · machine-checkable\n- **Detection:** count `source: X is conn.table(...)` base-source declarations per file. More than one → flag.\n- **Fix:** split each base source into its own file named after the source\n- **See:** `skill:malloy-model` § Base Source Templates (file convention is implicit; weak counterpart)\n\n---\n\n## Cross-cutting note\n\nWhen a file violates both S-01 (base+joined mixed) and S-05 (multiple base sources), surface the combined observation as a **Cross-Cutting Theme** in the review output: \"File layout suggests a restructure, see S-series findings for suggested splits.\""
|
|
361111
361320
|
},
|
|
361112
361321
|
{
|
|
361113
361322
|
name: "malloy-review/rubric-style",
|
|
@@ -361484,7 +361693,7 @@ Close with what you used, in a few lines: the agent and package, which skills yo
|
|
|
361484
361693
|
{
|
|
361485
361694
|
name: "malloy-source-unreachable",
|
|
361486
361695
|
description: `Work out why a source is missing from get_context, or why a query against it was refused with a 404 or a 403, and what to do next. Use when a source you expect is absent from discovery, when execute_query answers "No queryable source", when a query that used to work stops working, or when deciding whether a package's curation is hiding something you need.`,
|
|
361487
|
-
body: '# A source you cannot reach\n\n> **Tool names** are written bare here - `get_context`, `execute_query`, `get_status`, `compile_model`. The exact prefixed name depends on the host surface; match each against the tools you actually have.\n\nMost packages publish a curated surface, so a source being absent or refused is usually the package working as its author intended, not a fault. Read the refusal before working around it, because the three things that can refuse you need three different responses and they are told apart by the status code.\n\n## The three answers\n\n| what you got | what it means | what to do |\n| --- | --- | --- |\n| **404** that says "not on this package\'s published surface" | the name is real and the package does not publish it. It is curation, not a typo | follow the fix the message names (usually: name it in the `export { ... }` of `index.malloy` and address the query there), or query what IS published |\n| **404**, plain "No queryable source", "No queryable model" or "Query target is not queryable" | the name does not exist, or it is hidden in a model that carries an `#(authorize)` or `#(access_filter)` gate. In a gated model the two are deliberately indistinguishable, so a refusal cannot be used to probe for hidden names | check the name against `get_context`, then query something on the surface or ask the package\'s author to publish it |\n| **403**, "Access denied" | the source exists and is on the surface, but an `#(authorize)` gate did not admit you | supply the givens the gate reads, or accept that this caller may not read it |\n| **200** with zero rows | you were admitted, and an `#(access_filter)` narrowed the rows to none of them | this is a real answer. Report it as "no matching rows", never as an error |\n\nA 403 names a source, so it tells you the source exists. A plain 404 tells you nothing at all. That asymmetry is deliberate: a source that is both hidden AND gated answers a plain **404**, so you can never use a 403, or the wording of a 404, to discover that a hidden gated name is real.\n\n**One 404 is not about the source at all.** If a query that joins an exported source in its own text answers `Query target is not queryable`, check the joined source\'s givens. A source that reads a given with no default, which `index.malloy` does not import, is refused that way when joined. Queried directly, the same source answers a 400 that names the cause. Run the joining query through `compile_model`: the problem then reads "references given NAME ... which is not surfaced in this model". The fix belongs in the model, not the query, so report it to the model\'s author: `index.malloy` has to import that given, either by importing its declaring file whole or by naming the given in a selective import.\n\n## Addressing the surface, which is the most common 404\n\nA query names a model file. When a package curates its surface, only files on that surface are valid entry points, **even for a source that file declares itself**:\n\n```\nPOST .../packages/sales/models/index.malloy/query {"sourceName": "orders"} -> 200\nPOST .../packages/sales/models/orders.malloy/query {"sourceName": "orders"} -> 404\n```\n\nSame source, same package. `orders.malloy` is off the surface, so it is not an entry point.\n\n**Use the `model_path` that `get_context` gave you, verbatim.** Its `source_info.resource_id` holds `environment`, `package`, `model_path` and `source`, and those are exactly `execute_query`\'s `environmentName`, `packageName`, `modelPath` and `sourceName`. On a curated package the `model_path` is the surface file, not the file that declares the source. Substituting the declaring file because it looks more correct is how this 404 happens.\n\n## Other places a hidden source answers 404\n\nThe query route is not the only one held to the surface. On a curated package, a source or file off it also shows up as a 404 here:\n\n- **Reading a model.** `GET .../models/{path}` answers 404 for a file off the surface, with the same words as the query route. The file still exists and still compiles; read `index.malloy` instead. A model that is on the surface lists only the names it publishes, not everything it imports.\n- **A report tile.** A tile, or a filter `suggest` over a hidden source answers 404. The report itself is still listed. The package load warns once per tile, for example `Tile orders_staging -> by_flag on report overview reads orders_staging, which index.malloy doesn\'t export, so it won\'t load. Fix: add orders_staging to the export { ... } in index.malloy.` It is in the `warnings` on the package\'s own response, `GET .../packages/{pkg}`.\n
|
|
361696
|
+
body: '# A source you cannot reach\n\n> **Tool names** are written bare here - `get_context`, `execute_query`, `get_status`, `compile_model`. The exact prefixed name depends on the host surface; match each against the tools you actually have.\n\nMost packages publish a curated surface, so a source being absent or refused is usually the package working as its author intended, not a fault. Read the refusal before working around it, because the three things that can refuse you need three different responses and they are told apart by the status code.\n\n## The three answers\n\n| what you got | what it means | what to do |\n| --- | --- | --- |\n| **404** that says "not on this package\'s published surface" | the name is real and the package does not publish it. It is curation, not a typo | follow the fix the message names (usually: name it in the `export { ... }` of `index.malloy` and address the query there), or query what IS published |\n| **404**, plain "No queryable source", "No queryable model" or "Query target is not queryable" | the name does not exist, or it is hidden in a model that carries an `#(authorize)` or `#(access_filter)` gate. In a gated model the two are deliberately indistinguishable, so a refusal cannot be used to probe for hidden names | check the name against `get_context`, then query something on the surface or ask the package\'s author to publish it |\n| **403**, "Access denied" | the source exists and is on the surface, but an `#(authorize)` gate did not admit you | supply the givens the gate reads, or accept that this caller may not read it |\n| **200** with zero rows | you were admitted, and an `#(access_filter)` narrowed the rows to none of them | this is a real answer. Report it as "no matching rows", never as an error |\n\nA 403 names a source, so it tells you the source exists. A plain 404 tells you nothing at all. That asymmetry is deliberate: a source that is both hidden AND gated answers a plain **404**, so you can never use a 403, or the wording of a 404, to discover that a hidden gated name is real.\n\n**One 404 is not about the source at all.** If a query that joins an exported source in its own text answers `Query target is not queryable`, check the joined source\'s givens. A source that reads a given with no default, which `index.malloy` does not import, is refused that way when joined. Queried directly, the same source answers a 400 that names the cause. Run the joining query through `compile_model`: the problem then reads "references given NAME ... which is not surfaced in this model". The fix belongs in the model, not the query, so report it to the model\'s author: `index.malloy` has to import that given, either by importing its declaring file whole or by naming the given in a selective import.\n\n## Addressing the surface, which is the most common 404\n\nA query names a model file. When a package curates its surface, only files on that surface are valid entry points, **even for a source that file declares itself**:\n\n```\nPOST .../packages/sales/models/index.malloy/query {"sourceName": "orders"} -> 200\nPOST .../packages/sales/models/orders.malloy/query {"sourceName": "orders"} -> 404\n```\n\nSame source, same package. `orders.malloy` is off the surface, so it is not an entry point.\n\n**Use the `model_path` that `get_context` gave you, verbatim.** Its `source_info.resource_id` holds `environment`, `package`, `model_path` and `source`, and those are exactly `execute_query`\'s `environmentName`, `packageName`, `modelPath` and `sourceName`. On a curated package the `model_path` is the surface file, not the file that declares the source. Substituting the declaring file because it looks more correct is how this 404 happens.\n\n## Other places a hidden source answers 404\n\nThe query route is not the only one held to the surface. On a curated package, a source or file off it also shows up as a 404 here:\n\n- **Reading a model.** `GET .../models/{path}` answers 404 for a file off the surface, with the same words as the query route. The file still exists and still compiles; read `index.malloy` instead. A model that is on the surface lists only the names it publishes, not everything it imports.\n- **A report tile.** A tile, or a filter `suggest` over a hidden source answers 404. The report itself is still listed. The package load warns once per tile, for example `Tile orders_staging -> by_flag on report overview reads orders_staging, which index.malloy doesn\'t export, so it won\'t load. Fix: add orders_staging to the export { ... } in index.malloy.` It is in the `warnings` on the package\'s own response, `GET .../packages/{pkg}`.\n\nThe fix is the same in each case: add the source to the `export { ... }` in `index.malloy`, or use what is already published.\n\n## When the source is not in `get_context` at all\n\nWork down this list. The first three are far more common than the last.\n\n1. **It is off the surface.** The package publishes a curated set and this source is not in it. Nothing you can do from the query side; it is a package edit.\n2. **The package failed to load, or is serving a stale model.** Call `list_packages`: a package that failed to load appears with an `error`, and one serving the model it compiled *before* a failed reload carries `stale: true` and the `error` too. `get_status` has the full load errors.\n3. **You are talking to a different server than you think.** Call `list_packages` and check the environment and package names are the ones you expect. A stale `.mcp.json` outlives the server that wrote it, and another Publisher may hold that port.\n4. **Your search phrasing missed it.** Retry with a bare target (no `search_text`) to enumerate rather than rank. If it appears there, the source exists and was a ranking miss.\n\n## What curation does not do\n\n- **It does not hide fields.** A published source may `join` an unpublished one, and a query grouping by a joined field returns that field\'s values normally. Hiding a source removes it as a landing point; it does not redact columns a published source pulls in.\n- **It does not gate `/compile`.** `compile_model` is exempt, because compile is the authoring loop. A hidden source can still be compile-checked, and that is intended. The exceptions are a hidden source that is also gated, which answers 404 at compile too, and a document (text with a model-level `## artifact` tag): its tiles and cells are held to the surface, so one over a source off it comes back as a `query-not-queryable` problem rather than compiling.\n- **It is not access control.** Curation answers "what is queryable by name", not "who may query it". Identity is `#(authorize)` and `#(access_filter)`. A source is not protected by being hidden.\n\n## Reaching an unpublished source from your own model\n\nIf you are writing Malloy rather than just querying, an unpublished source is still usable, with one condition that trips people.\n\n`export { ... }` decides what an **importing file** can see, which is Malloy\'s rule and not Publisher\'s:\n\n- a file that declares **no** `export` hands an importer everything it declares, so import the file that declares the source and join it normally;\n- a file that **does** declare one hands over exactly that list. Importing a file whose `export` omits the source and then naming it fails to compile with `Reference to undefined object`.\n\nSo an unpublished source is reachable through a file that exports it, or that exports nothing, and not through a file whose `export` leaves it out. Re-export chains are fine at any depth: a source re-exported through several files stays queryable through the surface, because admission follows the declaration rather than the path taken to it.\n\n## Before you report a source as missing\n\nConfirm all three, in this order. Each one has been mistaken for "the data is not there".\n\n1. `get_status` shows the package loaded and not `stale`.\n2. `list_packages` names the environment and package you meant.\n3. An enumerating `get_context` call (a target with no `search_text`) does not list it.\n\nOnly then is it genuinely not published, and the answer to the user is that the package does not expose it, not that the data does not exist.'
|
|
361488
361697
|
}
|
|
361489
361698
|
]
|
|
361490
361699
|
};
|
|
@@ -368234,6 +368443,24 @@ function tryAcquire(sync, alreadyAcquiredError = E_ALREADY_LOCKED) {
|
|
|
368234
368443
|
// src/mcp/tools/embedding_index.ts
|
|
368235
368444
|
init_logger();
|
|
368236
368445
|
|
|
368446
|
+
// src/service/event_loop_yield.ts
|
|
368447
|
+
var DEFAULT_BUDGET_MS = 10;
|
|
368448
|
+
var budgetMs = DEFAULT_BUDGET_MS;
|
|
368449
|
+
function yieldToEventLoop() {
|
|
368450
|
+
return new Promise((resolve3) => setImmediate(resolve3));
|
|
368451
|
+
}
|
|
368452
|
+
function createEventLoopYielder() {
|
|
368453
|
+
let since = performance.now();
|
|
368454
|
+
return {
|
|
368455
|
+
async maybeYield() {
|
|
368456
|
+
if (performance.now() - since < budgetMs)
|
|
368457
|
+
return;
|
|
368458
|
+
await yieldToEventLoop();
|
|
368459
|
+
since = performance.now();
|
|
368460
|
+
}
|
|
368461
|
+
};
|
|
368462
|
+
}
|
|
368463
|
+
|
|
368237
368464
|
// src/mcp/tools/embedding_sync_queue.ts
|
|
368238
368465
|
init_logger();
|
|
368239
368466
|
|
|
@@ -369386,7 +369613,6 @@ function fingerprintOfRows(rows) {
|
|
|
369386
369613
|
return contentHash(rows.join(`
|
|
369387
369614
|
`));
|
|
369388
369615
|
}
|
|
369389
|
-
var yieldToEventLoop = () => new Promise((resolve3) => setImmediate(resolve3));
|
|
369390
369616
|
async function desiredFacetsChunked(entities, plan) {
|
|
369391
369617
|
const desired = [];
|
|
369392
369618
|
for (let i2 = 0;i2 < entities.length; i2 += FINGERPRINT_CHUNK_ENTITIES) {
|
|
@@ -372969,6 +373195,11 @@ function readGivenControlSpec(annotationTexts2) {
|
|
|
372969
373195
|
}
|
|
372970
373196
|
return spec;
|
|
372971
373197
|
}
|
|
373198
|
+
function givenDeclaresNoDefault(given) {
|
|
373199
|
+
const loose = given;
|
|
373200
|
+
const parsed = "default" in loose ? loose.default : loose._internal?.default;
|
|
373201
|
+
return parsed === undefined && loose._internal?.defaultText === undefined;
|
|
373202
|
+
}
|
|
372972
373203
|
function malloyGivenToApi(given) {
|
|
372973
373204
|
const type = given.type;
|
|
372974
373205
|
const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type === "array" && type.elementTypeDef?.type ? `${type.elementTypeDef.type}[]` : type.type;
|
|
@@ -372979,7 +373210,8 @@ function malloyGivenToApi(given) {
|
|
|
372979
373210
|
annotations: allNotes.filter((note) => !isReservedRoute(note.route) && note.route !== MARKDOWN_ROUTE).map((note) => note.text),
|
|
372980
373211
|
...readGivenControlSpec(allNotes.map((note) => note.text)),
|
|
372981
373212
|
...allNotes.some((note) => note.route === "secure") && { secure: true },
|
|
372982
|
-
default: given._internal?.defaultText
|
|
373213
|
+
default: given._internal?.defaultText,
|
|
373214
|
+
...givenDeclaresNoDefault(given) && { required: true }
|
|
372983
373215
|
};
|
|
372984
373216
|
}
|
|
372985
373217
|
function collectGivenRefs(value, into) {
|
|
@@ -373188,6 +373420,7 @@ function readReportModelFacts(modelPath, modelDef, surfacedGivenNames, gatesBySo
|
|
|
373188
373420
|
name: given.name,
|
|
373189
373421
|
type: isFilter ? `filter<${type.filterType}>` : type.type,
|
|
373190
373422
|
default: given.defaultText,
|
|
373423
|
+
...given.default === undefined && given.defaultText === undefined ? { required: true } : {},
|
|
373191
373424
|
annotations: (given.annotations?.blockNotes ?? []).concat(given.annotations?.notes ?? []).map((note) => note.text)
|
|
373192
373425
|
});
|
|
373193
373426
|
}
|
|
@@ -373339,6 +373572,7 @@ function givenSpec(declaration, facts) {
|
|
|
373339
373572
|
name: declaration.name,
|
|
373340
373573
|
type: declaration.type,
|
|
373341
373574
|
default: declaration.type.startsWith("filter<") && declaration.default !== undefined ? unwrapFilterLiteral(declaration.default) : declaration.default,
|
|
373575
|
+
...declaration.required ? { required: true } : {},
|
|
373342
373576
|
annotations: declaration.annotations.filter((text) => /^##?\(/.test(text) && !isMarkdownNote(text)),
|
|
373343
373577
|
...declaration.annotations.some((text) => /^#\(secure\)/.test(text)) && {
|
|
373344
373578
|
secure: true
|
|
@@ -380387,276 +380621,10 @@ function extractQueriesFromModelDef(modelDef) {
|
|
|
380387
380621
|
return { queries, misplacedAuthorize };
|
|
380388
380622
|
}
|
|
380389
380623
|
|
|
380390
|
-
// src/service/compile_document.ts
|
|
380391
|
-
var DECLARATION = /\b(?:source|query)\s*:\s*(`(?:[^`\\]|\\.)*`|[\p{L}_][\p{L}\p{N}_]*)\s+is\b/giu;
|
|
380392
|
-
function blankSpans(text, cells) {
|
|
380393
|
-
if (cells.length === 0)
|
|
380394
|
-
return text;
|
|
380395
|
-
const units = text.split("");
|
|
380396
|
-
for (const cell of cells) {
|
|
380397
|
-
const span = cellOffsets.get(cell);
|
|
380398
|
-
if (!span)
|
|
380399
|
-
throw new Error("restricted cell has no recorded span");
|
|
380400
|
-
const [start, end] = span;
|
|
380401
|
-
for (let i2 = start;i2 < end && i2 < units.length; i2++) {
|
|
380402
|
-
if (units[i2] !== `
|
|
380403
|
-
` && units[i2] !== "\r")
|
|
380404
|
-
units[i2] = " ";
|
|
380405
|
-
}
|
|
380406
|
-
}
|
|
380407
|
-
return units.join("");
|
|
380408
|
-
}
|
|
380409
|
-
function declaredNames(text) {
|
|
380410
|
-
const found = new Set(buildDerivationBaseMap(text).keys());
|
|
380411
|
-
for (const match of stripMalloyCommentsAndLiterals(text).matchAll(DECLARATION)) {
|
|
380412
|
-
const name = match[1];
|
|
380413
|
-
found.add(name.startsWith("`") ? name.slice(1, -1) : name);
|
|
380414
|
-
}
|
|
380415
|
-
return found;
|
|
380416
|
-
}
|
|
380417
|
-
function readsSource(prepared) {
|
|
380418
|
-
const ref = prepared._query?.structRef;
|
|
380419
|
-
if (typeof ref === "string")
|
|
380420
|
-
return ref;
|
|
380421
|
-
const named = ref;
|
|
380422
|
-
return named?.as || named?.name;
|
|
380423
|
-
}
|
|
380424
|
-
async function denied(gates, text) {
|
|
380425
|
-
try {
|
|
380426
|
-
await gates.text(text);
|
|
380427
|
-
return false;
|
|
380428
|
-
} catch (error2) {
|
|
380429
|
-
if (error2 instanceof AccessDeniedError)
|
|
380430
|
-
return true;
|
|
380431
|
-
throw error2;
|
|
380432
|
-
}
|
|
380433
|
-
}
|
|
380434
|
-
function tileProblem(expression, reason, cause) {
|
|
380435
|
-
return {
|
|
380436
|
-
code: "tile-does-not-compile",
|
|
380437
|
-
severity: "error",
|
|
380438
|
-
message: `Tile "${expression}" ${cause === "parse" ? "could not be parsed" : "does not compile"}: ${reason.replace(/\.?\s*$/, ".")} ` + `Fix: write the tile as one query over a source and a view, such as \`source -> view\` or \`source -> { group_by: field }\`.`
|
|
380439
|
-
};
|
|
380440
|
-
}
|
|
380441
|
-
function problemLine(problem) {
|
|
380442
|
-
const line = problem.at?.range?.start?.line;
|
|
380443
|
-
return line === undefined ? undefined : line + 1;
|
|
380444
|
-
}
|
|
380445
|
-
async function compileDocument(input) {
|
|
380446
|
-
const { base, source, modelName, gates } = input;
|
|
380447
|
-
const tag = readReportTagOfText(source);
|
|
380448
|
-
if (tag.status !== "report")
|
|
380449
|
-
return;
|
|
380450
|
-
const parse5 = parseNotebookText(source);
|
|
380451
|
-
if (isNotebookReaderError(parse5))
|
|
380452
|
-
return;
|
|
380453
|
-
const read = readNotebookCells(parse5, undefined, source);
|
|
380454
|
-
if (read.error) {
|
|
380455
|
-
return { problems: [cellsUnreadableProblem(read.error, "")] };
|
|
380456
|
-
}
|
|
380457
|
-
const code = read.cells.filter((cell) => cell.type === "code");
|
|
380458
|
-
const restrictedCells = new Set;
|
|
380459
|
-
const tainted = new Set;
|
|
380460
|
-
const mentionsTainted = (text) => tainted.size > 0 && [...scanIdentifiers(text)].some((name) => tainted.has(name));
|
|
380461
|
-
for (const cell of code) {
|
|
380462
|
-
if (mentionsTainted(cell.text) || await denied(gates, cell.text)) {
|
|
380463
|
-
restrictedCells.add(cell);
|
|
380464
|
-
if (cell.kind === "definition") {
|
|
380465
|
-
for (const name of declaredNames(cell.text))
|
|
380466
|
-
tainted.add(name);
|
|
380467
|
-
}
|
|
380468
|
-
}
|
|
380469
|
-
}
|
|
380470
|
-
const tileExpressions = readDocumentTileExpressions(read.annotations);
|
|
380471
|
-
const restrictedTiles = new Set;
|
|
380472
|
-
for (const expression of tileExpressions) {
|
|
380473
|
-
const text = `run: ${expression}`;
|
|
380474
|
-
if (mentionsTainted(text) || await denied(gates, text)) {
|
|
380475
|
-
restrictedTiles.add(normalizeTileExpression(expression));
|
|
380476
|
-
}
|
|
380477
|
-
}
|
|
380478
|
-
const definitions = code.filter((cell) => cell.kind === "definition" && !restrictedCells.has(cell)).map((cell) => cell.text).join(`
|
|
380479
|
-
`);
|
|
380480
|
-
const unparsedTiles = new Set;
|
|
380481
|
-
const tileProblems = [];
|
|
380482
|
-
for (const expression of tileExpressions) {
|
|
380483
|
-
const key = normalizeTileExpression(expression);
|
|
380484
|
-
if (restrictedTiles.has(key))
|
|
380485
|
-
continue;
|
|
380486
|
-
try {
|
|
380487
|
-
await gates.constructs(`${definitions}
|
|
380488
|
-
run: ${expression}`);
|
|
380489
|
-
} catch (error2) {
|
|
380490
|
-
if (!(error2 instanceof UnparseableTextError))
|
|
380491
|
-
throw error2;
|
|
380492
|
-
unparsedTiles.add(key);
|
|
380493
|
-
tileProblems.push(tileProblem(expression, error2.detail, "parse"));
|
|
380494
|
-
}
|
|
380495
|
-
}
|
|
380496
|
-
const boundaryProblems = [];
|
|
380497
|
-
const checkBoundary = async (query, line) => {
|
|
380498
|
-
try {
|
|
380499
|
-
await gates.boundary(query, definitions);
|
|
380500
|
-
} catch (error2) {
|
|
380501
|
-
if (!(error2 instanceof OffSurfaceError))
|
|
380502
|
-
throw error2;
|
|
380503
|
-
boundaryProblems.push({
|
|
380504
|
-
code: "query-not-queryable",
|
|
380505
|
-
severity: "error",
|
|
380506
|
-
message: error2.message,
|
|
380507
|
-
...line !== undefined && {
|
|
380508
|
-
at: {
|
|
380509
|
-
url: "",
|
|
380510
|
-
range: {
|
|
380511
|
-
start: { line: line - 1, character: 0 },
|
|
380512
|
-
end: { line: line - 1, character: 0 }
|
|
380513
|
-
}
|
|
380514
|
-
}
|
|
380515
|
-
}
|
|
380516
|
-
});
|
|
380517
|
-
}
|
|
380518
|
-
};
|
|
380519
|
-
for (const cell of code) {
|
|
380520
|
-
if (cell.kind === "query" && !restrictedCells.has(cell)) {
|
|
380521
|
-
await checkBoundary(cell.text, cell.startLine);
|
|
380522
|
-
}
|
|
380523
|
-
}
|
|
380524
|
-
for (const expression of tileExpressions) {
|
|
380525
|
-
const key = normalizeTileExpression(expression);
|
|
380526
|
-
if (!restrictedTiles.has(key) && !unparsedTiles.has(key)) {
|
|
380527
|
-
await checkBoundary(`run: ${expression}`, undefined);
|
|
380528
|
-
}
|
|
380529
|
-
}
|
|
380530
|
-
if (boundaryProblems.length > 0)
|
|
380531
|
-
return { problems: boundaryProblems };
|
|
380532
|
-
const readable = [
|
|
380533
|
-
...code.filter((cell) => cell.kind === "query" && !restrictedCells.has(cell)).map((cell) => cell.text),
|
|
380534
|
-
...tileExpressions.filter((expression) => {
|
|
380535
|
-
const key = normalizeTileExpression(expression);
|
|
380536
|
-
return !restrictedTiles.has(key) && !unparsedTiles.has(key);
|
|
380537
|
-
}).map((expression) => `run: ${expression}`)
|
|
380538
|
-
];
|
|
380539
|
-
for (const query of readable.length > 0 ? readable : [""]) {
|
|
380540
|
-
gates.nameVisible(query, definitions);
|
|
380541
|
-
}
|
|
380542
|
-
const compiledText = blankSpans(source, [...restrictedCells]);
|
|
380543
|
-
await gates.document(compiledText);
|
|
380544
|
-
const extended = base.extendModel(compiledText);
|
|
380545
|
-
let model;
|
|
380546
|
-
try {
|
|
380547
|
-
model = await extended.getModel();
|
|
380548
|
-
} catch (error2) {
|
|
380549
|
-
if (error2 instanceof MalloyError4)
|
|
380550
|
-
return { problems: [...tileProblems, ...error2.problems] };
|
|
380551
|
-
throw error2;
|
|
380552
|
-
}
|
|
380553
|
-
const modelDef = model._modelDef;
|
|
380554
|
-
const registry2 = modelDef.givens ?? {};
|
|
380555
|
-
const sources = extractSourcesFromModelDef(modelDef, []).sources;
|
|
380556
|
-
const settle2 = async (runnable, onDenied) => {
|
|
380557
|
-
try {
|
|
380558
|
-
await gates.compiled(runnable);
|
|
380559
|
-
} catch (error2) {
|
|
380560
|
-
if (error2 instanceof AccessDeniedError)
|
|
380561
|
-
onDenied();
|
|
380562
|
-
else
|
|
380563
|
-
throw error2;
|
|
380564
|
-
}
|
|
380565
|
-
};
|
|
380566
|
-
const compiledBoundary = async (runnable, prepared, query) => {
|
|
380567
|
-
try {
|
|
380568
|
-
await gates.boundaryCompiled(runnable, readsSource(prepared), query, definitions);
|
|
380569
|
-
} catch (error2) {
|
|
380570
|
-
if (!(error2 instanceof OffSurfaceError))
|
|
380571
|
-
throw error2;
|
|
380572
|
-
boundaryProblems.push({
|
|
380573
|
-
code: "query-not-queryable",
|
|
380574
|
-
severity: "error",
|
|
380575
|
-
message: error2.message
|
|
380576
|
-
});
|
|
380577
|
-
}
|
|
380578
|
-
};
|
|
380579
|
-
for (const cell of code) {
|
|
380580
|
-
if (cell.kind !== "query" || restrictedCells.has(cell))
|
|
380581
|
-
continue;
|
|
380582
|
-
const runnable = extended.loadRestrictedQuery(cell.text);
|
|
380583
|
-
let denied2 = false;
|
|
380584
|
-
await settle2(runnable, () => {
|
|
380585
|
-
denied2 = true;
|
|
380586
|
-
restrictedCells.add(cell);
|
|
380587
|
-
});
|
|
380588
|
-
if (denied2)
|
|
380589
|
-
continue;
|
|
380590
|
-
try {
|
|
380591
|
-
await compiledBoundary(runnable, await runnable.getPreparedQuery(), cell.text);
|
|
380592
|
-
} catch (error2) {
|
|
380593
|
-
if (!(error2 instanceof MalloyError4))
|
|
380594
|
-
throw error2;
|
|
380595
|
-
}
|
|
380596
|
-
}
|
|
380597
|
-
const compiledTiles = await compileTileGivens(tileExpressions, { loadQuery: (text) => extended.loadRestrictedQuery(text) }, registry2, (sourceName) => gateGivenSource(sources, sourceName), new Set([...restrictedTiles, ...unparsedTiles]), async (tile, prepared) => {
|
|
380598
|
-
let denied2 = false;
|
|
380599
|
-
await settle2({ getPreparedQuery: async () => prepared }, () => {
|
|
380600
|
-
denied2 = true;
|
|
380601
|
-
restrictedTiles.add(normalizeTileExpression(tile));
|
|
380602
|
-
});
|
|
380603
|
-
if (!denied2) {
|
|
380604
|
-
await compiledBoundary({ getPreparedQuery: async () => prepared }, prepared, `run: ${tile}`);
|
|
380605
|
-
}
|
|
380606
|
-
}, (tile, error2) => {
|
|
380607
|
-
if (!(error2 instanceof MalloyError4))
|
|
380608
|
-
return;
|
|
380609
|
-
gates.nameVisible(`run: ${tile}`, definitions);
|
|
380610
|
-
tileProblems.push(tileProblem(tile, error2.problems.map((problem) => problem.message).join("; "), "compile"));
|
|
380611
|
-
});
|
|
380612
|
-
if (boundaryProblems.length > 0)
|
|
380613
|
-
return { problems: boundaryProblems };
|
|
380614
|
-
const spans = [...restrictedCells];
|
|
380615
|
-
const problems = [...tileProblems, ...model.problems].filter((problem) => {
|
|
380616
|
-
const line = problemLine(problem);
|
|
380617
|
-
return line === undefined || !spans.some((cell) => line >= cell.startLine && line <= cell.endLine);
|
|
380618
|
-
});
|
|
380619
|
-
const slug = modelName.replace(/^.*\//, "").replace(/\.malloy$/, "");
|
|
380620
|
-
const path13 = `${slug}.malloy`;
|
|
380621
|
-
const facts = readReportModelFacts(path13, modelDef, Object.values(registry2).map((given) => given.name), new Map(sources.flatMap((entry) => entry.name ? [
|
|
380622
|
-
[
|
|
380623
|
-
entry.name,
|
|
380624
|
-
gateGivenSource(sources, entry.name) ?? []
|
|
380625
|
-
]
|
|
380626
|
-
] : [])));
|
|
380627
|
-
const manifest = buildReportManifest({ ...facts, compiledTileGivens: compiledTiles }, { restrictedTiles });
|
|
380628
|
-
const hasTiles = manifest?.tiles !== undefined;
|
|
380629
|
-
const cells = read.cells.filter((cell) => !hasTiles || cell.kind === "definition").map((cell) => {
|
|
380630
|
-
const restricted = restrictedCells.has(cell);
|
|
380631
|
-
if (cell.kind === "markdown") {
|
|
380632
|
-
return { type: "markdown", kind: "markdown", text: cell.text };
|
|
380633
|
-
}
|
|
380634
|
-
return {
|
|
380635
|
-
type: "code",
|
|
380636
|
-
kind: cell.kind,
|
|
380637
|
-
text: cell.text,
|
|
380638
|
-
...cell.markdown !== undefined && { markdown: cell.markdown },
|
|
380639
|
-
...cell.proseLines && { proseLines: cell.proseLines },
|
|
380640
|
-
...cell.codeLine !== undefined && { codeLine: cell.codeLine },
|
|
380641
|
-
...cell.caption !== undefined && { caption: cell.caption },
|
|
380642
|
-
...restricted && { restricted: true }
|
|
380643
|
-
};
|
|
380644
|
-
});
|
|
380645
|
-
if (!manifest) {
|
|
380646
|
-
return { problems, document: { layout: tag.layout, cells } };
|
|
380647
|
-
}
|
|
380648
|
-
const { entryFile: _entry, ...rest } = manifest;
|
|
380649
|
-
return {
|
|
380650
|
-
problems,
|
|
380651
|
-
document: {
|
|
380652
|
-
layout: manifest.layout,
|
|
380653
|
-
manifest: { ...rest, name: slug },
|
|
380654
|
-
cells
|
|
380655
|
-
}
|
|
380656
|
-
};
|
|
380657
|
-
}
|
|
380658
|
-
|
|
380659
380624
|
// src/service/notebook_lint.ts
|
|
380625
|
+
var CELLS_REMOVED = "report-cells-removed";
|
|
380626
|
+
var LAYOUT_UNKNOWN = "report-layout-unknown";
|
|
380627
|
+
var CELLS_REMOVED_MESSAGE = "a linear report written as `run:` cells is no longer read, so this file is not a report. Fix: list its tiles in `tiles=[…]` (a text tile per prose block, a query tile per view), or remove the `## artifact` tag.";
|
|
380660
380628
|
var TILE_ENTRY_FIX = 'Fix: every `source -> view` entry in `tiles` is a quoted string (`"orders_tiles -> headline"`), and a text entry is a bare name followed by `{ kind=text }` (`intro { kind=text }`).';
|
|
380661
380629
|
var quoted = (line, max = 60) => line.trim().slice(0, max);
|
|
380662
380630
|
var asTileName = (token) => {
|
|
@@ -380689,10 +380657,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
380689
380657
|
const tag = readReportTag(tagNotes);
|
|
380690
380658
|
if (tag.status === "none")
|
|
380691
380659
|
return [];
|
|
380692
|
-
const hasTiles = tag.status === "report" && motlyTag(tagNotes)?.tag("artifact")?.has("tiles") === true;
|
|
380693
380660
|
const linear = tag.status === "report" && tag.layout === "linear";
|
|
380694
|
-
const inNotebooks = linear && !hasTiles;
|
|
380695
|
-
const layoutNotebook = linear && hasTiles;
|
|
380696
380661
|
const page = linear ? "notebook" : "dashboard";
|
|
380697
380662
|
const nodeText = (node) => node.start && node.stop ? text.slice(map[node.start.startIndex], map[node.stop.stopIndex + 1]) : "";
|
|
380698
380663
|
const lineOfNode = (node) => node.start?.line ?? 1;
|
|
@@ -380709,12 +380674,10 @@ function lintNotebookText(modelPath, text) {
|
|
|
380709
380674
|
let artifact;
|
|
380710
380675
|
let givensEnabled = false;
|
|
380711
380676
|
let firstGiven;
|
|
380712
|
-
const spans = [];
|
|
380713
380677
|
let closerLine;
|
|
380714
|
-
const aboveArtifact = [];
|
|
380715
380678
|
const modelNotes = [];
|
|
380716
380679
|
const docNotes = [];
|
|
380717
|
-
children.forEach((child
|
|
380680
|
+
children.forEach((child) => {
|
|
380718
380681
|
if (!isRuleNode(child)) {
|
|
380719
380682
|
const token = child?.symbol;
|
|
380720
380683
|
const name = token ? symbolOf(token) : undefined;
|
|
@@ -380727,16 +380690,11 @@ function lintNotebookText(modelPath, text) {
|
|
|
380727
380690
|
return;
|
|
380728
380691
|
}
|
|
380729
380692
|
closerLine = undefined;
|
|
380730
|
-
if (child.start && child.stop)
|
|
380731
|
-
spans.push([child.start.startIndex, child.stop.stopIndex]);
|
|
380732
380693
|
const group = callAccessor(child, "docAnnotations");
|
|
380733
380694
|
if (group) {
|
|
380734
380695
|
const notes = callAccessor(group, "docAnnotation") ?? [];
|
|
380735
380696
|
for (const note of notes) {
|
|
380736
380697
|
const noteText = nodeText(note);
|
|
380737
|
-
const route = routeOfNote(noteText);
|
|
380738
|
-
const first = noteText.split(`
|
|
380739
|
-
`, 1)[0];
|
|
380740
380698
|
const bodyText = modelNoteBody(noteText);
|
|
380741
380699
|
modelNotes.push(bodyText);
|
|
380742
380700
|
if (/^##!\s*experimental\b[\s\S]*\bgivens\b/.test(noteText))
|
|
@@ -380745,23 +380703,8 @@ function lintNotebookText(modelPath, text) {
|
|
|
380745
380703
|
if (!artifact && note.start && isArtifactNoteText(noteText)) {
|
|
380746
380704
|
artifact = {
|
|
380747
380705
|
text: bodyText,
|
|
380748
|
-
line: lineOfNode(note)
|
|
380749
|
-
startIndex: note.start.startIndex
|
|
380706
|
+
line: lineOfNode(note)
|
|
380750
380707
|
};
|
|
380751
|
-
} else if (!artifact && inNotebooks) {
|
|
380752
|
-
if (route === MARKDOWN_ROUTE || route === "text") {
|
|
380753
|
-
aboveArtifact.push({
|
|
380754
|
-
line: lineOfNode(note),
|
|
380755
|
-
code: "notebook-markdown-above-artifact",
|
|
380756
|
-
what: quoted(first)
|
|
380757
|
-
});
|
|
380758
|
-
} else if (!/^##!/.test(noteText) && !/^#{1,2}\|?"/.test(noteText)) {
|
|
380759
|
-
aboveArtifact.push({
|
|
380760
|
-
line: lineOfNode(note),
|
|
380761
|
-
code: "notebook-tag-above-artifact",
|
|
380762
|
-
what: quoted(first)
|
|
380763
|
-
});
|
|
380764
|
-
}
|
|
380765
380708
|
}
|
|
380766
380709
|
}
|
|
380767
380710
|
const last = notes[notes.length - 1];
|
|
@@ -380773,35 +380716,15 @@ function lintNotebookText(modelPath, text) {
|
|
|
380773
380716
|
if (callAccessor(child, "defineGivenStatement")) {
|
|
380774
380717
|
firstGiven ??= keywordLine(child, "GIVEN");
|
|
380775
380718
|
}
|
|
380776
|
-
if (
|
|
380719
|
+
if (linear && callAccessor(child, "runStatement")) {
|
|
380777
380720
|
const line = keywordLine(child, "RUN");
|
|
380778
380721
|
add(line, "notebook-layout-run", "a `run:` is never shown in a notebook written as tiles, whose cells are the entries in `tiles=[…]`. Fix: define the query as a `view:` on a source, list `source -> view` in `tiles`, and delete the `run:`.");
|
|
380779
380722
|
}
|
|
380780
380723
|
if (callAccessor(child, "ignoredObjectAnnotations")) {
|
|
380781
380724
|
const notes = leadingNotes(child);
|
|
380782
380725
|
for (const note of notes.filter((n) => isMarkdownNote(n.text))) {
|
|
380783
|
-
add(note.line, "notebook-markdown-attached-nowhere", `\`${quoted(note.text)}\` annotates no statement, since what follows it (the end of the file, an import or export, or a \`##\` model-level note) takes no annotation. Fix: ${
|
|
380784
|
-
}
|
|
380785
|
-
if (inNotebooks && notes.some((n) => !isMarkdownNote(n.text))) {
|
|
380786
|
-
const next = children[index + 1];
|
|
380787
|
-
const nextRun = children.slice(index + 1).find((sibling) => callAccessor(sibling, "runStatement"));
|
|
380788
|
-
add(lineOfNode(child), "notebook-orphaned-tag", `this # tag is followed by ${describeNext(next)}, not by a run:, so it annotates nothing. Render tags sit directly above their run:. Fix: move the tag, and any #" caption, directly above ${nextRun ? `the run: on line ${keywordLine(nextRun, "RUN")}` : "its run:"}.`);
|
|
380789
|
-
}
|
|
380790
|
-
} else if (inNotebooks && !artifact) {
|
|
380791
|
-
for (const note of leadingNotes(child).filter((n) => isMarkdownNote(n.text))) {
|
|
380792
|
-
aboveArtifact.push({
|
|
380793
|
-
line: note.line,
|
|
380794
|
-
code: "notebook-markdown-above-artifact",
|
|
380795
|
-
what: quoted(note.text)
|
|
380796
|
-
});
|
|
380726
|
+
add(note.line, "notebook-markdown-attached-nowhere", `\`${quoted(note.text)}\` annotates no statement, since what follows it (the end of the file, an import or export, or a \`##\` model-level note) takes no annotation. Fix: ${tileEntryFix("name")}, with the text as its body, or delete it.`, "error");
|
|
380797
380727
|
}
|
|
380798
|
-
const first = firstCodeToken(child);
|
|
380799
|
-
aboveArtifact.push({
|
|
380800
|
-
line: first?.line ?? lineOfNode(child),
|
|
380801
|
-
code: "notebook-statement-above-artifact",
|
|
380802
|
-
what: quoted(first ? text.slice(map[first.startIndex]).split(/\r?\n/, 1)[0] : nodeText(child).split(`
|
|
380803
|
-
`, 1)[0])
|
|
380804
|
-
});
|
|
380805
380728
|
}
|
|
380806
380729
|
});
|
|
380807
380730
|
if (!artifact)
|
|
@@ -380816,44 +380739,13 @@ function lintNotebookText(modelPath, text) {
|
|
|
380816
380739
|
leadingNoteLines.add(note.line);
|
|
380817
380740
|
lintBlocks();
|
|
380818
380741
|
lintHeadings();
|
|
380819
|
-
for (const { line, code, what } of aboveArtifact) {
|
|
380820
|
-
add(line, code, `\`${what}\` sits above the \`## artifact\` tag, and only \`##!\` flags, \`//\` comments and \`"\` notes may. Fix: move it below the artifact tag.`, "error");
|
|
380821
|
-
}
|
|
380822
380742
|
if (firstGiven !== undefined && !givensEnabled) {
|
|
380823
380743
|
add(firstGiven, "notebook-givens-not-enabled", "`given:` needs the givens experiment, which this file does not enable, so it does not compile. Fix: add the line `##! experimental.givens` at the top of the file.");
|
|
380824
380744
|
}
|
|
380825
|
-
|
|
380826
|
-
|
|
380827
|
-
|
|
380828
|
-
lintUnreferencedMarkdownBlocks(artifact);
|
|
380829
|
-
if (artifact && !inNotebooks)
|
|
380830
|
-
lintDescriptionBelow(artifact.line);
|
|
380831
|
-
if (inNotebooks && artifact)
|
|
380832
|
-
lintComments(artifact.startIndex);
|
|
380745
|
+
lintArtifact(artifact);
|
|
380746
|
+
lintUnreferencedMarkdownBlocks(artifact);
|
|
380747
|
+
lintDescriptionBelow(artifact.line);
|
|
380833
380748
|
return findings.sort((a, b3) => a.line - b3.line);
|
|
380834
|
-
function lintComments(from) {
|
|
380835
|
-
const all3 = tokens;
|
|
380836
|
-
const isComment = (t3) => t3.channel !== 0 && /COMMENT/.test(symbolOf(t3) ?? "");
|
|
380837
|
-
const endLine = (t3) => t3.line + (tokenText(t3).replace(/[\r\n]+$/, "").match(/\n/g)?.length ?? 0);
|
|
380838
|
-
const isNoteToken = (t3) => children.some((c3) => c3.start !== undefined && c3.stop !== undefined && t3.startIndex >= c3.start.startIndex && t3.stopIndex <= c3.stop.stopIndex && callAccessor(c3, "docAnnotations") !== undefined);
|
|
380839
|
-
const attached = [];
|
|
380840
|
-
for (let i2 = all3.length - 1;i2 >= 0; i2--) {
|
|
380841
|
-
const next = all3[i2 + 1];
|
|
380842
|
-
attached[i2] = next !== undefined && symbolOf(next) !== "EOF" && next.line === endLine(all3[i2]) + 1 && (isComment(next) ? attached[i2 + 1] : !isNoteToken(next));
|
|
380843
|
-
}
|
|
380844
|
-
all3.forEach((token, i2) => {
|
|
380845
|
-
if (!isComment(token) || token.startIndex < from)
|
|
380846
|
-
return;
|
|
380847
|
-
if (!attached[i2])
|
|
380848
|
-
return;
|
|
380849
|
-
const before = all3[i2 - 1];
|
|
380850
|
-
if (before && endLine(before) === token.line)
|
|
380851
|
-
return;
|
|
380852
|
-
if (spans.some(([lo, hi]) => token.startIndex >= lo && token.stopIndex <= hi))
|
|
380853
|
-
return;
|
|
380854
|
-
add(token.line, "notebook-comment-not-shown", "this comment sits directly above a cell but is not part of it, so the notebook does not show it. Fix: write it as a `##(markdown)` prose note, or move it inside the statement it describes.");
|
|
380855
|
-
});
|
|
380856
|
-
}
|
|
380857
380749
|
function leadingNotes(node) {
|
|
380858
380750
|
const from = node.start?.startIndex ?? 0;
|
|
380859
380751
|
const to2 = node.stop?.stopIndex ?? -1;
|
|
@@ -380876,24 +380768,6 @@ function lintNotebookText(modelPath, text) {
|
|
|
380876
380768
|
}
|
|
380877
380769
|
return notes;
|
|
380878
380770
|
}
|
|
380879
|
-
function describeNext(next) {
|
|
380880
|
-
if (!next)
|
|
380881
|
-
return "the end of the file";
|
|
380882
|
-
if (!isRuleNode(next)) {
|
|
380883
|
-
const token = next.symbol;
|
|
380884
|
-
return !token || symbolOf(token) === "EOF" ? "the end of the file" : `\`${tokenText(token)}\``;
|
|
380885
|
-
}
|
|
380886
|
-
if (callAccessor(next, "docAnnotations"))
|
|
380887
|
-
return "a note";
|
|
380888
|
-
const first = nodeText(next).trim().split(`
|
|
380889
|
-
`)[0].slice(0, 40);
|
|
380890
|
-
return `\`${first}\``;
|
|
380891
|
-
}
|
|
380892
|
-
function firstCodeToken(node) {
|
|
380893
|
-
const from = node.start?.startIndex ?? 0;
|
|
380894
|
-
const to2 = node.stop?.stopIndex ?? -1;
|
|
380895
|
-
return tokens?.find((t3) => t3.channel === 0 && t3.startIndex >= from && t3.stopIndex <= to2 && !/ANNOTATION/.test(symbolOf(t3) ?? ""));
|
|
380896
|
-
}
|
|
380897
380771
|
function keywordLine(node, keyword) {
|
|
380898
380772
|
const from = node.start?.startIndex ?? 0;
|
|
380899
380773
|
const to2 = node.stop?.stopIndex ?? -1;
|
|
@@ -380912,26 +380786,26 @@ function lintNotebookText(modelPath, text) {
|
|
|
380912
380786
|
const line = tokenText(token).replace(/\r?\n$/, "");
|
|
380913
380787
|
if (/^##"\S/.test(line)) {
|
|
380914
380788
|
const rest = quoted(line).slice(3);
|
|
380915
|
-
const fix = belowTag(token.line) ?
|
|
380789
|
+
const fix = belowTag(token.line) ? `Fix: ${tileEntryFix("name")}, with the text as its body.` : `Did you mean \`##" ${rest}\`?`;
|
|
380916
380790
|
add(token.line, "notebook-block-opener-spacing", `\`${quoted(line)}\` has no space after the route, so Malloy drops the note. ${fix}`);
|
|
380917
380791
|
continue;
|
|
380918
380792
|
}
|
|
380919
380793
|
if (/^##\(markdown\)\S/.test(line)) {
|
|
380920
|
-
const fix =
|
|
380794
|
+
const fix = `Fix: ${tileEntryFix("name")}, with the text as its body.`;
|
|
380921
380795
|
add(token.line, "notebook-block-opener-spacing", `\`${quoted(line)}\` has no space after the route, so Malloy drops the note. ${fix}`);
|
|
380922
380796
|
continue;
|
|
380923
380797
|
}
|
|
380924
380798
|
if (/^##\(?markdown\)?(?=[ \t]|$)/i.test(line) && !isMarkdownNote(line)) {
|
|
380925
|
-
add(token.line, "notebook-markdown-opener", `\`${quoted(line)}\` is not on the \`(markdown)\` route, so it is not shown as prose.
|
|
380799
|
+
add(token.line, "notebook-markdown-opener", `\`${quoted(line)}\` is not on the \`(markdown)\` route, so it is not shown as prose. Fix: ${tileEntryFix("name")}, with the text as its body.`);
|
|
380926
380800
|
continue;
|
|
380927
380801
|
}
|
|
380928
|
-
if (
|
|
380802
|
+
if (isMarkdownNote(line) || routeOfNote(line) === "text") {
|
|
380929
380803
|
add(token.line, "notebook-markdown-block-unnamed", `\`${quoted(line)}\` is a floating \`(${routeOfNote(line)})\` line, which a ${page} does not show, since its text tiles are named blocks listed in \`tiles=[…]\`. Fix: ${tileEntryFix("name")}, or delete the line.`);
|
|
380930
380804
|
continue;
|
|
380931
380805
|
}
|
|
380932
380806
|
const content = /^##[ \t]+(\S.*)$/.exec(line)?.[1];
|
|
380933
380807
|
if (content && /^[A-Za-z_]\w*[ \t]+[A-Za-z0-9_]/.test(content) && !/[={]/.test(content)) {
|
|
380934
|
-
const fix = !belowTag(token.line) ? 'Did you mean `##"`?' :
|
|
380808
|
+
const fix = !belowTag(token.line) ? 'Did you mean `##"`?' : `Fix: ${tileEntryFix("name")}, with the text as its body.`;
|
|
380935
380809
|
add(token.line, "notebook-heading-line", `\`${quoted(line)}\` is read as model tags, not shown as prose. ${fix}`);
|
|
380936
380810
|
}
|
|
380937
380811
|
}
|
|
@@ -380950,13 +380824,11 @@ function lintNotebookText(modelPath, text) {
|
|
|
380950
380824
|
return `this \`|##\` closes the block, so the text after it on line ${strayLine} is not prose and does not compile. Fix: a body line cannot start with \`|##\`, so reword it if the block should go on, or delete the stray text if the block is over.`;
|
|
380951
380825
|
}
|
|
380952
380826
|
function nestedMarkdownMessage(note) {
|
|
380953
|
-
return `\`${quoted(note)}\` sits inside a statement, where nothing reads a \`(markdown)\` note, so it is not shown. Fix:
|
|
380827
|
+
return `\`${quoted(note)}\` sits inside a statement, where nothing reads a \`(markdown)\` note, so it is not shown. Fix: a ${page} reads no attached note, so ${tileEntryFix("name")}, with the text as its body, or delete it.`;
|
|
380954
380828
|
}
|
|
380955
380829
|
function placement(sigil, line) {
|
|
380956
380830
|
if (sigil === "#" && !leadingNoteLines.has(line))
|
|
380957
|
-
return
|
|
380958
|
-
if (sigil === "##" && inNotebooks && !belowTag(line))
|
|
380959
|
-
return " It also sits above the `## artifact` tag, where a `(markdown)` note is not read, so put it below the tag.";
|
|
380831
|
+
return " It also sits inside a statement, and a dashboard reads no attached note, so use a text tile or delete it.";
|
|
380960
380832
|
return "";
|
|
380961
380833
|
}
|
|
380962
380834
|
function lintBlocks() {
|
|
@@ -380975,11 +380847,11 @@ function lintNotebookText(modelPath, text) {
|
|
|
380975
380847
|
const markdown = parseMarkdownOpener(opener);
|
|
380976
380848
|
const spelled = `(${markdown?.route})`;
|
|
380977
380849
|
const topLevel = sigil === "##" || leadingNoteLines.has(line);
|
|
380978
|
-
const dashboardTile = sigil === "##"
|
|
380850
|
+
const dashboardTile = sigil === "##";
|
|
380979
380851
|
const glued = /^("|\(markdown\))(\S.*)$/.exec(rest);
|
|
380980
380852
|
if (glued) {
|
|
380981
380853
|
const [, glueRoute, after] = glued;
|
|
380982
|
-
const description2 = glueRoute === '"'
|
|
380854
|
+
const description2 = glueRoute === '"';
|
|
380983
380855
|
const asTile = dashboardTile && (glueRoute !== '"' || belowTag(line));
|
|
380984
380856
|
const fix = !asTile ? `write \`${sigil}|${description2 ? '"' : "(markdown)"}\` and put \`${after}\` on the line below it` : TEXT_BLOCK_NAME.test(after) ? `put a space after the route, as in \`##|(markdown) ${after}\`, and list \`${after} { kind=text }\` in \`tiles\`` : tileBlockFix("name", after);
|
|
380985
380857
|
add(line, "notebook-block-opener-spacing", `\`${quoted(opener)}\` has no space after the route, so Malloy drops the note. Fix: ${fix}.${asTile || glueRoute === '"' ? "" : placement(sigil, line)}`);
|
|
@@ -380991,13 +380863,13 @@ function lintNotebookText(modelPath, text) {
|
|
|
380991
380863
|
add(line, "notebook-markdown-opener-text", `\`${quoted(markdown.rest)}\` is not a valid name for a \`${spelled}\` tile, which takes one bare word of letters, digits and underscores that does not start with a digit. Fix: ${tileEntryFix(asTileName(markdown.rest))}.`, "error");
|
|
380992
380864
|
} else if (opensText) {
|
|
380993
380865
|
add(line, "notebook-markdown-opener-text", `\`${quoted(markdown.rest)}\` follows \`${sigil}|${spelled}\` on its opener line, where only one bare word may go (a name), so the block would show it as its first line. Fix: ${dashboardTile ? tileBlockFix("name", markdown.rest) : "move it into the body, on the line below the opener"}.${placement(sigil, line)}`, "error");
|
|
380994
|
-
} else if (markdown.name &&
|
|
380995
|
-
add(line, "notebook-markdown-block-named", `the name \`${markdown.name}\` on this \`${spelled}\` block means nothing
|
|
380866
|
+
} else if (markdown.name && sigil === "#") {
|
|
380867
|
+
add(line, "notebook-markdown-block-named", `the name \`${markdown.name}\` on this \`${spelled}\` block means nothing on a block attached to a statement, and it is not shown. Fix: remove the name.`);
|
|
380996
380868
|
} else if (!markdown.name && dashboardTile) {
|
|
380997
380869
|
add(line, "notebook-markdown-block-unnamed", `an unnamed \`${spelled}\` block is not shown on a ${page}, whose text tiles are named blocks listed in \`tiles=[…]\`. Fix: ${tileEntryFix("name")}, or delete the block.`);
|
|
380998
380870
|
}
|
|
380999
380871
|
} else if (/^[ \t]*\(?markdown\)?(?=[ \t]|$)/i.test(rest)) {
|
|
381000
|
-
add(line, "notebook-markdown-opener", `\`${opener.trim()}\` opens a block that is not on the \`(markdown)\` route, so its body is not ${sigil === "#" ? "the statement's prose" :
|
|
380872
|
+
add(line, "notebook-markdown-opener", `\`${opener.trim()}\` opens a block that is not on the \`(markdown)\` route, so its body is not ${sigil === "#" ? "the statement's prose" : "a text tile"}. ${dashboardTile ? `Fix: ${tileEntryFix("name")}.` : `Did you mean \`${sigil}|(markdown)\`?${placement(sigil, line)}`}`);
|
|
381001
380873
|
}
|
|
381002
380874
|
let j3 = i2 + 1;
|
|
381003
380875
|
let firstRun;
|
|
@@ -381020,7 +380892,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
381020
380892
|
}
|
|
381021
380893
|
const trailing = tokenText(end).replace(closer, "").trim();
|
|
381022
380894
|
if (tokenText(end).startsWith(closer) && trailing) {
|
|
381023
|
-
const own2 = sigil !== "##" ? "" : !belowTag(end.line) ? 'on its own `##"` line, or ' :
|
|
380895
|
+
const own2 = sigil !== "##" ? "" : !belowTag(end.line) ? 'on its own `##"` line, or ' : `in its own text tile (${tileEntryFix("name")}), or `;
|
|
381024
380896
|
add(end.line, "notebook-text-after-closer", `the text after the closing \`${closer}\` (\`${trailing}\`) is dropped, not shown. Fix: put it ${own2}inside the block.`);
|
|
381025
380897
|
}
|
|
381026
380898
|
}
|
|
@@ -381036,23 +380908,35 @@ function lintNotebookText(modelPath, text) {
|
|
|
381036
380908
|
}
|
|
381037
380909
|
function lintLayout(line, declaredLayout) {
|
|
381038
380910
|
if (declaredLayout !== undefined && declaredLayout !== "grid" && declaredLayout !== "linear") {
|
|
381039
|
-
add(line,
|
|
381040
|
-
}
|
|
381041
|
-
if (tag.status === "report" && tag.decidedBy === "default") {
|
|
381042
|
-
add(line, "report-layout-implicit", "this artifact tag names no `layout` and lists no tiles, so the report is linear only by default. Fix: write `## artifact { layout=linear }` (or `layout=grid` with `tiles=[…]`).");
|
|
380911
|
+
add(line, LAYOUT_UNKNOWN, `\`layout=${declaredLayout}\` is not a layout Publisher knows (grid, linear), so the tag's other properties decide it. Fix: write \`layout=grid\` or \`layout=linear\`.`, "error");
|
|
381043
380912
|
}
|
|
381044
380913
|
}
|
|
381045
380914
|
function tagFinding(tagNote) {
|
|
381046
|
-
const [
|
|
381047
|
-
const kind = tag.status === "kind-removed" ? tag.kind : undefined;
|
|
381048
|
-
const message = kind !== undefined ? `\`kind=${kind}\` is no longer read, so this file is not a report. Fix: remove \`kind=${kind}\` and write \`layout=grid\` or \`layout=linear\`.` : `the \`## artifact\` tag does not parse${parseError !== undefined ? ` (${parseError})` : ""}, so its properties are not read and this file is not a report. ${/\btiles\s*=/.test(tagNote.text) ? TILE_ENTRY_FIX : "Fix: correct the tag so it parses."}`;
|
|
380915
|
+
const [code, message] = tagFindingText(tagNote.text);
|
|
381049
380916
|
return {
|
|
381050
380917
|
line: tagNote.line,
|
|
381051
|
-
code
|
|
380918
|
+
code,
|
|
381052
380919
|
severity: "error",
|
|
381053
380920
|
message: `Line ${tagNote.line}: ${message}`
|
|
381054
380921
|
};
|
|
381055
380922
|
}
|
|
380923
|
+
function tagFindingText(tagText2) {
|
|
380924
|
+
if (tag.status === "kind-removed")
|
|
380925
|
+
return [
|
|
380926
|
+
"report-kind-removed",
|
|
380927
|
+
`\`kind=${tag.kind}\` is no longer read, so this file is not a report. Fix: remove \`kind=${tag.kind}\` and write \`layout=grid\` or \`layout=linear\`.`
|
|
380928
|
+
];
|
|
380929
|
+
if (tag.status === "cells-removed")
|
|
380930
|
+
return tag.unknownLayout !== undefined ? [
|
|
380931
|
+
LAYOUT_UNKNOWN,
|
|
380932
|
+
`\`layout=${tag.unknownLayout}\` is not a layout Publisher knows (grid, linear), and the tag lists no tiles, so this file is not a report. Fix: write \`layout=grid\` or \`layout=linear\`, and list the tiles in \`tiles=[…]\`.`
|
|
380933
|
+
] : [CELLS_REMOVED, CELLS_REMOVED_MESSAGE];
|
|
380934
|
+
const [parseError] = motlyParseErrors([tagText2]);
|
|
380935
|
+
return [
|
|
380936
|
+
"notebook-artifact-unparsed",
|
|
380937
|
+
`the \`## artifact\` tag does not parse${parseError !== undefined ? ` (${parseError})` : ""}, so its properties are not read and this file is not a report. ${/\btiles\s*=/.test(tagText2) ? TILE_ENTRY_FIX : "Fix: correct the tag so it parses."}`
|
|
380938
|
+
];
|
|
380939
|
+
}
|
|
381056
380940
|
function lintUnreferencedMarkdownBlocks(tagNote) {
|
|
381057
380941
|
if (motlyParseErrors([tagNote.text])[0] !== undefined)
|
|
381058
380942
|
return;
|
|
@@ -381076,9 +380960,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
381076
380960
|
return;
|
|
381077
380961
|
const properties = Object.keys(tag2.dict ?? {});
|
|
381078
380962
|
lintLayout(tagNote.line, tagText(tag2, "layout"));
|
|
381079
|
-
if (
|
|
381080
|
-
return;
|
|
381081
|
-
if (layoutNotebook) {
|
|
380963
|
+
if (linear) {
|
|
381082
380964
|
for (const entry of tag2.array("tiles") ?? []) {
|
|
381083
380965
|
const ignored = ["colspan", "break"].filter((p2) => entry.has(p2));
|
|
381084
380966
|
if (ignored.length === 0)
|
|
@@ -381087,7 +380969,7 @@ function lintNotebookText(modelPath, text) {
|
|
|
381087
380969
|
}
|
|
381088
380970
|
}
|
|
381089
380971
|
const columns = motlyTag(modelNotes)?.tag("dashboard");
|
|
381090
|
-
if (
|
|
380972
|
+
if (linear && columns?.has("columns")) {
|
|
381091
380973
|
const width = tagNumeric(columns, "columns");
|
|
381092
380974
|
if (width !== 1) {
|
|
381093
380975
|
add(tagNote.line, "notebook-columns-ignored", "`dashboard { columns }` other than 1 is ignored on a notebook, which is always one column. Fix: remove `dashboard { columns }`.");
|
|
@@ -381126,6 +381008,286 @@ function notebookLintProblems(modelPath, text, url3) {
|
|
|
381126
381008
|
});
|
|
381127
381009
|
}
|
|
381128
381010
|
|
|
381011
|
+
// src/service/compile_document.ts
|
|
381012
|
+
var DECLARATION = /\b(?:source|query)\s*:\s*(`(?:[^`\\]|\\.)*`|[\p{L}_][\p{L}\p{N}_]*)\s+is\b/giu;
|
|
381013
|
+
function blankSpans(text, cells) {
|
|
381014
|
+
if (cells.length === 0)
|
|
381015
|
+
return text;
|
|
381016
|
+
const units = text.split("");
|
|
381017
|
+
for (const cell of cells) {
|
|
381018
|
+
const span = cellOffsets.get(cell);
|
|
381019
|
+
if (!span)
|
|
381020
|
+
throw new Error("restricted cell has no recorded span");
|
|
381021
|
+
const [start, end] = span;
|
|
381022
|
+
for (let i2 = start;i2 < end && i2 < units.length; i2++) {
|
|
381023
|
+
if (units[i2] !== `
|
|
381024
|
+
` && units[i2] !== "\r")
|
|
381025
|
+
units[i2] = " ";
|
|
381026
|
+
}
|
|
381027
|
+
}
|
|
381028
|
+
return units.join("");
|
|
381029
|
+
}
|
|
381030
|
+
function declaredNames(text) {
|
|
381031
|
+
const found = new Set(buildDerivationBaseMap(text).keys());
|
|
381032
|
+
for (const match of stripMalloyCommentsAndLiterals(text).matchAll(DECLARATION)) {
|
|
381033
|
+
const name = match[1];
|
|
381034
|
+
found.add(name.startsWith("`") ? name.slice(1, -1) : name);
|
|
381035
|
+
}
|
|
381036
|
+
return found;
|
|
381037
|
+
}
|
|
381038
|
+
function readsSource(prepared) {
|
|
381039
|
+
const ref = prepared._query?.structRef;
|
|
381040
|
+
if (typeof ref === "string")
|
|
381041
|
+
return ref;
|
|
381042
|
+
const named = ref;
|
|
381043
|
+
return named?.as || named?.name;
|
|
381044
|
+
}
|
|
381045
|
+
async function denied(gates, text) {
|
|
381046
|
+
try {
|
|
381047
|
+
await gates.text(text);
|
|
381048
|
+
return false;
|
|
381049
|
+
} catch (error2) {
|
|
381050
|
+
if (error2 instanceof AccessDeniedError)
|
|
381051
|
+
return true;
|
|
381052
|
+
throw error2;
|
|
381053
|
+
}
|
|
381054
|
+
}
|
|
381055
|
+
function tileProblem(expression, reason, cause) {
|
|
381056
|
+
return {
|
|
381057
|
+
code: "tile-does-not-compile",
|
|
381058
|
+
severity: "error",
|
|
381059
|
+
message: `Tile "${expression}" ${cause === "parse" ? "could not be parsed" : "does not compile"}: ${reason.replace(/\.?\s*$/, ".")} ` + `Fix: write the tile as one query over a source and a view, such as \`source -> view\` or \`source -> { group_by: field }\`.`
|
|
381060
|
+
};
|
|
381061
|
+
}
|
|
381062
|
+
function problemLine(problem) {
|
|
381063
|
+
const line = problem.at?.range?.start?.line;
|
|
381064
|
+
return line === undefined ? undefined : line + 1;
|
|
381065
|
+
}
|
|
381066
|
+
function cellsRemovedProblems(source) {
|
|
381067
|
+
const problems = notebookLintProblems("document.malloy", source, "").filter((problem) => problem.code === CELLS_REMOVED || problem.code === LAYOUT_UNKNOWN);
|
|
381068
|
+
return problems.length > 0 ? problems : [
|
|
381069
|
+
{
|
|
381070
|
+
code: CELLS_REMOVED,
|
|
381071
|
+
severity: "error",
|
|
381072
|
+
message: CELLS_REMOVED_MESSAGE
|
|
381073
|
+
}
|
|
381074
|
+
];
|
|
381075
|
+
}
|
|
381076
|
+
async function compileDocument(input) {
|
|
381077
|
+
const { base, source, modelName, gates } = input;
|
|
381078
|
+
const tag = readReportTagOfText(source);
|
|
381079
|
+
if (tag.status === "cells-removed")
|
|
381080
|
+
return { problems: cellsRemovedProblems(source) };
|
|
381081
|
+
if (tag.status !== "report")
|
|
381082
|
+
return;
|
|
381083
|
+
const parse5 = parseNotebookText(source);
|
|
381084
|
+
if (isNotebookReaderError(parse5))
|
|
381085
|
+
return;
|
|
381086
|
+
const read = readNotebookCells(parse5, undefined, source);
|
|
381087
|
+
if (read.error) {
|
|
381088
|
+
return { problems: [cellsUnreadableProblem(read.error, "")] };
|
|
381089
|
+
}
|
|
381090
|
+
const code = read.cells.filter((cell) => cell.type === "code");
|
|
381091
|
+
const restrictedCells = new Set;
|
|
381092
|
+
const tainted = new Set;
|
|
381093
|
+
const mentionsTainted = (text) => tainted.size > 0 && [...scanIdentifiers(text)].some((name) => tainted.has(name));
|
|
381094
|
+
for (const cell of code) {
|
|
381095
|
+
if (mentionsTainted(cell.text) || await denied(gates, cell.text)) {
|
|
381096
|
+
restrictedCells.add(cell);
|
|
381097
|
+
if (cell.kind === "definition") {
|
|
381098
|
+
for (const name of declaredNames(cell.text))
|
|
381099
|
+
tainted.add(name);
|
|
381100
|
+
}
|
|
381101
|
+
}
|
|
381102
|
+
}
|
|
381103
|
+
const tileExpressions = readDocumentTileExpressions(read.annotations);
|
|
381104
|
+
const restrictedTiles = new Set;
|
|
381105
|
+
for (const expression of tileExpressions) {
|
|
381106
|
+
const text = `run: ${expression}`;
|
|
381107
|
+
if (mentionsTainted(text) || await denied(gates, text)) {
|
|
381108
|
+
restrictedTiles.add(normalizeTileExpression(expression));
|
|
381109
|
+
}
|
|
381110
|
+
}
|
|
381111
|
+
const definitions = code.filter((cell) => cell.kind === "definition" && !restrictedCells.has(cell)).map((cell) => cell.text).join(`
|
|
381112
|
+
`);
|
|
381113
|
+
const unparsedTiles = new Set;
|
|
381114
|
+
const tileProblems = [];
|
|
381115
|
+
for (const expression of tileExpressions) {
|
|
381116
|
+
const key = normalizeTileExpression(expression);
|
|
381117
|
+
if (restrictedTiles.has(key))
|
|
381118
|
+
continue;
|
|
381119
|
+
try {
|
|
381120
|
+
await gates.constructs(`${definitions}
|
|
381121
|
+
run: ${expression}`);
|
|
381122
|
+
} catch (error2) {
|
|
381123
|
+
if (!(error2 instanceof UnparseableTextError))
|
|
381124
|
+
throw error2;
|
|
381125
|
+
unparsedTiles.add(key);
|
|
381126
|
+
tileProblems.push(tileProblem(expression, error2.detail, "parse"));
|
|
381127
|
+
}
|
|
381128
|
+
}
|
|
381129
|
+
const boundaryProblems = [];
|
|
381130
|
+
const checkBoundary = async (query, line) => {
|
|
381131
|
+
try {
|
|
381132
|
+
await gates.boundary(query, definitions);
|
|
381133
|
+
} catch (error2) {
|
|
381134
|
+
if (!(error2 instanceof OffSurfaceError))
|
|
381135
|
+
throw error2;
|
|
381136
|
+
boundaryProblems.push({
|
|
381137
|
+
code: "query-not-queryable",
|
|
381138
|
+
severity: "error",
|
|
381139
|
+
message: error2.message,
|
|
381140
|
+
...line !== undefined && {
|
|
381141
|
+
at: {
|
|
381142
|
+
url: "",
|
|
381143
|
+
range: {
|
|
381144
|
+
start: { line: line - 1, character: 0 },
|
|
381145
|
+
end: { line: line - 1, character: 0 }
|
|
381146
|
+
}
|
|
381147
|
+
}
|
|
381148
|
+
}
|
|
381149
|
+
});
|
|
381150
|
+
}
|
|
381151
|
+
};
|
|
381152
|
+
for (const cell of code) {
|
|
381153
|
+
if (cell.kind === "query" && !restrictedCells.has(cell)) {
|
|
381154
|
+
await checkBoundary(cell.text, cell.startLine);
|
|
381155
|
+
}
|
|
381156
|
+
}
|
|
381157
|
+
for (const expression of tileExpressions) {
|
|
381158
|
+
const key = normalizeTileExpression(expression);
|
|
381159
|
+
if (!restrictedTiles.has(key) && !unparsedTiles.has(key)) {
|
|
381160
|
+
await checkBoundary(`run: ${expression}`, undefined);
|
|
381161
|
+
}
|
|
381162
|
+
}
|
|
381163
|
+
if (boundaryProblems.length > 0)
|
|
381164
|
+
return { problems: boundaryProblems };
|
|
381165
|
+
const readable = [
|
|
381166
|
+
...code.filter((cell) => cell.kind === "query" && !restrictedCells.has(cell)).map((cell) => cell.text),
|
|
381167
|
+
...tileExpressions.filter((expression) => {
|
|
381168
|
+
const key = normalizeTileExpression(expression);
|
|
381169
|
+
return !restrictedTiles.has(key) && !unparsedTiles.has(key);
|
|
381170
|
+
}).map((expression) => `run: ${expression}`)
|
|
381171
|
+
];
|
|
381172
|
+
for (const query of readable.length > 0 ? readable : [""]) {
|
|
381173
|
+
gates.nameVisible(query, definitions);
|
|
381174
|
+
}
|
|
381175
|
+
const compiledText = blankSpans(source, [...restrictedCells]);
|
|
381176
|
+
await gates.document(compiledText);
|
|
381177
|
+
const extended = base.extendModel(compiledText);
|
|
381178
|
+
let model;
|
|
381179
|
+
try {
|
|
381180
|
+
model = await extended.getModel();
|
|
381181
|
+
} catch (error2) {
|
|
381182
|
+
if (error2 instanceof MalloyError4)
|
|
381183
|
+
return { problems: [...tileProblems, ...error2.problems] };
|
|
381184
|
+
throw error2;
|
|
381185
|
+
}
|
|
381186
|
+
const modelDef = model._modelDef;
|
|
381187
|
+
const registry2 = modelDef.givens ?? {};
|
|
381188
|
+
const sources = extractSourcesFromModelDef(modelDef, []).sources;
|
|
381189
|
+
const settle2 = async (runnable, onDenied) => {
|
|
381190
|
+
try {
|
|
381191
|
+
await gates.compiled(runnable);
|
|
381192
|
+
} catch (error2) {
|
|
381193
|
+
if (error2 instanceof AccessDeniedError)
|
|
381194
|
+
onDenied();
|
|
381195
|
+
else
|
|
381196
|
+
throw error2;
|
|
381197
|
+
}
|
|
381198
|
+
};
|
|
381199
|
+
const compiledBoundary = async (runnable, prepared, query) => {
|
|
381200
|
+
try {
|
|
381201
|
+
await gates.boundaryCompiled(runnable, readsSource(prepared), query, definitions);
|
|
381202
|
+
} catch (error2) {
|
|
381203
|
+
if (!(error2 instanceof OffSurfaceError))
|
|
381204
|
+
throw error2;
|
|
381205
|
+
boundaryProblems.push({
|
|
381206
|
+
code: "query-not-queryable",
|
|
381207
|
+
severity: "error",
|
|
381208
|
+
message: error2.message
|
|
381209
|
+
});
|
|
381210
|
+
}
|
|
381211
|
+
};
|
|
381212
|
+
for (const cell of code) {
|
|
381213
|
+
if (cell.kind !== "query" || restrictedCells.has(cell))
|
|
381214
|
+
continue;
|
|
381215
|
+
const runnable = extended.loadRestrictedQuery(cell.text);
|
|
381216
|
+
let denied2 = false;
|
|
381217
|
+
await settle2(runnable, () => {
|
|
381218
|
+
denied2 = true;
|
|
381219
|
+
restrictedCells.add(cell);
|
|
381220
|
+
});
|
|
381221
|
+
if (denied2)
|
|
381222
|
+
continue;
|
|
381223
|
+
try {
|
|
381224
|
+
await compiledBoundary(runnable, await runnable.getPreparedQuery(), cell.text);
|
|
381225
|
+
} catch (error2) {
|
|
381226
|
+
if (!(error2 instanceof MalloyError4))
|
|
381227
|
+
throw error2;
|
|
381228
|
+
}
|
|
381229
|
+
}
|
|
381230
|
+
const compiledTiles = await compileTileGivens(tileExpressions, { loadQuery: (text) => extended.loadRestrictedQuery(text) }, registry2, (sourceName) => gateGivenSource(sources, sourceName), new Set([...restrictedTiles, ...unparsedTiles]), async (tile, prepared) => {
|
|
381231
|
+
let denied2 = false;
|
|
381232
|
+
await settle2({ getPreparedQuery: async () => prepared }, () => {
|
|
381233
|
+
denied2 = true;
|
|
381234
|
+
restrictedTiles.add(normalizeTileExpression(tile));
|
|
381235
|
+
});
|
|
381236
|
+
if (!denied2) {
|
|
381237
|
+
await compiledBoundary({ getPreparedQuery: async () => prepared }, prepared, `run: ${tile}`);
|
|
381238
|
+
}
|
|
381239
|
+
}, (tile, error2) => {
|
|
381240
|
+
if (!(error2 instanceof MalloyError4))
|
|
381241
|
+
return;
|
|
381242
|
+
gates.nameVisible(`run: ${tile}`, definitions);
|
|
381243
|
+
tileProblems.push(tileProblem(tile, error2.problems.map((problem) => problem.message).join("; "), "compile"));
|
|
381244
|
+
});
|
|
381245
|
+
if (boundaryProblems.length > 0)
|
|
381246
|
+
return { problems: boundaryProblems };
|
|
381247
|
+
const spans = [...restrictedCells];
|
|
381248
|
+
const problems = [...tileProblems, ...model.problems].filter((problem) => {
|
|
381249
|
+
const line = problemLine(problem);
|
|
381250
|
+
return line === undefined || !spans.some((cell) => line >= cell.startLine && line <= cell.endLine);
|
|
381251
|
+
});
|
|
381252
|
+
const slug = modelName.replace(/^.*\//, "").replace(/\.malloy$/, "");
|
|
381253
|
+
const path13 = `${slug}.malloy`;
|
|
381254
|
+
const facts = readReportModelFacts(path13, modelDef, Object.values(registry2).map((given) => given.name), new Map(sources.flatMap((entry) => entry.name ? [
|
|
381255
|
+
[
|
|
381256
|
+
entry.name,
|
|
381257
|
+
gateGivenSource(sources, entry.name) ?? []
|
|
381258
|
+
]
|
|
381259
|
+
] : [])));
|
|
381260
|
+
const manifest = buildReportManifest({ ...facts, compiledTileGivens: compiledTiles }, { restrictedTiles });
|
|
381261
|
+
const cells = read.cells.filter((cell) => cell.kind === "definition").map((cell) => {
|
|
381262
|
+
const restricted = restrictedCells.has(cell);
|
|
381263
|
+
if (cell.kind === "markdown") {
|
|
381264
|
+
return { type: "markdown", kind: "markdown", text: cell.text };
|
|
381265
|
+
}
|
|
381266
|
+
return {
|
|
381267
|
+
type: "code",
|
|
381268
|
+
kind: cell.kind,
|
|
381269
|
+
text: cell.text,
|
|
381270
|
+
...cell.markdown !== undefined && { markdown: cell.markdown },
|
|
381271
|
+
...cell.proseLines && { proseLines: cell.proseLines },
|
|
381272
|
+
...cell.codeLine !== undefined && { codeLine: cell.codeLine },
|
|
381273
|
+
...cell.caption !== undefined && { caption: cell.caption },
|
|
381274
|
+
...restricted && { restricted: true }
|
|
381275
|
+
};
|
|
381276
|
+
});
|
|
381277
|
+
if (!manifest) {
|
|
381278
|
+
return { problems, document: { layout: tag.layout, cells } };
|
|
381279
|
+
}
|
|
381280
|
+
const { entryFile: _entry, ...rest } = manifest;
|
|
381281
|
+
return {
|
|
381282
|
+
problems,
|
|
381283
|
+
document: {
|
|
381284
|
+
layout: manifest.layout,
|
|
381285
|
+
manifest: { ...rest, name: slug },
|
|
381286
|
+
cells
|
|
381287
|
+
}
|
|
381288
|
+
};
|
|
381289
|
+
}
|
|
381290
|
+
|
|
381129
381291
|
// src/service/environment.ts
|
|
381130
381292
|
init_constants();
|
|
381131
381293
|
init_errors();
|
|
@@ -381516,7 +381678,7 @@ var storageServeRoutingCounter = lazyCounter4("publisher_storage_serve_routing_t
|
|
|
381516
381678
|
var storageTableRetainedCounter = lazyCounter4("publisher_storage_tables_retained_total", "Tables a FAILED run left in a storage= destination and deliberately did not " + "reclaim, because the source is refreshed incrementally and the name may be " + "the one it serves from. Label: destination. Not all of these are orphans — " + "a rebuild at a fresh generational name is, a seed on the live serving name " + "is not, and the manifest entry cannot separate them — so read this as an " + "upper bound on what is accumulating rather than a leak count. It is the " + "only accounting there is until reclaiming a destination exists, which is " + "why it is a counter and not just a log line: the question is a rate, not " + "whether it ever happened.");
|
|
381517
381679
|
var storageBuildFailureCounter = lazyCounter4("publisher_storage_build_failures_total", "storage= build failures (federation/passthrough/attach/CTAS), distinct from " + "in-warehouse build failures. Labels: destination (connection name), " + "reason ('build_failed'|'billed_read_not_captured'). The second is the " + "expensive one: the warehouse read ran and was charged, and the rows could " + "not be captured — so a re-drive pays for it again. Worth alerting on " + "separately from a failure that costs only a retry.");
|
|
381518
381680
|
var attributionSkippedCounter = lazyCounter4("publisher_storage_build_attribution_skipped_total", "storage= builds whose warehouse read went out UNATTRIBUTED while tagging was " + "on. Label: reason ('job_listing_unavailable'|'tag_failed'|" + "'read_row_not_found'|'read_row_ambiguous'|'cost_query_failed'). " + "The read still ran and the " + "build still succeeded — what was lost is the label in the customer's own " + "query history, and the cost on this side. Without this an operator who " + "turns tagging on and sees nothing has a single log line to go on.");
|
|
381519
|
-
var eligibilityRefusedCounter = lazyCounter4("publisher_materialization_eligibility_refused_total", "materialization-eligibility refusals, both tiers. Label: reason " + "('free_parameter'|'given_in_persisted_query'|'dynamic_projection'|" + "'dynamic_join'|'dynamic_joined_where'|'partition_without_storage'|" + "'partition_column_unknown'|'partition_column_not_public'|" + "'merge_key_scope_unresolved'|'preaggregate_over_dynamic_source'|" + "'authorize'|'partition'|'not_duckdb_portable'|" + "'public_surface_unknown'). 'given' is retained on the enum for records " + "that carry it and is no longer produced: where a given sits decides the " + "outcome, so one reason covering every placement cannot be raised.");
|
|
381681
|
+
var eligibilityRefusedCounter = lazyCounter4("publisher_materialization_eligibility_refused_total", "materialization-eligibility refusals, both tiers. Label: reason " + "('free_parameter'|'given_in_persisted_query'|'dynamic_projection'|" + "'dynamic_join'|'dynamic_joined_where'|'partition_without_storage'|" + "'partition_column_unknown'|'partition_column_not_public'|" + "'merge_key_scope_unresolved'|'preaggregate_over_dynamic_source'|" + "'authorize'|'partition'|'not_duckdb_portable'|" + "'public_surface_unknown'|'read_only_dialect'). 'given' is retained on the enum for records " + "that carry it and is no longer produced: where a given sits decides the " + "outcome, so one reason covering every placement cannot be raised.");
|
|
381520
381682
|
var serveShapeTierDropCounter = lazyCounter4("publisher_storage_serve_shape_tier_drop_total", "storage serve-shape compile escalations: a refinement tier failed to " + "compile and the riskiest category was dropped, or one or more lifted " + "entry points failed to compile and were dropped alone. Label: tier (the " + "failed tier index, 0=full; 'lifts' for dropped lifts).");
|
|
381521
381683
|
var serveShapeTypeFallbackCounter = lazyCounter4("publisher_storage_serve_shape_type_fallback_total", "Captured DuckDB column types mapped to json in the serve shape (type " + "fidelity loss). Label: kind ('array'|'unrecognized').");
|
|
381522
381684
|
var chainedStorageBuildCounter = lazyCounter4("publisher_storage_chained_build_total", "Chained storage= source builds (a source reading a storage-materialized " + "upstream). Label: outcome ('parent_reuse'|'inline_fallback'|" + "'strict_shape_fallback'|'strict_refused'|'infra_failure'). The " + "parent_reuse share is the headline signal for how far the " + "stack-on-the-parent path gets us vs recompute-from-raw.");
|
|
@@ -384549,7 +384711,16 @@ function assertMergeKeyScopeResolvable(persistSource, terms, annotationFields, w
|
|
|
384549
384711
|
message: `${what} cannot be materialized: it declares 'merge_key=' and is ` + `scoped by a term this pass cannot express as a predicate over the ` + `stored table's own columns (${unscopable.map((term) => `'${term.code}'`).join(", ")}). An incremental refresh matches rows by the merge ` + `key, and the stored table holds every caller's rows, so the match ` + `must also carry the scoping columns or it would update rows ` + `belonging to other callers. Scope this source with a term over its ` + `own columns, or drop 'merge_key=' to refresh by watermark range.`
|
|
384550
384712
|
});
|
|
384551
384713
|
}
|
|
384714
|
+
var READ_ONLY_DIALECTS = new Set(["sqlserver"]);
|
|
384552
384715
|
function assertColocatedPersistNotAuthorizeGated(persistSource, sourceName = persistSource.name, origin2 = "persist", gateOutcome, annotationFields = {}) {
|
|
384716
|
+
if (!annotationFields.storage && READ_ONLY_DIALECTS.has(persistSource.dialectName)) {
|
|
384717
|
+
recordEligibilityRefused("read_only_dialect");
|
|
384718
|
+
const what2 = origin2 === "preaggregate" ? `Pre-aggregation rollup '${sourceName}'` : `Source '${sourceName}'`;
|
|
384719
|
+
throw new MaterializationEligibilityError({
|
|
384720
|
+
reason: "read_only_dialect",
|
|
384721
|
+
message: `${what2} cannot be materialized (colocated '#@ persist'): its ` + `connection '${persistSource.connectionName}' is on the ` + `'${persistSource.dialectName}' dialect, whose connection is ` + `read-only and creates no tables, so there is nowhere in the ` + `source warehouse to write the result. The source serves live. ` + `Remove the annotation, or persist a source on a connection that ` + `can write.`
|
|
384722
|
+
});
|
|
384723
|
+
}
|
|
384553
384724
|
const partition = resolvePartitionColumns(persistSource, annotationFields);
|
|
384554
384725
|
if (!partition.ok) {
|
|
384555
384726
|
recordEligibilityRefused(partition.reason);
|
|
@@ -385488,7 +385659,7 @@ function assertEveryCallerJoinPlaced(prepared, found, isCaller) {
|
|
|
385488
385659
|
}
|
|
385489
385660
|
}
|
|
385490
385661
|
}
|
|
385491
|
-
function derivationTerminals(start, basesOf, isModelSource, maxNames = DERIVATION_CHAIN_MAX_NAMES) {
|
|
385662
|
+
function derivationTerminals(start, basesOf, isModelSource, maxNames = DERIVATION_CHAIN_MAX_NAMES, modelQuerySource) {
|
|
385492
385663
|
const seen = new Set;
|
|
385493
385664
|
const terminals = [];
|
|
385494
385665
|
const worklist = [start];
|
|
@@ -385504,8 +385675,13 @@ function derivationTerminals(start, basesOf, isModelSource, maxNames = DERIVATIO
|
|
|
385504
385675
|
continue;
|
|
385505
385676
|
}
|
|
385506
385677
|
const bases = basesOf.get(name);
|
|
385507
|
-
if (!bases || bases.size === 0)
|
|
385508
|
-
|
|
385678
|
+
if (!bases || bases.size === 0) {
|
|
385679
|
+
const querySource = modelQuerySource?.(name);
|
|
385680
|
+
if (!querySource)
|
|
385681
|
+
return { proven: false, at: name };
|
|
385682
|
+
worklist.push(querySource);
|
|
385683
|
+
continue;
|
|
385684
|
+
}
|
|
385509
385685
|
for (const base of bases)
|
|
385510
385686
|
worklist.push(base);
|
|
385511
385687
|
}
|
|
@@ -385530,7 +385706,7 @@ function resolveCallerJoin(callerJoin, compiledModelDef, region, context) {
|
|
|
385530
385706
|
if (forBoundary)
|
|
385531
385707
|
boundaryNames.add(name);
|
|
385532
385708
|
};
|
|
385533
|
-
const chainOf = (start) => derivationTerminals(start, text.derivations(), (name) => context.isModelSource(name));
|
|
385709
|
+
const chainOf = (start) => derivationTerminals(start, text.derivations(), (name) => context.isModelSource(name), undefined, (name) => context.modelQuerySource(name));
|
|
385534
385710
|
const byContentsKey = (key, forBoundary) => {
|
|
385535
385711
|
const entry = contentsStruct(compiledModelDef, key);
|
|
385536
385712
|
if (entry && isCaller(entry.location)) {
|
|
@@ -386160,7 +386336,47 @@ function fieldUsageClosure(declaring, refSummary) {
|
|
|
386160
386336
|
}
|
|
386161
386337
|
return { paths, truncated: false };
|
|
386162
386338
|
}
|
|
386163
|
-
function
|
|
386339
|
+
function isCompositePlaceholder(field) {
|
|
386340
|
+
return field.e?.node === "compositeField";
|
|
386341
|
+
}
|
|
386342
|
+
function isComposite(struct) {
|
|
386343
|
+
return struct?.type === "composite";
|
|
386344
|
+
}
|
|
386345
|
+
function compositeMembers(composite, out = [], depth = 0) {
|
|
386346
|
+
if (depth > MAX_JOIN_RECURSION_DEPTH)
|
|
386347
|
+
return out;
|
|
386348
|
+
const members = composite.sources;
|
|
386349
|
+
for (const member of members ?? []) {
|
|
386350
|
+
out.push(member);
|
|
386351
|
+
if (isComposite(member))
|
|
386352
|
+
compositeMembers(member, out, depth + 1);
|
|
386353
|
+
}
|
|
386354
|
+
return out;
|
|
386355
|
+
}
|
|
386356
|
+
function withCompositeMembersResolved(declaring, executed, composite) {
|
|
386357
|
+
if (!composite || !isComposite(declaring) || !isComposite(composite)) {
|
|
386358
|
+
return declaring;
|
|
386359
|
+
}
|
|
386360
|
+
const leaf = (member) => !isComposite(member);
|
|
386361
|
+
const declared = new Set(compositeMembers(declaring).filter(leaf).map((member) => JSON.stringify(strip(member))));
|
|
386362
|
+
const memberFields = new Set(compositeMembers(composite).filter((member) => leaf(member) && declared.has(JSON.stringify(strip(member)))).flatMap((member) => member.fields ?? []));
|
|
386363
|
+
let resolved = false;
|
|
386364
|
+
const fields = (declaring.fields ?? []).map((field) => {
|
|
386365
|
+
if (!isCompositePlaceholder(field))
|
|
386366
|
+
return field;
|
|
386367
|
+
const executedField = executed.fields?.find((f5) => activeName2(f5) === activeName2(field));
|
|
386368
|
+
if (!executedField || !memberFields.has(executedField))
|
|
386369
|
+
return field;
|
|
386370
|
+
resolved = true;
|
|
386371
|
+
return executedField;
|
|
386372
|
+
});
|
|
386373
|
+
return resolved ? { ...declaring, fields } : declaring;
|
|
386374
|
+
}
|
|
386375
|
+
function membersDeclaringCondition(composite, condition) {
|
|
386376
|
+
return compositeMembers(composite).filter((member) => member.filterList?.includes(condition));
|
|
386377
|
+
}
|
|
386378
|
+
function assertFilterConditionBindsToDeclaringSource(declaringStructAsWritten, executedStruct, condition, executedComposite = undefined) {
|
|
386379
|
+
const declaringStruct = withCompositeMembersResolved(declaringStructAsWritten, executedStruct, executedComposite);
|
|
386164
386380
|
const { paths, truncated, unrecordedJoin } = fieldUsageClosure(declaringStruct, condition.refSummary);
|
|
386165
386381
|
if (truncated) {
|
|
386166
386382
|
throw new Error("a row-security filter's field-usage closure exceeded the resolution bound");
|
|
@@ -386429,13 +386645,14 @@ function isOwnFreshFilter(struct, condition, freshness) {
|
|
|
386429
386645
|
}
|
|
386430
386646
|
return !!freshness.compiledUrl && at2.url === freshness.compiledUrl;
|
|
386431
386647
|
}
|
|
386432
|
-
function assertInheritedSourceFiltersBind(struct, modelDef, freshness = {}, resolveSibling = undefined, depth = 0, visited = new Set, alreadyProven = new Set) {
|
|
386648
|
+
function assertInheritedSourceFiltersBind(struct, modelDef, freshness = {}, resolveSibling = undefined, depth = 0, visited = new Set, alreadyProven = new Set, unresolved = undefined) {
|
|
386433
386649
|
if (visited.has(struct))
|
|
386434
386650
|
return;
|
|
386435
386651
|
if (depth > MAX_JOIN_RECURSION_DEPTH) {
|
|
386436
386652
|
throw new Error("row-security filter recursion exceeded the join-depth resolution bound");
|
|
386437
386653
|
}
|
|
386438
386654
|
visited.add(struct);
|
|
386655
|
+
const composite = isComposite(unresolved) ? unresolved : undefined;
|
|
386439
386656
|
for (const condition of struct.filterList ?? []) {
|
|
386440
386657
|
if (alreadyProven.has(condition))
|
|
386441
386658
|
continue;
|
|
@@ -386443,16 +386660,24 @@ function assertInheritedSourceFiltersBind(struct, modelDef, freshness = {}, reso
|
|
|
386443
386660
|
continue;
|
|
386444
386661
|
const origin2 = findFilterOrigin(struct, condition, modelDef, resolveSibling);
|
|
386445
386662
|
if (origin2 !== struct) {
|
|
386446
|
-
assertFilterConditionBindsToDeclaringSource(origin2, struct, condition);
|
|
386663
|
+
assertFilterConditionBindsToDeclaringSource(origin2, struct, condition, composite);
|
|
386447
386664
|
continue;
|
|
386448
386665
|
}
|
|
386449
386666
|
if (isOwnFreshFilter(struct, condition, freshness))
|
|
386450
386667
|
continue;
|
|
386668
|
+
const members = composite ? membersDeclaringCondition(composite, condition) : [];
|
|
386669
|
+
if (members.length > 0) {
|
|
386670
|
+
for (const member of members) {
|
|
386671
|
+
assertFilterConditionBindsToDeclaringSource(member, struct, condition);
|
|
386672
|
+
}
|
|
386673
|
+
continue;
|
|
386674
|
+
}
|
|
386451
386675
|
throw new Error("a row-security filter's declaring source could not be resolved");
|
|
386452
386676
|
}
|
|
386453
386677
|
for (const field of struct.fields ?? []) {
|
|
386454
386678
|
if (isJoined6(field) && isSourceDef8(field)) {
|
|
386455
|
-
|
|
386679
|
+
const unresolvedJoin = unresolved?.fields?.find((f5) => isJoined6(f5) && activeName2(f5) === activeName2(field));
|
|
386680
|
+
assertInheritedSourceFiltersBind(field, modelDef, freshness, resolveSibling, depth + 1, visited, alreadyProven, unresolvedJoin);
|
|
386456
386681
|
}
|
|
386457
386682
|
}
|
|
386458
386683
|
}
|
|
@@ -386670,6 +386895,9 @@ class Model {
|
|
|
386670
386895
|
preaggregateRollupPlans = [];
|
|
386671
386896
|
preaggregatePlansVersion = 0;
|
|
386672
386897
|
compiledSourceText;
|
|
386898
|
+
reportModelFacts;
|
|
386899
|
+
reportTagCarried;
|
|
386900
|
+
sharedQueryModels = new WeakMap;
|
|
386673
386901
|
compiledReportFacts;
|
|
386674
386902
|
settledCompiledReportFacts;
|
|
386675
386903
|
sources;
|
|
@@ -387020,13 +387248,15 @@ class Model {
|
|
|
387020
387248
|
await this.authorizeAndBindRunnable(runnable, givens, {
|
|
387021
387249
|
bypassAuthorize,
|
|
387022
387250
|
checkOnly: options?.checkOnly,
|
|
387251
|
+
compileCallerText: options?.callerRegion?.text,
|
|
387023
387252
|
callerJoins: options?.callerRegion ? {
|
|
387024
387253
|
runnable,
|
|
387025
387254
|
region: options.callerRegion
|
|
387026
387255
|
} : undefined
|
|
387027
387256
|
});
|
|
387028
387257
|
}
|
|
387029
|
-
async collectAuthorizeEntryPointGates(runnable, givens, graftScope,
|
|
387258
|
+
async collectAuthorizeEntryPointGates(runnable, givens, graftScope, options = {}) {
|
|
387259
|
+
const { skipOwnSourceGate = false, callerText } = options;
|
|
387030
387260
|
const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
|
|
387031
387261
|
if (!skipOwnSourceGate) {
|
|
387032
387262
|
await this.assertAuthorized(ownSourceName, givens, false, graftScope);
|
|
@@ -387049,8 +387279,29 @@ class Model {
|
|
|
387049
387279
|
entryPointGates = Array.from(byKey.values());
|
|
387050
387280
|
}
|
|
387051
387281
|
}
|
|
387282
|
+
if (callerText && !skipOwnSourceGate && ownSourceName) {
|
|
387283
|
+
entryPointGates = this.foldRequestDeclaredTwins(ownSourceName, callerText, entryPointGates);
|
|
387284
|
+
}
|
|
387052
387285
|
return { entryPointGates, modelDef, struct };
|
|
387053
387286
|
}
|
|
387287
|
+
foldRequestDeclaredTwins(ownSourceName, callerText, walked) {
|
|
387288
|
+
if (this.entryPointGatesBySource.has(ownSourceName))
|
|
387289
|
+
return walked;
|
|
387290
|
+
const proof = this.requestChainGates(ownSourceName, callerText);
|
|
387291
|
+
if (!proof.proven)
|
|
387292
|
+
return walked;
|
|
387293
|
+
return this.swapForOnDiskTwins(walked, proof.gatesByBase.flatMap(([, gates]) => gates));
|
|
387294
|
+
}
|
|
387295
|
+
swapForOnDiskTwins(walked, twins) {
|
|
387296
|
+
const used = new Set;
|
|
387297
|
+
return walked.map((entry) => {
|
|
387298
|
+
const twin = twins.find((t3) => !used.has(t3) && t3.route === entry.route && t3.exprs.length === entry.exprs.length && t3.exprs.every((expr, i2) => expr === entry.exprs[i2]));
|
|
387299
|
+
if (!twin)
|
|
387300
|
+
return entry;
|
|
387301
|
+
used.add(twin);
|
|
387302
|
+
return twin;
|
|
387303
|
+
});
|
|
387304
|
+
}
|
|
387054
387305
|
async assertAuthorizedFromCompiledRunnable(runnable, givens) {
|
|
387055
387306
|
await this.authorizeAndBindRunnable(runnable, givens, {
|
|
387056
387307
|
skipOwnSourceGate: true,
|
|
@@ -387068,8 +387319,8 @@ class Model {
|
|
|
387068
387319
|
queryHadRowLevelFilterAttached(runnable) {
|
|
387069
387320
|
return this.rowLevelFilteredRunnables.has(runnable);
|
|
387070
387321
|
}
|
|
387071
|
-
async probeEntryPointGates(runnable, givens, graftScope,
|
|
387072
|
-
const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope,
|
|
387322
|
+
async probeEntryPointGates(runnable, givens, graftScope, options = {}) {
|
|
387323
|
+
const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, options);
|
|
387073
387324
|
const rowLevel = [];
|
|
387074
387325
|
for (const entry of entryPointGates) {
|
|
387075
387326
|
const resolution = modelDef ? await this.resolveGateShape(entry, modelDef, graftScope) : { shape: "rejected", cause: undefined };
|
|
@@ -387118,10 +387369,10 @@ class Model {
|
|
|
387118
387369
|
return;
|
|
387119
387370
|
if (this.entryPointGatesBySource.has(entryPoint))
|
|
387120
387371
|
return;
|
|
387121
|
-
const proof = this.
|
|
387372
|
+
const proof = this.requestChainGates(entryPoint, query);
|
|
387122
387373
|
if (proof.proven) {
|
|
387123
|
-
for (const base of proof.
|
|
387124
|
-
for (const entry of
|
|
387374
|
+
for (const [base, gates] of proof.gatesByBase) {
|
|
387375
|
+
for (const entry of gates) {
|
|
387125
387376
|
const resolution = this.modelDef ? await this.resolveGateShape(entry, this.modelDef, graftScope) : { shape: "rejected", cause: undefined };
|
|
387126
387377
|
if (resolution.shape === "row_level") {
|
|
387127
387378
|
this.denyLaunderedEntryPoint(entryPoint, "row_filter_not_graftable", base);
|
|
@@ -387133,6 +387384,15 @@ class Model {
|
|
|
387133
387384
|
}
|
|
387134
387385
|
this.denyLaunderedEntryPoint(entryPoint, proof.reason, proof.at);
|
|
387135
387386
|
}
|
|
387387
|
+
requestChainGates(entryPoint, text) {
|
|
387388
|
+
const proof = this.requestChainProvesUngated(entryPoint, buildDerivationBaseMap(text));
|
|
387389
|
+
if (!proof.proven)
|
|
387390
|
+
return proof;
|
|
387391
|
+
return {
|
|
387392
|
+
proven: true,
|
|
387393
|
+
gatesByBase: proof.gatedBases.map((base) => [base, this.entryPointGatesBySource.get(base) ?? []])
|
|
387394
|
+
};
|
|
387395
|
+
}
|
|
387136
387396
|
async assertLocksOnRequestDeclaredBases(entryPoint, query, givens, bypassAuthorize) {
|
|
387137
387397
|
if (!this.declaresAnyGate())
|
|
387138
387398
|
return;
|
|
@@ -387170,7 +387430,7 @@ class Model {
|
|
|
387170
387430
|
return this.declaresAnyGateMemo;
|
|
387171
387431
|
}
|
|
387172
387432
|
requestChainProvesUngated(entryPoint, basesOf) {
|
|
387173
|
-
const chain = derivationTerminals(entryPoint, basesOf, (name) => this.entryPointGatesBySource.has(name), REQUEST_CHAIN_MAX_NAMES);
|
|
387433
|
+
const chain = derivationTerminals(entryPoint, basesOf, (name) => this.entryPointGatesBySource.has(name), REQUEST_CHAIN_MAX_NAMES, (name) => this.modelQuerySource(name));
|
|
387174
387434
|
if (!chain.proven) {
|
|
387175
387435
|
return {
|
|
387176
387436
|
proven: false,
|
|
@@ -387183,6 +387443,9 @@ class Model {
|
|
|
387183
387443
|
gatedBases: chain.terminals.filter((name) => (this.entryPointGatesBySource.get(name)?.length ?? 0) > 0)
|
|
387184
387444
|
};
|
|
387185
387445
|
}
|
|
387446
|
+
modelQuerySource(name) {
|
|
387447
|
+
return this.queries?.find((q3) => q3.name === name)?.sourceName;
|
|
387448
|
+
}
|
|
387186
387449
|
resolvedCallerJoins(runnable, region) {
|
|
387187
387450
|
const cached4 = this.callerJoinResolutions.get(runnable);
|
|
387188
387451
|
if (cached4 && cached4.region === region)
|
|
@@ -387207,6 +387470,7 @@ class Model {
|
|
|
387207
387470
|
const context = {
|
|
387208
387471
|
isModelSource: (name) => this.entryPointGatesBySource.has(name),
|
|
387209
387472
|
isModelQuery: (name) => this.modelDef?.contents[name]?.type === "query",
|
|
387473
|
+
modelQuerySource: (name) => this.modelQuerySource(name),
|
|
387210
387474
|
authorUrls
|
|
387211
387475
|
};
|
|
387212
387476
|
return collectCallerJoins(prepared, region, authorUrls).map((callerJoin) => ({
|
|
@@ -387248,7 +387512,9 @@ class Model {
|
|
|
387248
387512
|
async decideCallerSource(source, compiledModelDef, alias, givens, graftScope) {
|
|
387249
387513
|
const gates = this.collectEntryPointGates(source.struct, compiledModelDef, new Set, true);
|
|
387250
387514
|
if (gates.length > 0) {
|
|
387251
|
-
|
|
387515
|
+
const twins = source.chain.proven ? source.chain.terminals.flatMap((base) => this.entryPointGatesBySource.get(base) ?? []) : [];
|
|
387516
|
+
const swapped = this.swapForOnDiskTwins(gates, twins);
|
|
387517
|
+
return Promise.all(swapped.map((entry, i2) => entry === gates[i2] ? this.resolveGateShape(entry, compiledModelDef, graftScope) : this.resolveGateShape(entry, this.modelDef ?? compiledModelDef, graftScope)));
|
|
387252
387518
|
}
|
|
387253
387519
|
if (!source.chain.proven) {
|
|
387254
387520
|
this.denyCallerJoin(alias, "chain_not_established", source.chain.at);
|
|
@@ -387450,7 +387716,10 @@ ${query}`;
|
|
|
387450
387716
|
return runnable;
|
|
387451
387717
|
}
|
|
387452
387718
|
const graftScope = options?.graftScope ?? this.defaultGraftScope();
|
|
387453
|
-
const entryPointRowLevel = await this.probeEntryPointGates(runnable, givens, graftScope,
|
|
387719
|
+
const entryPointRowLevel = await this.probeEntryPointGates(runnable, givens, graftScope, {
|
|
387720
|
+
skipOwnSourceGate: options?.skipOwnSourceGate,
|
|
387721
|
+
callerText: options?.compileCallerText ?? options?.callerQueryText
|
|
387722
|
+
});
|
|
387454
387723
|
if (entryPointRowLevel.length === 0) {
|
|
387455
387724
|
if (options?.callerQueryText && !options?.skipOwnSourceGate) {
|
|
387456
387725
|
await this.assertRequestDeclaredEntryPointIsNotLaundered(runnable, options.callerQueryText, givens, graftScope);
|
|
@@ -387480,6 +387749,7 @@ ${query}`;
|
|
|
387480
387749
|
g2
|
|
387481
387750
|
])).values());
|
|
387482
387751
|
const graftedMaterializer = this.getOrBuildGraftedMaterializer(grafts, graftScope);
|
|
387752
|
+
await this.ensureSharedQueryModel(graftedMaterializer);
|
|
387483
387753
|
const recompiled = options.recompile(graftedMaterializer, grafts);
|
|
387484
387754
|
if (options.proveGraft) {
|
|
387485
387755
|
if (callerJoinRowLevel.length > 0) {
|
|
@@ -387644,14 +387914,15 @@ ${query}`;
|
|
|
387644
387914
|
if (!target)
|
|
387645
387915
|
return;
|
|
387646
387916
|
try {
|
|
387647
|
-
assertInheritedSourceFiltersBind(target, modelDef, { compiledUrl, queryLocation }, this.siblingModelDefResolver, undefined, undefined, alreadyProven);
|
|
387917
|
+
assertInheritedSourceFiltersBind(target, modelDef, { compiledUrl, queryLocation }, this.siblingModelDefResolver, undefined, undefined, alreadyProven, struct);
|
|
387648
387918
|
} catch (err) {
|
|
387649
387919
|
recordRowLevelGateDecision("denied_by_gate", site);
|
|
387650
387920
|
logger.debug("Inherited source filter binding check failed; denying", {
|
|
387651
387921
|
modelPath: this.modelPath,
|
|
387652
387922
|
error: err instanceof Error ? err.message : String(err)
|
|
387653
387923
|
});
|
|
387654
|
-
const
|
|
387924
|
+
const named = struct ?? target;
|
|
387925
|
+
const label = named.as || named.name;
|
|
387655
387926
|
throw new AccessDeniedError(`Access denied for source "${label}".`);
|
|
387656
387927
|
}
|
|
387657
387928
|
}
|
|
@@ -387946,6 +388217,27 @@ ${query}`;
|
|
|
387946
388217
|
model.preparedPreaggregateCompanion = data.preaggregateCompanion;
|
|
387947
388218
|
return model;
|
|
387948
388219
|
}
|
|
388220
|
+
ensureSharedQueryModel(materializer) {
|
|
388221
|
+
if (!materializer)
|
|
388222
|
+
return Promise.resolve();
|
|
388223
|
+
let built = this.sharedQueryModels.get(materializer);
|
|
388224
|
+
if (!built) {
|
|
388225
|
+
built = (async () => {
|
|
388226
|
+
try {
|
|
388227
|
+
const base = await materializer.getModel();
|
|
388228
|
+
base.queryModel;
|
|
388229
|
+
} catch (error2) {
|
|
388230
|
+
logger.warn("Could not build a shared QueryModel; each query builds its own", {
|
|
388231
|
+
packageName: this.packageName,
|
|
388232
|
+
modelPath: this.modelPath,
|
|
388233
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
388234
|
+
});
|
|
388235
|
+
}
|
|
388236
|
+
})();
|
|
388237
|
+
this.sharedQueryModels.set(materializer, built);
|
|
388238
|
+
}
|
|
388239
|
+
return built;
|
|
388240
|
+
}
|
|
387949
388241
|
static fromCompilationError(packageName, modelPath, modelType, error2, sourceText) {
|
|
387950
388242
|
const model = new Model(packageName, modelPath, {}, modelType, undefined, undefined, undefined, undefined, undefined, undefined, error2);
|
|
387951
388243
|
model.compiledSourceText = sourceText;
|
|
@@ -388029,6 +388321,11 @@ ${query}`;
|
|
|
388029
388321
|
return this.compiledSourceText;
|
|
388030
388322
|
}
|
|
388031
388323
|
getReportModelFacts() {
|
|
388324
|
+
return (this.reportModelFacts ??= {
|
|
388325
|
+
value: this.deriveReportModelFacts()
|
|
388326
|
+
}).value;
|
|
388327
|
+
}
|
|
388328
|
+
deriveReportModelFacts() {
|
|
388032
388329
|
if (!this.modelDef)
|
|
388033
388330
|
return;
|
|
388034
388331
|
return readReportModelFacts(this.modelPath, this.modelDef, (this.givens ?? []).map((given) => given.name).filter((name) => name !== undefined), new Map((this.sources ?? []).flatMap((source) => source.name ? [
|
|
@@ -388264,6 +388561,9 @@ ${query}`;
|
|
|
388264
388561
|
return this.modelPath.endsWith(NOTEBOOK_FILE_SUFFIX) ? "malloynb" : "malloy";
|
|
388265
388562
|
}
|
|
388266
388563
|
carriesReportTag() {
|
|
388564
|
+
return this.reportTagCarried ??= this.deriveCarriesReportTag();
|
|
388565
|
+
}
|
|
388566
|
+
deriveCarriesReportTag() {
|
|
388267
388567
|
if (!isReportEligiblePath(this.modelPath))
|
|
388268
388568
|
return false;
|
|
388269
388569
|
const facts = this.getReportModelFacts();
|
|
@@ -388365,7 +388665,9 @@ ${query}`;
|
|
|
388365
388665
|
];
|
|
388366
388666
|
const findings = [];
|
|
388367
388667
|
const carriesReportTag = this.carriesReportTag();
|
|
388668
|
+
const yielder = createEventLoopYielder();
|
|
388368
388669
|
for (const { label, result } of prepared) {
|
|
388670
|
+
await yielder.maybeYield();
|
|
388369
388671
|
const logs = filterPublisherOwnedRenderLogs(validateRenderTags2(result), carriesReportTag);
|
|
388370
388672
|
if (logs.length > 0) {
|
|
388371
388673
|
logger.warn(`Render tag findings on '${label}': ${logs.map((e2) => `[${e2.severity}] ${e2.message}`).join("; ")}`);
|
|
@@ -388382,8 +388684,11 @@ ${query}`;
|
|
|
388382
388684
|
}
|
|
388383
388685
|
preparedRenderTagResults;
|
|
388384
388686
|
async prepareRenderTagTargets(mm, targets) {
|
|
388687
|
+
await this.ensureSharedQueryModel(mm);
|
|
388688
|
+
const yielder = createEventLoopYielder();
|
|
388385
388689
|
const prepared = [];
|
|
388386
388690
|
for (const target of targets) {
|
|
388691
|
+
await yielder.maybeYield();
|
|
388387
388692
|
try {
|
|
388388
388693
|
const result = await mm.loadQuery(target.queryString).getPreparedResult();
|
|
388389
388694
|
prepared.push({
|
|
@@ -388459,48 +388764,19 @@ ${query}`;
|
|
|
388459
388764
|
const read = readNotebookCells(parse5, modelDef, text);
|
|
388460
388765
|
if (read.error)
|
|
388461
388766
|
return refuse(read.error);
|
|
388462
|
-
const anonymousQueries = this.modelInfo?.anonymous_queries ?? [];
|
|
388463
388767
|
this.notebookAnnotations = read.annotations;
|
|
388464
388768
|
const layout = this.readNotebookLayout();
|
|
388465
388769
|
this.notebookLayout = layout;
|
|
388466
|
-
const
|
|
388467
|
-
|
|
388468
|
-
|
|
388469
|
-
|
|
388470
|
-
|
|
388471
|
-
|
|
388472
|
-
|
|
388473
|
-
|
|
388474
|
-
|
|
388475
|
-
|
|
388476
|
-
markdown: cell.markdown,
|
|
388477
|
-
proseLines: cell.proseLines,
|
|
388478
|
-
codeLine: cell.codeLine,
|
|
388479
|
-
caption: cell.caption,
|
|
388480
|
-
modelDef
|
|
388481
|
-
};
|
|
388482
|
-
}
|
|
388483
|
-
const k2 = cell.queryIndex ?? -1;
|
|
388484
|
-
const anonymous = anonymousQueries[k2];
|
|
388485
|
-
const compiled = modelDef.queryList[k2];
|
|
388486
|
-
return {
|
|
388487
|
-
type: "code",
|
|
388488
|
-
kind: "query",
|
|
388489
|
-
text: cell.text,
|
|
388490
|
-
markdown: cell.markdown,
|
|
388491
|
-
proseLines: cell.proseLines,
|
|
388492
|
-
codeLine: cell.codeLine,
|
|
388493
|
-
caption: cell.caption,
|
|
388494
|
-
queryIndex: cell.queryIndex,
|
|
388495
|
-
runnable: materializer.loadQuery(cell.text),
|
|
388496
|
-
modelMaterializer: materializer,
|
|
388497
|
-
modelDef,
|
|
388498
|
-
queryInfo: anonymous && {
|
|
388499
|
-
...anonymous,
|
|
388500
|
-
name: compiled?.as || compiled?.name || ""
|
|
388501
|
-
}
|
|
388502
|
-
};
|
|
388503
|
-
});
|
|
388770
|
+
const cells = read.cells.filter((cell) => cell.kind === "definition").map((cell) => ({
|
|
388771
|
+
type: "code",
|
|
388772
|
+
kind: "definition",
|
|
388773
|
+
text: cell.text,
|
|
388774
|
+
markdown: cell.markdown,
|
|
388775
|
+
proseLines: cell.proseLines,
|
|
388776
|
+
codeLine: cell.codeLine,
|
|
388777
|
+
caption: cell.caption,
|
|
388778
|
+
modelDef
|
|
388779
|
+
}));
|
|
388504
388780
|
if (layout) {
|
|
388505
388781
|
for (const tile of layout.tiles ?? []) {
|
|
388506
388782
|
if (tile.kind === "text") {
|
|
@@ -388567,7 +388843,7 @@ ${query}`;
|
|
|
388567
388843
|
throw readerError;
|
|
388568
388844
|
return this.getNotebookModel();
|
|
388569
388845
|
} else {
|
|
388570
|
-
throw new ModelNotFoundError(`${this.modelPath} is not a valid notebook name. Notebook files must end in .malloynb, or be a .malloy with a model-level ## artifact { layout=linear } tag.`);
|
|
388846
|
+
throw new ModelNotFoundError(`${this.modelPath} is not a valid notebook name. Notebook files must end in .malloynb, or be a .malloy report with a model-level ## artifact { layout=linear tiles=[…] } tag.`);
|
|
388571
388847
|
}
|
|
388572
388848
|
}
|
|
388573
388849
|
setServeDestinationConfig(provider) {
|
|
@@ -388615,6 +388891,7 @@ ${query}`;
|
|
|
388615
388891
|
...ordinary,
|
|
388616
388892
|
...groups.flatMap((g2) => g2.members)
|
|
388617
388893
|
]);
|
|
388894
|
+
await this.ensureSharedQueryModel(this.serveShapeCache.materializer);
|
|
388618
388895
|
const runnable = this.serveShapeCache.materializer.loadRestrictedQuery(queryString);
|
|
388619
388896
|
const probeSQL = await runnable.getSQL({ virtualMap, givens });
|
|
388620
388897
|
const rollupPaths = [
|
|
@@ -388900,6 +389177,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
|
|
|
388900
389177
|
}
|
|
388901
389178
|
}
|
|
388902
389179
|
}
|
|
389180
|
+
await this.ensureSharedQueryModel(this.modelMaterializer);
|
|
388903
389181
|
runnable = this.modelMaterializer.loadRestrictedQuery(queryString);
|
|
388904
389182
|
liveRunnable = runnable;
|
|
388905
389183
|
querySurfaceGivens = this.filterGivensToModelSurface(givens, await this.givenNamesReadByQuery(runnable, givens));
|
|
@@ -389251,6 +389529,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
|
|
|
389251
389529
|
return {
|
|
389252
389530
|
type: "source",
|
|
389253
389531
|
packageName: this.packageName,
|
|
389532
|
+
path: this.modelPath,
|
|
389254
389533
|
modelPath: this.modelPath,
|
|
389255
389534
|
malloyVersion: MALLOY_VERSION,
|
|
389256
389535
|
dataStyles: JSON.stringify(this.dataStyles),
|
|
@@ -389920,9 +390199,11 @@ init_constants();
|
|
|
389920
390199
|
init_logger();
|
|
389921
390200
|
async function compilePackageBuildPlan(pkg, signal) {
|
|
389922
390201
|
const parts = emptyBuildPlanParts();
|
|
390202
|
+
const yielder = createEventLoopYielder();
|
|
389923
390203
|
for (const modelPath of pkg.getModelPaths()) {
|
|
389924
390204
|
if (!modelPath.endsWith(MODEL_FILE_SUFFIX))
|
|
389925
390205
|
continue;
|
|
390206
|
+
await yielder.maybeYield();
|
|
389926
390207
|
if (signal?.aborted)
|
|
389927
390208
|
throw new Error("Build cancelled");
|
|
389928
390209
|
const { runtime, modelURL, importBaseURL } = await Model.getModelRuntime(pkg.getPackagePath(), modelPath, pkg.getMalloyConfig());
|
|
@@ -391066,8 +391347,11 @@ class Package {
|
|
|
391066
391347
|
const { packageName, packagePath, malloyConfig, buildManifest } = ctx;
|
|
391067
391348
|
const models = new Map;
|
|
391068
391349
|
const renderTagWarnings = [];
|
|
391350
|
+
const timings = { hydrateMs: 0, renderValidationMs: 0 };
|
|
391069
391351
|
const placeholder = (sm, err) => Model.fromCompilationError(packageName, sm.modelPath, sm.modelType, err, sm.modelPath === ctx.replacement?.modelPath ? ctx.replacement.source : undefined);
|
|
391352
|
+
const yielder = createEventLoopYielder();
|
|
391070
391353
|
for (const sm of outcome.models) {
|
|
391354
|
+
await yielder.maybeYield();
|
|
391071
391355
|
if (sm.compilationError) {
|
|
391072
391356
|
const err = Model.deserializeCompilationError(sm.compilationError);
|
|
391073
391357
|
if (onCompileError === "throw") {
|
|
@@ -391089,6 +391373,7 @@ class Package {
|
|
|
391089
391373
|
continue;
|
|
391090
391374
|
}
|
|
391091
391375
|
let model;
|
|
391376
|
+
const hydrateStart = performance.now();
|
|
391092
391377
|
try {
|
|
391093
391378
|
model = Model.fromSerialized(packageName, packagePath, malloyConfig, sm, {
|
|
391094
391379
|
buildManifest,
|
|
@@ -391109,8 +391394,11 @@ class Package {
|
|
|
391109
391394
|
});
|
|
391110
391395
|
models.set(sm.modelPath, placeholder(sm, hydrateErr instanceof Error ? hydrateErr : new Error(String(hydrateErr))));
|
|
391111
391396
|
continue;
|
|
391397
|
+
} finally {
|
|
391398
|
+
timings.hydrateMs += performance.now() - hydrateStart;
|
|
391112
391399
|
}
|
|
391113
391400
|
let warnings;
|
|
391401
|
+
const validationStart = performance.now();
|
|
391114
391402
|
try {
|
|
391115
391403
|
warnings = await model.validateRenderTags();
|
|
391116
391404
|
} catch (renderErr) {
|
|
@@ -391131,6 +391419,8 @@ class Package {
|
|
|
391131
391419
|
}
|
|
391132
391420
|
models.set(sm.modelPath, placeholder(sm, err));
|
|
391133
391421
|
continue;
|
|
391422
|
+
} finally {
|
|
391423
|
+
timings.renderValidationMs += performance.now() - validationStart;
|
|
391134
391424
|
}
|
|
391135
391425
|
for (const w2 of warnings) {
|
|
391136
391426
|
renderTagWarnings.push({
|
|
@@ -391140,10 +391430,12 @@ class Package {
|
|
|
391140
391430
|
severity: w2.severity
|
|
391141
391431
|
});
|
|
391142
391432
|
}
|
|
391433
|
+
const afterHydrateStart = performance.now();
|
|
391143
391434
|
await afterHydrate?.(sm);
|
|
391435
|
+
timings.hydrateMs += performance.now() - afterHydrateStart;
|
|
391144
391436
|
models.set(sm.modelPath, model);
|
|
391145
391437
|
}
|
|
391146
|
-
return { models, renderTagWarnings };
|
|
391438
|
+
return { models, renderTagWarnings, timings };
|
|
391147
391439
|
}
|
|
391148
391440
|
static async lintWorkerOutcome(environmentName, packageName, packagePath, malloyConfig, outcome, buildManifest, replacement) {
|
|
391149
391441
|
const { models, renderTagWarnings } = await Package.hydrateWorkerModels(outcome, {
|
|
@@ -391213,8 +391505,13 @@ class Package {
|
|
|
391213
391505
|
const packageConfig = Package.packageConfigFromOutcome(environmentName, packageName, outcome);
|
|
391214
391506
|
let models;
|
|
391215
391507
|
let renderTagWarnings;
|
|
391508
|
+
let hydrationTimings;
|
|
391216
391509
|
try {
|
|
391217
|
-
({
|
|
391510
|
+
({
|
|
391511
|
+
models,
|
|
391512
|
+
renderTagWarnings,
|
|
391513
|
+
timings: hydrationTimings
|
|
391514
|
+
} = await Package.hydrateWorkerModels(outcome, { packageName, packagePath, malloyConfig }, "throw", async (sm) => {
|
|
391218
391515
|
if (sm.modelPath.endsWith(MODEL_FILE_SUFFIX)) {
|
|
391219
391516
|
const modelSource = await fs14.readFile(path16.join(packagePath, sm.modelPath), "utf-8");
|
|
391220
391517
|
assertPersistNamesQuoted(modelSource, sm.modelPath);
|
|
@@ -391237,15 +391534,16 @@ class Package {
|
|
|
391237
391534
|
});
|
|
391238
391535
|
const pkg = new Package(environmentName, packageName, packagePath, packageConfig, databases, models, malloyConfig);
|
|
391239
391536
|
pkg.renderTagWarnings = renderTagWarnings;
|
|
391240
|
-
await pkg.discoverReports("load");
|
|
391537
|
+
const reportTimings = await pkg.discoverReports("load");
|
|
391241
391538
|
pkg.manifestWarnings = outcome.packageMetadata.manifestWarnings ?? [];
|
|
391242
391539
|
pkg.recordDeprecatedUsage(outcome);
|
|
391243
391540
|
pkg.retrievalSettings = outcome.packageMetadata.retrieval ?? DEFAULT_PACKAGE_RETRIEVAL;
|
|
391244
391541
|
pkg.rawAgents = outcome.packageMetadata.agents;
|
|
391245
391542
|
pkg.wireFreshnessResolvers();
|
|
391246
391543
|
pkg.refreshServingIdentity();
|
|
391544
|
+
let buildPlanMs;
|
|
391545
|
+
const buildPlanPhaseStart = performance.now();
|
|
391247
391546
|
try {
|
|
391248
|
-
const buildPlanStart = Date.now();
|
|
391249
391547
|
const workerPlan = outcome.buildPlan;
|
|
391250
391548
|
for (const [reason, count4] of Object.entries(workerPlan?.eligibilityRefused ?? {})) {
|
|
391251
391549
|
for (let i2 = 0;i2 < (count4 ?? 0); i2++) {
|
|
@@ -391278,7 +391576,8 @@ class Package {
|
|
|
391278
391576
|
sourceEntityId: source.sourceEntityId,
|
|
391279
391577
|
declaration: incrementalDeclarations[sourceID]
|
|
391280
391578
|
}));
|
|
391281
|
-
recordBuildPlanComputeDuration(workerPlan ? workerPlan.durationMs :
|
|
391579
|
+
recordBuildPlanComputeDuration(workerPlan ? workerPlan.durationMs : performance.now() - buildPlanPhaseStart, workerPlan ? "worker" : "main_thread");
|
|
391580
|
+
buildPlanMs = performance.now() - buildPlanPhaseStart;
|
|
391282
391581
|
} catch (err) {
|
|
391283
391582
|
recordBuildPlanComputeFailed();
|
|
391284
391583
|
logger.warn(`Failed to compute build plan for package ${packageName}`, {
|
|
@@ -391334,6 +391633,15 @@ class Package {
|
|
|
391334
391633
|
}
|
|
391335
391634
|
await pkg.pushPreaggregateServeModels();
|
|
391336
391635
|
pkg.logEmptyDiscoveryWarnings();
|
|
391636
|
+
recordPackageLoadPhaseDurations({
|
|
391637
|
+
worker: workerDoneTime - dispatchTime,
|
|
391638
|
+
hydrate: hydrationTimings.hydrateMs,
|
|
391639
|
+
render_validation: hydrationTimings.renderValidationMs,
|
|
391640
|
+
discovery: reportTimings.discoveryMs,
|
|
391641
|
+
lint: reportTimings.lintMs,
|
|
391642
|
+
build_plan: buildPlanMs,
|
|
391643
|
+
total: performance.now() - startTime
|
|
391644
|
+
});
|
|
391337
391645
|
return pkg;
|
|
391338
391646
|
}
|
|
391339
391647
|
getPackageName() {
|
|
@@ -391987,8 +392295,11 @@ class Package {
|
|
|
391987
392295
|
const started = performance.now();
|
|
391988
392296
|
const discovery = await this.discoverReportsIn(this.models, during);
|
|
391989
392297
|
this.installReports(discovery);
|
|
392298
|
+
const linting = performance.now();
|
|
391990
392299
|
await this.relintReports({ textLint: during !== "compile" });
|
|
391991
|
-
|
|
392300
|
+
const finished = performance.now();
|
|
392301
|
+
this.reportDiscovered(during, discovery, finished - started);
|
|
392302
|
+
return { discoveryMs: linting - started, lintMs: finished - linting };
|
|
391992
392303
|
}
|
|
391993
392304
|
reportDiscovered(during, discovery, durationMs) {
|
|
391994
392305
|
recordReportDiscoveryDuration(during, durationMs);
|
|
@@ -392089,19 +392400,17 @@ class Package {
|
|
|
392089
392400
|
model: modelPath,
|
|
392090
392401
|
error: errMessage(err)
|
|
392091
392402
|
});
|
|
392092
|
-
|
|
392093
|
-
|
|
392094
|
-
continue;
|
|
392095
|
-
}
|
|
392403
|
+
files.set(modelPath, { status: "dropped", name });
|
|
392404
|
+
continue;
|
|
392096
392405
|
}
|
|
392097
|
-
if (
|
|
392406
|
+
if (facts && !manifest) {
|
|
392098
392407
|
files.set(modelPath, { status: "tileless", name, facts });
|
|
392099
392408
|
continue;
|
|
392100
392409
|
}
|
|
392101
|
-
if (
|
|
392410
|
+
if (!facts) {
|
|
392102
392411
|
manifest = {
|
|
392103
392412
|
name,
|
|
392104
|
-
layout:
|
|
392413
|
+
layout: tag.layout,
|
|
392105
392414
|
title: name,
|
|
392106
392415
|
autorun: true,
|
|
392107
392416
|
entryFile: modelPath,
|
|
@@ -392397,7 +392706,7 @@ class Package {
|
|
|
392397
392706
|
layout: report.layout,
|
|
392398
392707
|
title: manifest?.title ?? listing.title ?? report.name,
|
|
392399
392708
|
description: manifest?.description ?? listing.description,
|
|
392400
|
-
editable: report.path.endsWith(MODEL_FILE_SUFFIX)
|
|
392709
|
+
editable: report.path.endsWith(MODEL_FILE_SUFFIX),
|
|
392401
392710
|
...error2 !== undefined && { error: error2 }
|
|
392402
392711
|
};
|
|
392403
392712
|
}
|
|
@@ -394166,7 +394475,7 @@ ${query}`);
|
|
|
394166
394475
|
}
|
|
394167
394476
|
}
|
|
394168
394477
|
// package.json
|
|
394169
|
-
var version2 = "0.9.
|
|
394478
|
+
var version2 = "0.9.12";
|
|
394170
394479
|
|
|
394171
394480
|
// src/version.ts
|
|
394172
394481
|
var SERVER_VERSION = version2;
|
|
@@ -400737,7 +401046,7 @@ function registerSearchDatabaseSchemaTool(mcpServer, environmentStore) {
|
|
|
400737
401046
|
if (n === 0)
|
|
400738
401047
|
return [];
|
|
400739
401048
|
return [
|
|
400740
|
-
`${n} table(s) have a path this server will not vouch for across every dialect it serves, so malloySource is omitted for them rather than risk a line that does not compile. Build it from tablePath: on your own dialect the path may work as-is, or may need the offending segment quoted. Double quotes on DuckDB, Postgres, Snowflake and
|
|
401049
|
+
`${n} table(s) have a path this server will not vouch for across every dialect it serves, so malloySource is omitted for them rather than risk a line that does not compile. Build it from tablePath: on your own dialect the path may work as-is, or may need the offending segment quoted. Double quotes on DuckDB, Postgres, Snowflake, Trino and SQL Server; backticks on MySQL, BigQuery and Databricks.`
|
|
400741
401050
|
];
|
|
400742
401051
|
};
|
|
400743
401052
|
const assertCanAdmit = async (environmentName) => {
|
|
@@ -405983,6 +406292,15 @@ function parseFrameAncestors(raw) {
|
|
|
405983
406292
|
return raw.trim();
|
|
405984
406293
|
}
|
|
405985
406294
|
|
|
406295
|
+
// src/service/experimental_dialects.ts
|
|
406296
|
+
import { getDialect } from "@malloydata/malloy";
|
|
406297
|
+
var BUNDLED_EXPERIMENTAL_DIALECTS = ["sqlserver"];
|
|
406298
|
+
function acceptBundledExperimentalDialects() {
|
|
406299
|
+
for (const name of BUNDLED_EXPERIMENTAL_DIALECTS) {
|
|
406300
|
+
getDialect(name).acceptExperimental();
|
|
406301
|
+
}
|
|
406302
|
+
}
|
|
406303
|
+
|
|
405986
406304
|
// src/server.ts
|
|
405987
406305
|
assertSupportedNodeVersion();
|
|
405988
406306
|
function parseArgs() {
|
|
@@ -406058,6 +406376,7 @@ parseArgs();
|
|
|
406058
406376
|
getPersistStorageMode();
|
|
406059
406377
|
getPersistCollisionEnforce();
|
|
406060
406378
|
getQueryMetadataMode();
|
|
406379
|
+
acceptBundledExperimentalDialects();
|
|
406061
406380
|
var PUBLISHER_PORT = Number(process.env.PUBLISHER_PORT || 4000);
|
|
406062
406381
|
var PUBLISHER_HOST = process.env.PUBLISHER_HOST || "0.0.0.0";
|
|
406063
406382
|
var MCP_PORT = Number(process.env.MCP_PORT || 4040);
|