@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/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. Label: destination (connection name).");
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
- "malloy.model.path": this.modelPath,
279465
- "malloy.model.query.name": queryName,
279466
- "malloy.model.query.source": sourceName,
279467
- "malloy.model.query.query": query,
279468
- "malloy.model.query.status": "error"
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
- "malloy.model.path": this.modelPath,
279572
- "malloy.model.query.name": queryName,
279573
- "malloy.model.query.source": sourceName,
279574
- "malloy.model.query.query": query,
279575
- "malloy.model.query.status": "error",
279576
- ...servedFrom ? { "malloy.model.query.served_from": servedFrom } : {}
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.queryExecutionHistogram.record(executionTime, {
279639
- "malloy.model.path": this.modelPath,
279640
- "malloy.model.query.name": queryName,
279641
- "malloy.model.query.source": sourceName,
279642
- "malloy.model.query.query": query,
279643
- "malloy.model.query.rows_limit": rowLimit,
279644
- "malloy.model.query.rows_total": queryResults.totalRows,
279645
- "malloy.model.query.connection": queryResults.connectionName,
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 passthrough = wrapPassthrough(sourceType, federated.handle, buildSQL);
293132
- const schema = await createTableAndDescribe(session, target, passthrough);
293133
- return { storageDestinationName: destinationName, schema };
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 { storageDestinationName: destinationName, schema };
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
- recordStorageBuildFailure(destinationName);
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
- throw new Error(`Failed to materialize source '${persistSource.name}' into ` + `storage destination '${destinationName}': ${safeDetail}`);
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) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.0.243",
4
+ "version": "0.0.244",
5
5
  "main": "dist/server.mjs",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.mjs"