@malloy-publisher/server 0.0.243 → 0.0.244
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 +122 -1
- package/dist/app/assets/{EnvironmentPage-zu0NeDu-.js → EnvironmentPage-BEvyNJlI.js} +1 -1
- package/dist/app/assets/{HomePage-kuBM3jiQ.js → HomePage-BOJ5_n2P.js} +1 -1
- package/dist/app/assets/{LightMode-DGBAdVPb.js → LightMode-DiRk-oG_.js} +1 -1
- package/dist/app/assets/{MainPage-CCVx9yPs.js → MainPage-CvWxTRF_.js} +1 -1
- package/dist/app/assets/{MaterializationsPage-M-bWIx8L.js → MaterializationsPage-DTkN3KGF.js} +1 -1
- package/dist/app/assets/{ModelPage-D-L2pqIy.js → ModelPage-C-Ds_tSI.js} +1 -1
- package/dist/app/assets/{PackagePage-BZglxDlj.js → PackagePage-C9kY78dt.js} +1 -1
- package/dist/app/assets/{RouteError-DGoCQGam.js → RouteError-dWMSDw8R.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-e8MLf7SV.js → ThemeEditorPage-EDVJy_ps.js} +1 -1
- package/dist/app/assets/{WorkbookPage-C8ufpN5i.js → WorkbookPage-6kCZgXzD.js} +1 -1
- package/dist/app/assets/{core-KFsKleo0.es-D6cYhbct.js → core-CbOoBenI.es-m46Eq2w-.js} +1 -1
- package/dist/app/assets/{index-B-jw9EkA.js → index-B8MmVXwE.js} +4 -4
- package/dist/app/assets/{index-CLwzlAx6.js → index-Bofgo-1r.js} +1 -1
- package/dist/app/assets/{index-Dnef41hg.js → index-CmKKDEOJ.js} +1 -1
- package/dist/app/assets/{index-CDNkCta4.js → index-CzHda4nF.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +380 -47
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -269386,7 +269386,10 @@ class QueryController {
|
|
|
269386
269386
|
serializedResult,
|
|
269387
269387
|
rowLimit,
|
|
269388
269388
|
rowLimitSource,
|
|
269389
|
-
queryCorrelationId
|
|
269389
|
+
queryCorrelationId,
|
|
269390
|
+
servedFrom,
|
|
269391
|
+
executionTimeMs,
|
|
269392
|
+
queryCostBytes
|
|
269390
269393
|
} = await runWithQueryTimeout((abortSignal) => model.getQueryResults(sourceName, queryName, query, filterParams, bypassFilters, givens, abortSignal, {
|
|
269391
269394
|
request: requestMetadata,
|
|
269392
269395
|
queryClass,
|
|
@@ -269413,7 +269416,10 @@ class QueryController {
|
|
|
269413
269416
|
renderLogs: renderLogs.length > 0 ? renderLogs : undefined,
|
|
269414
269417
|
queryRowLimit: rowLimit,
|
|
269415
269418
|
queryRowLimitSource: rowLimitSource,
|
|
269416
|
-
queryCorrelationId
|
|
269419
|
+
queryCorrelationId,
|
|
269420
|
+
servedFrom,
|
|
269421
|
+
executionTimeMs,
|
|
269422
|
+
queryCostBytes
|
|
269417
269423
|
};
|
|
269418
269424
|
}
|
|
269419
269425
|
}
|
|
@@ -277262,7 +277268,8 @@ var sourceBuildDuration = lazyHistogram("publisher_materialization_source_build_
|
|
|
277262
277268
|
var dropTablesCounter = lazyCounter2("publisher_materialization_drop_tables_total", "Physical tables dropped on delete. Label: outcome ('success'|'failure').");
|
|
277263
277269
|
var scheduledFireCounter = lazyCounter2("publisher_materialization_scheduled_fires_total", "Standalone-scheduler attempts to fire a package's materialization.schedule. " + "Label: outcome ('fired'|'conflict'|'error').");
|
|
277264
277270
|
var storageServeRoutingCounter = lazyCounter2("publisher_storage_serve_routing_total", "storage= serve routing decisions. Label: outcome ('storage'|'live_fallback'|'runtime_live_fallback').");
|
|
277265
|
-
var storageBuildFailureCounter = lazyCounter2("publisher_storage_build_failures_total", "storage= build failures (federation/passthrough/attach/CTAS), distinct from " + "in-warehouse build failures.
|
|
277271
|
+
var storageBuildFailureCounter = lazyCounter2("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.");
|
|
277272
|
+
var attributionSkippedCounter = lazyCounter2("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.");
|
|
277266
277273
|
var eligibilityRefusedCounter = lazyCounter2("publisher_materialization_eligibility_refused_total", "storage= materialization-eligibility refusals. Label: reason " + "('free_parameter'|'given'|'authorize'|'not_duckdb_portable'|" + "'public_surface_unknown').");
|
|
277267
277274
|
var serveShapeTierDropCounter = lazyCounter2("publisher_storage_serve_shape_tier_drop_total", "storage serve-shape compile escalations: a refinement tier failed to " + "compile and the riskiest category was dropped. Label: tier (the failed " + "tier index, 0=full).");
|
|
277268
277275
|
var serveShapeTypeFallbackCounter = lazyCounter2("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').");
|
|
@@ -277303,8 +277310,11 @@ function recordSourceBuildDuration(durationMs, engine) {
|
|
|
277303
277310
|
function recordDropTables(outcome, engine) {
|
|
277304
277311
|
dropTablesCounter().add(1, { outcome, engine });
|
|
277305
277312
|
}
|
|
277306
|
-
function recordStorageBuildFailure(destination) {
|
|
277307
|
-
storageBuildFailureCounter().add(1, { destination });
|
|
277313
|
+
function recordStorageBuildFailure(destination, reason = "build_failed") {
|
|
277314
|
+
storageBuildFailureCounter().add(1, { destination, reason });
|
|
277315
|
+
}
|
|
277316
|
+
function recordAttributionSkipped(reason) {
|
|
277317
|
+
attributionSkippedCounter().add(1, { reason });
|
|
277308
277318
|
}
|
|
277309
277319
|
function recordEligibilityRefused(reason) {
|
|
277310
277320
|
eligibilityRefusedCounter().add(1, { reason });
|
|
@@ -278685,6 +278695,10 @@ class Model {
|
|
|
278685
278695
|
description: "How long it takes to execute a Malloy model query",
|
|
278686
278696
|
unit: "ms"
|
|
278687
278697
|
});
|
|
278698
|
+
queryScannedBytesCounter = this.meter.createCounter("malloy_model_query_scanned_bytes", {
|
|
278699
|
+
description: "Warehouse bytes scanned by Malloy model queries, where the backend reports them. NOT bytes billed: BigQuery bills a 10MB minimum per query, so spend exceeds this on small reads.",
|
|
278700
|
+
unit: "By"
|
|
278701
|
+
});
|
|
278688
278702
|
constructor(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos, runnableNotebookCells, compilationError, filterMap, givens, modelInfo) {
|
|
278689
278703
|
this.packageName = packageName;
|
|
278690
278704
|
this.modelPath = modelPath;
|
|
@@ -279460,13 +279474,12 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
279460
279474
|
} else {
|
|
279461
279475
|
const endTime = performance.now();
|
|
279462
279476
|
const executionTime2 = endTime - startTime;
|
|
279463
|
-
this.queryExecutionHistogram.record(executionTime2, {
|
|
279464
|
-
|
|
279465
|
-
|
|
279466
|
-
|
|
279467
|
-
"
|
|
279468
|
-
|
|
279469
|
-
});
|
|
279477
|
+
this.queryExecutionHistogram.record(executionTime2, this.queryMetricAttributes({
|
|
279478
|
+
environment: queryMetadataInput?.environment,
|
|
279479
|
+
queryName,
|
|
279480
|
+
sourceName,
|
|
279481
|
+
status: "error"
|
|
279482
|
+
}));
|
|
279470
279483
|
throw new BadRequestError("Invalid query request. (Query AND !sourceName) OR (queryName AND sourceName) must be defined.");
|
|
279471
279484
|
}
|
|
279472
279485
|
const isAdHocQuery = !sourceName && !queryName && !!query;
|
|
@@ -279553,7 +279566,6 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
279553
279566
|
maxRows
|
|
279554
279567
|
});
|
|
279555
279568
|
appliedQueryMetadata = this.resolveQueryMetadata(queryMetadataInput, preparedResult.connectionName);
|
|
279556
|
-
executionTime = performance.now() - startTime;
|
|
279557
279569
|
queryResults = await runnable.run({
|
|
279558
279570
|
rowLimit,
|
|
279559
279571
|
givens: effectiveGivens,
|
|
@@ -279562,19 +279574,19 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
279562
279574
|
virtualMap: serveVirtualMap,
|
|
279563
279575
|
queryMetadata: appliedQueryMetadata
|
|
279564
279576
|
});
|
|
279577
|
+
executionTime = performance.now() - startTime;
|
|
279565
279578
|
} catch (error) {
|
|
279566
279579
|
const canDegradeToLive = !!serveVirtualMap && !!liveRunnable && !abortSignal?.aborted && !String(error?.code ?? "").startsWith("runtime-given-") && serveShapeBindings.length > 0 && serveShapeBindings.every((b) => b.freshnessFallback === "live");
|
|
279567
279580
|
const failQuery = (err) => {
|
|
279568
279581
|
const errorEndTime = performance.now();
|
|
279569
279582
|
const errorExecutionTime = errorEndTime - startTime;
|
|
279570
|
-
this.queryExecutionHistogram.record(errorExecutionTime, {
|
|
279571
|
-
|
|
279572
|
-
|
|
279573
|
-
|
|
279574
|
-
"
|
|
279575
|
-
|
|
279576
|
-
|
|
279577
|
-
});
|
|
279583
|
+
this.queryExecutionHistogram.record(errorExecutionTime, this.queryMetricAttributes({
|
|
279584
|
+
environment: queryMetadataInput?.environment,
|
|
279585
|
+
queryName,
|
|
279586
|
+
sourceName,
|
|
279587
|
+
status: "error",
|
|
279588
|
+
servedFrom
|
|
279589
|
+
}));
|
|
279578
279590
|
const givenCode = err?.code;
|
|
279579
279591
|
if (typeof givenCode === "string" && givenCode.startsWith("runtime-given-")) {
|
|
279580
279592
|
logger.debug("Rejected client-supplied given", {
|
|
@@ -279635,17 +279647,20 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
279635
279647
|
const wrappedResult = API.util.wrapResult(queryResults);
|
|
279636
279648
|
const serializedResult = stringifyQueryResponse(responseShape === "compact" ? queryResults.data.value : wrappedResult, queryResults.totalRows, maxBytes, "model_query", responseShape === "compact" ? bigIntReplacer : undefined);
|
|
279637
279649
|
assertWithinModelByteLimit(serializedResult, maxBytes, "model_query");
|
|
279638
|
-
this.
|
|
279639
|
-
|
|
279640
|
-
|
|
279641
|
-
|
|
279642
|
-
"
|
|
279643
|
-
|
|
279644
|
-
|
|
279645
|
-
|
|
279646
|
-
"malloy.model.query.status": "success",
|
|
279647
|
-
...servedFrom ? { "malloy.model.query.served_from": servedFrom } : {}
|
|
279650
|
+
const metricAttributes = this.queryMetricAttributes({
|
|
279651
|
+
environment: queryMetadataInput?.environment,
|
|
279652
|
+
queryName,
|
|
279653
|
+
sourceName,
|
|
279654
|
+
status: "success",
|
|
279655
|
+
connection: queryResults.connectionName,
|
|
279656
|
+
servedFrom,
|
|
279657
|
+
rowsLimit: rowLimit
|
|
279648
279658
|
});
|
|
279659
|
+
this.queryExecutionHistogram.record(executionTime, metricAttributes);
|
|
279660
|
+
const queryCostBytes = queryResults.runStats?.queryCostBytes;
|
|
279661
|
+
if (queryCostBytes !== undefined) {
|
|
279662
|
+
this.queryScannedBytesCounter.add(queryCostBytes, metricAttributes);
|
|
279663
|
+
}
|
|
279649
279664
|
return {
|
|
279650
279665
|
result: wrappedResult,
|
|
279651
279666
|
serializedResult,
|
|
@@ -279654,7 +279669,23 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
279654
279669
|
dataStyles: this.dataStyles,
|
|
279655
279670
|
rowLimit,
|
|
279656
279671
|
rowLimitSource,
|
|
279657
|
-
queryCorrelationId: appliedQueryMetadata?.query_id ?? null
|
|
279672
|
+
queryCorrelationId: appliedQueryMetadata?.query_id ?? null,
|
|
279673
|
+
servedFrom: servedFrom ?? null,
|
|
279674
|
+
executionTimeMs: Math.round(executionTime),
|
|
279675
|
+
queryCostBytes: queryCostBytes ?? null
|
|
279676
|
+
};
|
|
279677
|
+
}
|
|
279678
|
+
queryMetricAttributes(args) {
|
|
279679
|
+
return {
|
|
279680
|
+
"malloy.model.path": this.modelPath,
|
|
279681
|
+
"malloy.package": this.packageName,
|
|
279682
|
+
"malloy.model.query.name": args.queryName,
|
|
279683
|
+
"malloy.model.query.source": args.sourceName,
|
|
279684
|
+
"malloy.model.query.status": args.status,
|
|
279685
|
+
...args.environment ? { "malloy.environment": args.environment } : {},
|
|
279686
|
+
...args.connection ? { "malloy.model.query.connection": args.connection } : {},
|
|
279687
|
+
...args.rowsLimit === undefined ? {} : { "malloy.model.query.rows_limit": args.rowsLimit },
|
|
279688
|
+
...args.servedFrom ? { "malloy.model.query.served_from": args.servedFrom } : {}
|
|
279658
279689
|
};
|
|
279659
279690
|
}
|
|
279660
279691
|
resolveQueryMetadata(input, connectionName) {
|
|
@@ -293040,6 +293071,130 @@ import {
|
|
|
293040
293071
|
import { mkdirSync as mkdirSync2, mkdtempSync, rmSync } from "node:fs";
|
|
293041
293072
|
import os4 from "node:os";
|
|
293042
293073
|
import path13 from "node:path";
|
|
293074
|
+
|
|
293075
|
+
// src/service/build_query_tag.ts
|
|
293076
|
+
var MAX_QUERY_TAG_LENGTH = 2000;
|
|
293077
|
+
var BQ_MAX_LEN = 63;
|
|
293078
|
+
function snowflakeQueryTagValue(metadata) {
|
|
293079
|
+
if (metadata === undefined || Object.keys(metadata).length === 0) {
|
|
293080
|
+
return;
|
|
293081
|
+
}
|
|
293082
|
+
const tag = JSON.stringify(metadata);
|
|
293083
|
+
return tag.length > MAX_QUERY_TAG_LENGTH ? undefined : tag;
|
|
293084
|
+
}
|
|
293085
|
+
function snowflakeSetQueryTagSQL(metadata, secretName) {
|
|
293086
|
+
const tag = snowflakeQueryTagValue(metadata);
|
|
293087
|
+
if (tag === undefined)
|
|
293088
|
+
return;
|
|
293089
|
+
const inner = `ALTER SESSION SET QUERY_TAG = '${sqlLiteral(tag, "snowflake")}'`;
|
|
293090
|
+
return `SELECT * FROM snowflake_query('${inner.replace(/'/g, "''")}', '${secretName.replace(/'/g, "''")}')`;
|
|
293091
|
+
}
|
|
293092
|
+
function sanitizeBigQueryValue(value) {
|
|
293093
|
+
return value.toLowerCase().replace(/[^a-z0-9_-]/g, "_").slice(0, BQ_MAX_LEN);
|
|
293094
|
+
}
|
|
293095
|
+
function sanitizeBigQueryKey(key) {
|
|
293096
|
+
const sanitized = sanitizeBigQueryValue(key);
|
|
293097
|
+
return /^[a-z]/.test(sanitized) ? sanitized : undefined;
|
|
293098
|
+
}
|
|
293099
|
+
function bigQueryQueryLabelValue(metadata) {
|
|
293100
|
+
if (metadata === undefined)
|
|
293101
|
+
return;
|
|
293102
|
+
const rendered = new Map;
|
|
293103
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
293104
|
+
const sanitizedKey = sanitizeBigQueryKey(key);
|
|
293105
|
+
if (sanitizedKey === undefined)
|
|
293106
|
+
continue;
|
|
293107
|
+
rendered.set(sanitizedKey, sanitizeBigQueryValue(value));
|
|
293108
|
+
}
|
|
293109
|
+
if (rendered.size === 0)
|
|
293110
|
+
return;
|
|
293111
|
+
return [...rendered].map(([key, value]) => `${key}:${value}`).join(",");
|
|
293112
|
+
}
|
|
293113
|
+
|
|
293114
|
+
// src/service/build_read_cost.ts
|
|
293115
|
+
var SNOWFLAKE_HISTORY_LIMIT = 1e4;
|
|
293116
|
+
var BIGQUERY_COST_COLUMNS = `
|
|
293117
|
+
total_slot_time_ms,
|
|
293118
|
+
json_extract_string(statistics, '$.query.totalBytesProcessed') AS bytes_processed,
|
|
293119
|
+
json_extract_string(statistics, '$.query.totalBytesBilled') AS bytes_billed,
|
|
293120
|
+
json_extract_string(statistics, '$.query.cacheHit') AS cache_hit`;
|
|
293121
|
+
function bigQueryReadCost(row, jobId, parentJobId) {
|
|
293122
|
+
return {
|
|
293123
|
+
engine: "bigquery",
|
|
293124
|
+
jobId,
|
|
293125
|
+
parentJobId,
|
|
293126
|
+
bytesScanned: num(row.bytes_processed),
|
|
293127
|
+
bytesBilled: num(row.bytes_billed),
|
|
293128
|
+
slotTimeMs: num(row.total_slot_time_ms),
|
|
293129
|
+
executionTimeMs: null,
|
|
293130
|
+
cacheHit: bool(row.cache_hit)
|
|
293131
|
+
};
|
|
293132
|
+
}
|
|
293133
|
+
function snowflakeDatabaseQualifier(database) {
|
|
293134
|
+
return typeof database === "string" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(database) ? database : "SNOWFLAKE";
|
|
293135
|
+
}
|
|
293136
|
+
function snowflakeCostSQL(queryTag, database) {
|
|
293137
|
+
const qualifier = `${snowflakeDatabaseQualifier(database)}.INFORMATION_SCHEMA`;
|
|
293138
|
+
return `
|
|
293139
|
+
SELECT QUERY_ID AS "job_id",
|
|
293140
|
+
QUERY_TEXT AS "query_text",
|
|
293141
|
+
BYTES_SCANNED AS "bytes_scanned",
|
|
293142
|
+
EXECUTION_TIME AS "execution_time_ms",
|
|
293143
|
+
ROWS_PRODUCED AS "rows_produced"
|
|
293144
|
+
FROM TABLE(${qualifier}.QUERY_HISTORY_BY_SESSION(
|
|
293145
|
+
RESULT_LIMIT => ${SNOWFLAKE_HISTORY_LIMIT}))
|
|
293146
|
+
WHERE QUERY_TAG = '${sqlLiteral(queryTag, "snowflake")}'
|
|
293147
|
+
AND EXECUTION_STATUS = 'SUCCESS'`;
|
|
293148
|
+
}
|
|
293149
|
+
function pickSnowflakeReadRow(rows, buildSQL) {
|
|
293150
|
+
const matches = rows.filter((row) => str(col(row, "query_text"))?.trim() === buildSQL.trim());
|
|
293151
|
+
return matches.length === 1 ? matches[0] : null;
|
|
293152
|
+
}
|
|
293153
|
+
function snowflakeReadCost(row) {
|
|
293154
|
+
const scanned = num(col(row, "bytes_scanned"));
|
|
293155
|
+
const produced = num(col(row, "rows_produced"));
|
|
293156
|
+
return {
|
|
293157
|
+
engine: "snowflake",
|
|
293158
|
+
jobId: str(col(row, "job_id")),
|
|
293159
|
+
parentJobId: null,
|
|
293160
|
+
bytesScanned: scanned,
|
|
293161
|
+
bytesBilled: null,
|
|
293162
|
+
slotTimeMs: null,
|
|
293163
|
+
executionTimeMs: num(col(row, "execution_time_ms")),
|
|
293164
|
+
cacheHit: scanned === null || produced === null ? null : scanned === 0 && produced > 0
|
|
293165
|
+
};
|
|
293166
|
+
}
|
|
293167
|
+
function col(row, key) {
|
|
293168
|
+
if (key in row)
|
|
293169
|
+
return row[key];
|
|
293170
|
+
const upper = key.toUpperCase();
|
|
293171
|
+
if (upper in row)
|
|
293172
|
+
return row[upper];
|
|
293173
|
+
const hit = Object.keys(row).find((k) => k.toLowerCase() === key);
|
|
293174
|
+
return hit === undefined ? undefined : row[hit];
|
|
293175
|
+
}
|
|
293176
|
+
function num(value) {
|
|
293177
|
+
if (value === null || value === undefined)
|
|
293178
|
+
return null;
|
|
293179
|
+
const n = Number(value);
|
|
293180
|
+
return Number.isFinite(n) ? n : null;
|
|
293181
|
+
}
|
|
293182
|
+
function str(value) {
|
|
293183
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
293184
|
+
}
|
|
293185
|
+
function bool(value) {
|
|
293186
|
+
if (typeof value === "boolean")
|
|
293187
|
+
return value;
|
|
293188
|
+
if (typeof value === "string") {
|
|
293189
|
+
if (value === "true")
|
|
293190
|
+
return true;
|
|
293191
|
+
if (value === "false")
|
|
293192
|
+
return false;
|
|
293193
|
+
}
|
|
293194
|
+
return null;
|
|
293195
|
+
}
|
|
293196
|
+
|
|
293197
|
+
// src/service/materialization_build_session.ts
|
|
293043
293198
|
var sharedGateSession;
|
|
293044
293199
|
var PASSTHROUGH_SOURCE_TYPES = [
|
|
293045
293200
|
"bigquery",
|
|
@@ -293063,6 +293218,157 @@ function wrapPassthrough(sourceType, handle, innerSQL) {
|
|
|
293063
293218
|
}
|
|
293064
293219
|
}
|
|
293065
293220
|
}
|
|
293221
|
+
async function tagSnowflakeSession(session, sourceType, handle, queryMetadata) {
|
|
293222
|
+
if (sourceType !== "snowflake")
|
|
293223
|
+
return;
|
|
293224
|
+
const sql = snowflakeSetQueryTagSQL(queryMetadata, handle);
|
|
293225
|
+
if (sql === undefined)
|
|
293226
|
+
return;
|
|
293227
|
+
try {
|
|
293228
|
+
await session.runSQL(sql);
|
|
293229
|
+
} catch (error) {
|
|
293230
|
+
logger.warn("Could not tag the Snowflake build session", {
|
|
293231
|
+
error: error instanceof Error ? error.message : String(error)
|
|
293232
|
+
});
|
|
293233
|
+
recordAttributionSkipped("tag_failed");
|
|
293234
|
+
}
|
|
293235
|
+
}
|
|
293236
|
+
async function canSplitBigQueryRead(session, handle) {
|
|
293237
|
+
try {
|
|
293238
|
+
await session.runSQL(`SELECT job_id FROM bigquery_jobs('${escapeSQL(handle)}', maxResults := 1)`);
|
|
293239
|
+
return true;
|
|
293240
|
+
} catch (error) {
|
|
293241
|
+
logger.warn("Cannot list BigQuery jobs for this connection, so this build's " + "warehouse read will not be labelled or costed", {
|
|
293242
|
+
error: error instanceof Error ? error.message : String(error)
|
|
293243
|
+
});
|
|
293244
|
+
recordAttributionSkipped("job_listing_unavailable");
|
|
293245
|
+
return false;
|
|
293246
|
+
}
|
|
293247
|
+
}
|
|
293248
|
+
|
|
293249
|
+
class BilledReadNotCapturedError extends Error {
|
|
293250
|
+
constructor(message, options) {
|
|
293251
|
+
super(message, options);
|
|
293252
|
+
this.name = "BilledReadNotCapturedError";
|
|
293253
|
+
}
|
|
293254
|
+
}
|
|
293255
|
+
var BIGQUERY_LOOKUP_ATTEMPTS = 3;
|
|
293256
|
+
var BIGQUERY_LOOKUP_BACKOFF_MS = 250;
|
|
293257
|
+
async function withBigQueryLookupRetry(work, parentJobId) {
|
|
293258
|
+
let lastError;
|
|
293259
|
+
for (let attempt = 1;attempt <= BIGQUERY_LOOKUP_ATTEMPTS; attempt++) {
|
|
293260
|
+
try {
|
|
293261
|
+
return await work();
|
|
293262
|
+
} catch (error) {
|
|
293263
|
+
lastError = error;
|
|
293264
|
+
if (attempt < BIGQUERY_LOOKUP_ATTEMPTS) {
|
|
293265
|
+
logger.warn("Could not read the BigQuery job record for a build's read; retrying", {
|
|
293266
|
+
attempt,
|
|
293267
|
+
of: BIGQUERY_LOOKUP_ATTEMPTS,
|
|
293268
|
+
parentJobId,
|
|
293269
|
+
error: error instanceof Error ? error.message : String(error)
|
|
293270
|
+
});
|
|
293271
|
+
await new Promise((resolve6) => setTimeout(resolve6, BIGQUERY_LOOKUP_BACKOFF_MS * attempt));
|
|
293272
|
+
}
|
|
293273
|
+
}
|
|
293274
|
+
}
|
|
293275
|
+
throw new BilledReadNotCapturedError(`Could not read the BigQuery job record for a build's read (parent job ` + `${parentJobId}) after ${BIGQUERY_LOOKUP_ATTEMPTS} attempts, so its ` + `rows cannot be captured: ` + `${lastError instanceof Error ? lastError.message : String(lastError)}`, { cause: lastError });
|
|
293276
|
+
}
|
|
293277
|
+
async function issuePassthroughRead(session, sourceType, handle, buildSQL, queryMetadata) {
|
|
293278
|
+
const label = sourceType === "bigquery" ? bigQueryQueryLabelValue(queryMetadata) : undefined;
|
|
293279
|
+
if (label === undefined || !await canSplitBigQueryRead(session, handle)) {
|
|
293280
|
+
return {
|
|
293281
|
+
selectSQL: wrapPassthrough(sourceType, handle, buildSQL),
|
|
293282
|
+
jobId: null,
|
|
293283
|
+
cost: null
|
|
293284
|
+
};
|
|
293285
|
+
}
|
|
293286
|
+
const script = `SET @@query_label = "${label}";
|
|
293287
|
+
${buildSQL};`;
|
|
293288
|
+
const executed = await session.runSQL(`SELECT * FROM bigquery_execute('${escapeSQL(handle)}', '${escapeSQL(script)}')`);
|
|
293289
|
+
const parentJobId = firstColumn(executed, "job_id");
|
|
293290
|
+
if (parentJobId === null) {
|
|
293291
|
+
throw new BilledReadNotCapturedError("bigquery_execute returned no job_id for the build's read, so its " + "result table cannot be located");
|
|
293292
|
+
}
|
|
293293
|
+
const located = await withBigQueryLookupRetry(async () => {
|
|
293294
|
+
const child = await session.runSQL(`
|
|
293295
|
+
SELECT job_id,
|
|
293296
|
+
json_extract_string(configuration, '$.query.destinationTable.projectId') AS project,
|
|
293297
|
+
json_extract_string(configuration, '$.query.destinationTable.datasetId') AS dataset,
|
|
293298
|
+
json_extract_string(configuration, '$.query.destinationTable.tableId') AS table,
|
|
293299
|
+
${BIGQUERY_COST_COLUMNS}
|
|
293300
|
+
FROM (SELECT * FROM bigquery_jobs('${escapeSQL(handle)}',
|
|
293301
|
+
parentJobId := '${escapeSQL(parentJobId)}')
|
|
293302
|
+
WHERE job_type = 'QUERY')
|
|
293303
|
+
ORDER BY creation_time DESC`);
|
|
293304
|
+
const row = resultRows(child).find((r) => str2(r.dataset) && str2(r.table));
|
|
293305
|
+
if (row === undefined) {
|
|
293306
|
+
throw new Error(`no child job with a result table under parent ${parentJobId}`);
|
|
293307
|
+
}
|
|
293308
|
+
return row;
|
|
293309
|
+
}, parentJobId);
|
|
293310
|
+
const path14 = [
|
|
293311
|
+
str2(located.project) ?? handle,
|
|
293312
|
+
str2(located.dataset),
|
|
293313
|
+
str2(located.table)
|
|
293314
|
+
].join(".");
|
|
293315
|
+
const jobId = str2(located.job_id);
|
|
293316
|
+
return {
|
|
293317
|
+
selectSQL: `SELECT * FROM bigquery_scan('${escapeSQL(path14)}')`,
|
|
293318
|
+
jobId,
|
|
293319
|
+
cost: bigQueryReadCost(located, jobId, parentJobId)
|
|
293320
|
+
};
|
|
293321
|
+
}
|
|
293322
|
+
async function snowflakeReadCostAfterBuild(session, sourceType, handle, buildSQL, queryMetadata, database) {
|
|
293323
|
+
if (sourceType !== "snowflake")
|
|
293324
|
+
return null;
|
|
293325
|
+
const tag = snowflakeQueryTagValue(queryMetadata);
|
|
293326
|
+
if (tag === undefined)
|
|
293327
|
+
return null;
|
|
293328
|
+
try {
|
|
293329
|
+
const costResult = await session.runSQL(passthroughSnowflake(snowflakeCostSQL(tag, database), handle));
|
|
293330
|
+
const candidates = resultRows(costResult);
|
|
293331
|
+
const row = pickSnowflakeReadRow(candidates, buildSQL);
|
|
293332
|
+
if (row === null) {
|
|
293333
|
+
recordAttributionSkipped(candidates.length === 0 ? "read_row_not_found" : "read_row_ambiguous");
|
|
293334
|
+
return null;
|
|
293335
|
+
}
|
|
293336
|
+
return snowflakeReadCost(row);
|
|
293337
|
+
} catch (error) {
|
|
293338
|
+
logger.warn("Could not read back what the Snowflake build read cost", {
|
|
293339
|
+
error: error instanceof Error ? error.message : String(error)
|
|
293340
|
+
});
|
|
293341
|
+
recordAttributionSkipped("cost_query_failed");
|
|
293342
|
+
return null;
|
|
293343
|
+
}
|
|
293344
|
+
}
|
|
293345
|
+
function passthroughSnowflake(innerSQL, handle) {
|
|
293346
|
+
return `SELECT * FROM snowflake_query('${escapeSQL(innerSQL)}', '${escapeSQL(handle)}')`;
|
|
293347
|
+
}
|
|
293348
|
+
function resultRows(result) {
|
|
293349
|
+
if (Array.isArray(result))
|
|
293350
|
+
return result;
|
|
293351
|
+
const rows = result?.rows;
|
|
293352
|
+
return Array.isArray(rows) ? rows : [];
|
|
293353
|
+
}
|
|
293354
|
+
function firstColumn(result, column) {
|
|
293355
|
+
const rows = resultRows(result);
|
|
293356
|
+
return rows.length > 0 ? str2(rows[0][column]) : null;
|
|
293357
|
+
}
|
|
293358
|
+
function str2(value) {
|
|
293359
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
293360
|
+
}
|
|
293361
|
+
async function clearSnowflakeSessionTag(session, sourceType, handle) {
|
|
293362
|
+
if (sourceType !== "snowflake" || handle === undefined)
|
|
293363
|
+
return;
|
|
293364
|
+
try {
|
|
293365
|
+
await session.runSQL(passthroughSnowflake("ALTER SESSION UNSET QUERY_TAG", handle));
|
|
293366
|
+
} catch (error) {
|
|
293367
|
+
logger.warn("Could not clear the Snowflake build session's query tag", {
|
|
293368
|
+
error: error instanceof Error ? error.message : String(error)
|
|
293369
|
+
});
|
|
293370
|
+
}
|
|
293371
|
+
}
|
|
293066
293372
|
function passthroughSourceType(sourceConnection) {
|
|
293067
293373
|
const type = sourceConnection.type;
|
|
293068
293374
|
if (PASSTHROUGH_SOURCE_TYPES.includes(type ?? "")) {
|
|
@@ -293116,22 +293422,30 @@ async function buildSourceIntoStorage(params) {
|
|
|
293116
293422
|
sourceConnection,
|
|
293117
293423
|
buildSQL,
|
|
293118
293424
|
physicalTableName,
|
|
293119
|
-
environmentPath
|
|
293425
|
+
environmentPath,
|
|
293426
|
+
queryMetadata
|
|
293120
293427
|
} = params;
|
|
293121
293428
|
assertSupportedDestination(destinationName, destinationConnection);
|
|
293122
293429
|
const sourceType = passthroughSourceType(sourceConnection);
|
|
293123
293430
|
const { session, dispose } = createIsolatedBuildSession(`build_${destinationName}`);
|
|
293431
|
+
let federatedHandle;
|
|
293124
293432
|
try {
|
|
293125
293433
|
await attachDestinationReadWrite(session, destinationName, destinationConnection, environmentPath);
|
|
293126
293434
|
if (destinationConnection.type === "ducklake") {
|
|
293127
293435
|
await session.runSQL("SET ducklake_default_data_inlining_row_limit=0");
|
|
293128
293436
|
}
|
|
293129
293437
|
const federated = await federateSourceForPassthrough(session, sourceType, sourceFederationConfig(sourceConnection));
|
|
293438
|
+
await tagSnowflakeSession(session, sourceType, federated.handle, queryMetadata);
|
|
293130
293439
|
const target = quoteManifestTablePath(`${destinationName}.${physicalTableName}`, "duckdb");
|
|
293131
|
-
const
|
|
293132
|
-
const schema = await createTableAndDescribe(session, target,
|
|
293133
|
-
return {
|
|
293440
|
+
const read = await issuePassthroughRead(session, sourceType, federated.handle, buildSQL, queryMetadata);
|
|
293441
|
+
const schema = await createTableAndDescribe(session, target, read.selectSQL);
|
|
293442
|
+
return {
|
|
293443
|
+
storageDestinationName: destinationName,
|
|
293444
|
+
schema,
|
|
293445
|
+
readCost: read.cost ?? await snowflakeReadCostAfterBuild(session, sourceType, federated.handle, buildSQL, queryMetadata, sourceConnection.snowflakeConnection?.database)
|
|
293446
|
+
};
|
|
293134
293447
|
} finally {
|
|
293448
|
+
await clearSnowflakeSessionTag(session, sourceType, federatedHandle);
|
|
293135
293449
|
await dispose();
|
|
293136
293450
|
}
|
|
293137
293451
|
}
|
|
@@ -293182,7 +293496,11 @@ async function buildDownstreamIntoStorage(params) {
|
|
|
293182
293496
|
const sql = projectToPublicColumns(downstream, downstream.getSQL({ virtualMap }));
|
|
293183
293497
|
const target = quoteManifestTablePath(`${destinationName}.${physicalTableName}`, "duckdb");
|
|
293184
293498
|
const schema = await createTableAndDescribe(session, target, sql);
|
|
293185
|
-
return {
|
|
293499
|
+
return {
|
|
293500
|
+
storageDestinationName: destinationName,
|
|
293501
|
+
schema,
|
|
293502
|
+
readCost: null
|
|
293503
|
+
};
|
|
293186
293504
|
} finally {
|
|
293187
293505
|
await dispose();
|
|
293188
293506
|
}
|
|
@@ -293981,15 +294299,15 @@ class MaterializationService {
|
|
|
293981
294299
|
buildManifest,
|
|
293982
294300
|
connectionDigests
|
|
293983
294301
|
});
|
|
294302
|
+
const runOptions = this.buildRunSQLOptions(persistSource, environment, buildMetadata);
|
|
293984
294303
|
if (isStorageBuild) {
|
|
293985
294304
|
const dependsOnStorageUpstream = persistSource.getSQL({
|
|
293986
294305
|
buildManifest: manifest.buildManifest,
|
|
293987
294306
|
connectionDigests
|
|
293988
294307
|
}) !== buildSQL;
|
|
293989
294308
|
const publicBuildSQL = projectToPublicColumns(persistSource, buildSQL);
|
|
293990
|
-
return this.buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, publicBuildSQL, builtEntries, dependsOnStorageUpstream);
|
|
294309
|
+
return this.buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, publicBuildSQL, builtEntries, dependsOnStorageUpstream, runOptions.queryMetadata);
|
|
293991
294310
|
}
|
|
293992
|
-
const runOptions = this.buildRunSQLOptions(persistSource, environment, buildMetadata);
|
|
293993
294311
|
const dialect = persistSource.dialectName;
|
|
293994
294312
|
const quotedPhysicalPath = quoteTablePath(physicalTableName, dialect);
|
|
293995
294313
|
const lineage = incremental && contentSourceEntityId ? incrementalLineage({
|
|
@@ -294028,8 +294346,10 @@ class MaterializationService {
|
|
|
294028
294346
|
}
|
|
294029
294347
|
const startTime = performance.now();
|
|
294030
294348
|
await connection.runSQL(`DROP TABLE IF EXISTS ${quotedStaging}`, runOptions);
|
|
294349
|
+
let buildCostBytes;
|
|
294031
294350
|
try {
|
|
294032
|
-
await connection.runSQL(`CREATE TABLE ${quotedStaging} AS (${buildSQL})`, runOptions);
|
|
294351
|
+
const ctas = await connection.runSQL(`CREATE TABLE ${quotedStaging} AS (${buildSQL})`, runOptions);
|
|
294352
|
+
buildCostBytes = ctas?.runStats?.queryCostBytes;
|
|
294033
294353
|
await connection.runSQL(`DROP TABLE IF EXISTS ${quotedPhysical}`, runOptions);
|
|
294034
294354
|
await connection.runSQL(`ALTER TABLE ${quotedStaging} RENAME TO ${quotedBareName}`, runOptions);
|
|
294035
294355
|
} catch (err) {
|
|
@@ -294067,7 +294387,9 @@ class MaterializationService {
|
|
|
294067
294387
|
realization: instruction.realization,
|
|
294068
294388
|
...ledgerFields(incrementalRefresh?.lineage, seededThrough),
|
|
294069
294389
|
...refreshFields(incrementalRefresh ? "full" : undefined),
|
|
294070
|
-
rowCount: null
|
|
294390
|
+
rowCount: null,
|
|
294391
|
+
buildDurationMs: durationMs,
|
|
294392
|
+
queryCostBytes: buildCostBytes ?? null
|
|
294071
294393
|
};
|
|
294072
294394
|
}
|
|
294073
294395
|
async refreshOneSourceIncrementally(params) {
|
|
@@ -294096,6 +294418,7 @@ class MaterializationService {
|
|
|
294096
294418
|
if (step.mode === "seed")
|
|
294097
294419
|
return;
|
|
294098
294420
|
const startTime = performance.now();
|
|
294421
|
+
let appliedDurationMs;
|
|
294099
294422
|
if (step.mode === "delta") {
|
|
294100
294423
|
await applyDeltaScript(runner, dialect, step.statements);
|
|
294101
294424
|
await advanceLedger({
|
|
@@ -294104,6 +294427,7 @@ class MaterializationService {
|
|
|
294104
294427
|
coveredThrough: step.coveredThrough
|
|
294105
294428
|
});
|
|
294106
294429
|
const durationMs = Math.round(performance.now() - startTime);
|
|
294430
|
+
appliedDurationMs = durationMs;
|
|
294107
294431
|
recordSourceBuildDuration(durationMs, "delta");
|
|
294108
294432
|
reportDeltaApplied({
|
|
294109
294433
|
packageName: context.packageName,
|
|
@@ -294125,11 +294449,13 @@ class MaterializationService {
|
|
|
294125
294449
|
connectionName: persistSource.connectionName,
|
|
294126
294450
|
realization: instruction.realization,
|
|
294127
294451
|
rowCount: null,
|
|
294452
|
+
buildDurationMs: appliedDurationMs ?? null,
|
|
294453
|
+
queryCostBytes: null,
|
|
294128
294454
|
...ledgerFields(lineage, step.coveredThrough),
|
|
294129
294455
|
...refreshFields(step.mode === "delta" ? "delta" : "none")
|
|
294130
294456
|
};
|
|
294131
294457
|
}
|
|
294132
|
-
async buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, buildSQL, builtEntries, dependsOnStorageUpstream) {
|
|
294458
|
+
async buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, buildSQL, builtEntries, dependsOnStorageUpstream, queryMetadata) {
|
|
294133
294459
|
const sourceEntityId = instruction.sourceEntityId;
|
|
294134
294460
|
const physicalTableName = instruction.physicalTableName;
|
|
294135
294461
|
const destinationName = instruction.destination;
|
|
@@ -294169,17 +294495,21 @@ class MaterializationService {
|
|
|
294169
294495
|
sourceConnection,
|
|
294170
294496
|
buildSQL,
|
|
294171
294497
|
physicalTableName,
|
|
294172
|
-
environmentPath: environment.getEnvironmentPath()
|
|
294498
|
+
environmentPath: environment.getEnvironmentPath(),
|
|
294499
|
+
queryMetadata
|
|
294173
294500
|
});
|
|
294174
294501
|
} catch (err) {
|
|
294175
294502
|
const safeDetail = redactConnectionSecrets(errMessage(err), sourceConnection, destinationConnection);
|
|
294176
|
-
|
|
294503
|
+
const alreadyBilled = err instanceof BilledReadNotCapturedError;
|
|
294504
|
+
recordStorageBuildFailure(destinationName, alreadyBilled ? "billed_read_not_captured" : "build_failed");
|
|
294177
294505
|
logger.warn("Storage materialization build failed", {
|
|
294178
294506
|
sourceName: persistSource.name,
|
|
294179
294507
|
destinationName,
|
|
294508
|
+
alreadyBilled,
|
|
294180
294509
|
error: safeDetail
|
|
294181
294510
|
});
|
|
294182
|
-
|
|
294511
|
+
const failure = `Failed to materialize source '${persistSource.name}' into ` + `storage destination '${destinationName}': ${safeDetail}`;
|
|
294512
|
+
throw alreadyBilled ? new BilledReadNotCapturedError(failure, { cause: err }) : new Error(failure, { cause: err });
|
|
294183
294513
|
}
|
|
294184
294514
|
}
|
|
294185
294515
|
try {
|
|
@@ -294215,7 +294545,8 @@ class MaterializationService {
|
|
|
294215
294545
|
physicalTableName,
|
|
294216
294546
|
storageDestinationName: result.storageDestinationName,
|
|
294217
294547
|
columns: result.schema.length,
|
|
294218
|
-
durationMs
|
|
294548
|
+
durationMs,
|
|
294549
|
+
readCost: result.readCost
|
|
294219
294550
|
});
|
|
294220
294551
|
return {
|
|
294221
294552
|
sourceEntityId,
|
|
@@ -294226,7 +294557,9 @@ class MaterializationService {
|
|
|
294226
294557
|
storageDestinationName: result.storageDestinationName,
|
|
294227
294558
|
schema: result.schema,
|
|
294228
294559
|
realization: instruction.realization,
|
|
294229
|
-
rowCount: null
|
|
294560
|
+
rowCount: null,
|
|
294561
|
+
buildDurationMs: durationMs,
|
|
294562
|
+
queryCostBytes: result.readCost?.bytesScanned ?? null
|
|
294230
294563
|
};
|
|
294231
294564
|
}
|
|
294232
294565
|
async buildDownstreamViaParents(persistSource, destinationName, destinationConnection, builtEntries, environment, physicalTableName) {
|