@malloy-publisher/server 0.0.242 → 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
@@ -265609,6 +265609,9 @@ var PROXIED_SSLMODES = [
265609
265609
  "verify-full"
265610
265610
  ];
265611
265611
  var PUBLISHER_DUCKDB_API_FIELDS = new Set(["attachedDatabases"]);
265612
+ function nullToUndefined(value) {
265613
+ return value ?? undefined;
265614
+ }
265612
265615
  function normalizeSnowflakePrivateKey(privateKey) {
265613
265616
  let privateKeyContent = privateKey.trim();
265614
265617
  if (!privateKeyContent.includes(`
@@ -266129,13 +266132,13 @@ function assembleEnvironmentConnections(connections = [], environmentPath = "")
266129
266132
  is: "snowflake",
266130
266133
  account: connection.snowflakeConnection?.account,
266131
266134
  username: connection.snowflakeConnection?.username,
266132
- password: connection.snowflakeConnection?.password,
266135
+ password: nullToUndefined(connection.snowflakeConnection?.password),
266133
266136
  privateKey: connection.snowflakeConnection?.privateKey ? normalizeSnowflakePrivateKey(connection.snowflakeConnection.privateKey) : undefined,
266134
- privateKeyPass: connection.snowflakeConnection?.privateKeyPass,
266137
+ privateKeyPass: nullToUndefined(connection.snowflakeConnection?.privateKeyPass),
266135
266138
  warehouse: connection.snowflakeConnection?.warehouse,
266136
- database: connection.snowflakeConnection?.database,
266137
- schema: connection.snowflakeConnection?.schema,
266138
- role: connection.snowflakeConnection?.role,
266139
+ database: nullToUndefined(connection.snowflakeConnection?.database),
266140
+ schema: nullToUndefined(connection.snowflakeConnection?.schema),
266141
+ role: nullToUndefined(connection.snowflakeConnection?.role),
266139
266142
  timeoutMs: connection.snowflakeConnection?.responseTimeoutMilliseconds,
266140
266143
  poolMin: 1,
266141
266144
  poolMax: 20
@@ -266657,32 +266660,44 @@ async function federateSnowflake(connection, config) {
266657
266660
  if (!sf) {
266658
266661
  throw new Error(`Snowflake connection configuration missing for: ${config.name}`);
266659
266662
  }
266660
- const required = {
266663
+ for (const [field, value] of Object.entries({
266661
266664
  account: sf.account,
266662
- username: sf.username,
266663
- password: sf.password
266664
- };
266665
- for (const [field, value] of Object.entries(required)) {
266665
+ username: sf.username
266666
+ })) {
266666
266667
  if (!value) {
266667
266668
  throw new Error(`Snowflake ${field} is required for: ${config.name}`);
266668
266669
  }
266669
266670
  }
266671
+ const usesKeyPair = !!sf.privateKey;
266672
+ if (!usesKeyPair && !sf.password) {
266673
+ throw new Error(`Snowflake privateKey or password is required for: ${config.name}`);
266674
+ }
266670
266675
  await installAndLoadExtension(connection, "snowflake", true);
266671
266676
  const params = {
266672
266677
  account: escapeSQL(sf.account || ""),
266673
266678
  user: escapeSQL(sf.username || ""),
266674
- password: escapeSQL(sf.password || ""),
266679
+ password: sf.password ? escapeSQL(sf.password) : undefined,
266680
+ privateKey: sf.privateKey ? escapeSQL(normalizeSnowflakePrivateKey(sf.privateKey)) : undefined,
266681
+ privateKeyPass: sf.privateKeyPass ? escapeSQL(sf.privateKeyPass) : undefined,
266675
266682
  database: sf.database ? escapeSQL(sf.database) : undefined,
266676
- warehouse: sf.warehouse ? escapeSQL(sf.warehouse) : undefined
266683
+ warehouse: sf.warehouse ? escapeSQL(sf.warehouse) : undefined,
266684
+ schema: sf.schema ? escapeSQL(sf.schema) : undefined,
266685
+ role: sf.role ? escapeSQL(sf.role) : undefined
266677
266686
  };
266678
266687
  const secretName = sanitizeSecretName(`snowflake_${config.name}`);
266679
266688
  const secretLines = [
266680
266689
  ` TYPE snowflake`,
266681
266690
  ` ACCOUNT '${params.account}'`,
266682
266691
  ` USER '${params.user}'`,
266683
- ` PASSWORD '${params.password}'`,
266692
+ ...usesKeyPair ? [
266693
+ ` AUTH_TYPE 'key_pair'`,
266694
+ ` PRIVATE_KEY '${params.privateKey}'`,
266695
+ ...params.privateKeyPass ? [` PRIVATE_KEY_PASSWORD '${params.privateKeyPass}'`] : []
266696
+ ] : [` PASSWORD '${params.password}'`],
266684
266697
  ...params.database ? [` DATABASE '${params.database}'`] : [],
266685
- ...params.warehouse ? [` WAREHOUSE '${params.warehouse}'`] : []
266698
+ ...params.warehouse ? [` WAREHOUSE '${params.warehouse}'`] : [],
266699
+ ...params.schema ? [` SCHEMA '${params.schema}'`] : [],
266700
+ ...params.role ? [` ROLE '${params.role}'`] : []
266686
266701
  ];
266687
266702
  await connection.runSQL(`CREATE OR REPLACE SECRET ${secretName} (
266688
266703
  ${secretLines.join(`,
@@ -267004,7 +267019,7 @@ function entryToDuckDBOptions(name, entry, workingDirectory) {
267004
267019
  return { ...removeUndefined(rest), name };
267005
267020
  }
267006
267021
  function removeUndefined(value) {
267007
- return Object.fromEntries(Object.entries(value).filter(([, fieldValue]) => fieldValue !== undefined));
267022
+ return Object.fromEntries(Object.entries(value).filter(([, fieldValue]) => fieldValue !== undefined && fieldValue !== null));
267008
267023
  }
267009
267024
  function buildSnowflakePrivateKeyConnection(metadata) {
267010
267025
  const name = metadata.apiConnection.name;
@@ -267905,6 +267920,52 @@ async function getSchemasForMySQL(connection) {
267905
267920
  }
267906
267921
  ];
267907
267922
  }
267923
+ var SNOWFLAKE_SYSTEM_DATABASES = new Set([
267924
+ "SNOWFLAKE",
267925
+ "SNOWFLAKE_SAMPLE_DATA"
267926
+ ]);
267927
+ async function listSnowflakeSchemasInDatabase(connection, malloyConnection, database, schema) {
267928
+ const filters = [
267929
+ `CATALOG_NAME = '${sqlLiteral(database, connection.type)}'`
267930
+ ];
267931
+ if (schema) {
267932
+ filters.push(`SCHEMA_NAME = '${sqlLiteral(schema, connection.type)}'`);
267933
+ }
267934
+ assertSafeSqlIdentifier(database, "database name");
267935
+ const result = await runIntrospectionSQL(malloyConnection, `SELECT CATALOG_NAME, SCHEMA_NAME, SCHEMA_OWNER FROM ${database}.INFORMATION_SCHEMA.SCHEMATA WHERE ${filters.join(" AND ")} ORDER BY SCHEMA_NAME`);
267936
+ return standardizeRunSQLResult2(result).map((row) => {
267937
+ const r = row;
267938
+ return {
267939
+ catalogName: String(r.CATALOG_NAME ?? r.catalog_name ?? ""),
267940
+ schemaName: String(r.SCHEMA_NAME ?? r.schema_name ?? ""),
267941
+ owner: String(r.SCHEMA_OWNER ?? r.schema_owner ?? "")
267942
+ };
267943
+ });
267944
+ }
267945
+ var SNOWFLAKE_SHOW_ROW_LIMIT = 1e4;
267946
+ async function listSnowflakeSchemasInAccount(malloyConnection) {
267947
+ const result = await runIntrospectionSQL(malloyConnection, `SHOW SCHEMAS IN ACCOUNT LIMIT ${SNOWFLAKE_SHOW_ROW_LIMIT}`);
267948
+ const returnedRows = standardizeRunSQLResult2(result);
267949
+ if (returnedRows.length >= SNOWFLAKE_SHOW_ROW_LIMIT) {
267950
+ logger.warn("Snowflake account-wide schema listing hit the SHOW row limit; the schema list is incomplete and some tables will not be discoverable", {
267951
+ rowLimit: SNOWFLAKE_SHOW_ROW_LIMIT,
267952
+ returnedRows: returnedRows.length
267953
+ });
267954
+ }
267955
+ const parsed = returnedRows.map((row) => {
267956
+ const r = row;
267957
+ return {
267958
+ catalogName: String(r.database_name ?? r.DATABASE_NAME ?? ""),
267959
+ schemaName: String(r.name ?? r.NAME ?? ""),
267960
+ owner: String(r.owner ?? r.OWNER ?? "")
267961
+ };
267962
+ });
267963
+ const usable = parsed.filter((r) => r.catalogName && r.schemaName);
267964
+ if (usable.length < parsed.length) {
267965
+ logger.warn("Dropped Snowflake schema rows missing a database or schema name; the schema list is incomplete", { dropped: parsed.length - usable.length, returned: parsed.length });
267966
+ }
267967
+ return usable.sort((a, b) => a.catalogName.localeCompare(b.catalogName) || a.schemaName.localeCompare(b.schemaName));
267968
+ }
267908
267969
  async function getSchemasForSnowflake(connection, malloyConnection) {
267909
267970
  if (!connection.snowflakeConnection) {
267910
267971
  throw new Error("Snowflake connection is required");
@@ -267912,28 +267973,15 @@ async function getSchemasForSnowflake(connection, malloyConnection) {
267912
267973
  try {
267913
267974
  const database = connection.snowflakeConnection.database;
267914
267975
  const schema = connection.snowflakeConnection.schema;
267915
- const filters = [];
267916
- if (database) {
267917
- filters.push(`CATALOG_NAME = '${sqlLiteral(database, connection.type)}'`);
267918
- }
267919
- if (schema) {
267920
- filters.push(`SCHEMA_NAME = '${sqlLiteral(schema, connection.type)}'`);
267921
- }
267922
- const whereClause = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
267923
- const result = await runIntrospectionSQL(malloyConnection, `SELECT CATALOG_NAME, SCHEMA_NAME, SCHEMA_OWNER FROM ${database ? `${database}.` : ""}INFORMATION_SCHEMA.SCHEMATA ${whereClause} ORDER BY SCHEMA_NAME`);
267924
- const rows = standardizeRunSQLResult2(result);
267925
- return rows.map((row) => {
267926
- const typedRow = row;
267927
- const catalogName = String(typedRow.CATALOG_NAME ?? typedRow.catalog_name ?? "");
267928
- const schemaName = String(typedRow.SCHEMA_NAME ?? typedRow.schema_name ?? "");
267929
- const owner = String(typedRow.SCHEMA_OWNER ?? typedRow.schema_owner ?? "");
267930
- return {
267931
- name: `${catalogName}.${schemaName}`,
267932
- isHidden: ["SNOWFLAKE", ""].includes(owner) || schemaName === "INFORMATION_SCHEMA",
267933
- isDefault: schema ? schemaName === schema : false
267934
- };
267935
- });
267976
+ const rows = database ? await listSnowflakeSchemasInDatabase(connection, malloyConnection, database, schema) : await listSnowflakeSchemasInAccount(malloyConnection);
267977
+ return rows.map(({ catalogName, schemaName, owner }) => ({
267978
+ name: `${catalogName}.${schemaName}`,
267979
+ isHidden: owner === "SNOWFLAKE" || Boolean(database) && owner === "" || schemaName === "INFORMATION_SCHEMA" || !database && SNOWFLAKE_SYSTEM_DATABASES.has(catalogName),
267980
+ isDefault: Boolean(database) && Boolean(schema) && schemaName === schema
267981
+ }));
267936
267982
  } catch (error) {
267983
+ if (error instanceof BadRequestError)
267984
+ throw error;
267937
267985
  logger.error(`Error getting schemas for Snowflake connection ${connection.name}`, { error });
267938
267986
  throw new Error(`Failed to get schemas for Snowflake connection ${connection.name}: ${error.message}`);
267939
267987
  }
@@ -269338,7 +269386,10 @@ class QueryController {
269338
269386
  serializedResult,
269339
269387
  rowLimit,
269340
269388
  rowLimitSource,
269341
- queryCorrelationId
269389
+ queryCorrelationId,
269390
+ servedFrom,
269391
+ executionTimeMs,
269392
+ queryCostBytes
269342
269393
  } = await runWithQueryTimeout((abortSignal) => model.getQueryResults(sourceName, queryName, query, filterParams, bypassFilters, givens, abortSignal, {
269343
269394
  request: requestMetadata,
269344
269395
  queryClass,
@@ -269365,7 +269416,10 @@ class QueryController {
269365
269416
  renderLogs: renderLogs.length > 0 ? renderLogs : undefined,
269366
269417
  queryRowLimit: rowLimit,
269367
269418
  queryRowLimitSource: rowLimitSource,
269368
- queryCorrelationId
269419
+ queryCorrelationId,
269420
+ servedFrom,
269421
+ executionTimeMs,
269422
+ queryCostBytes
269369
269423
  };
269370
269424
  }
269371
269425
  }
@@ -277214,7 +277268,8 @@ var sourceBuildDuration = lazyHistogram("publisher_materialization_source_build_
277214
277268
  var dropTablesCounter = lazyCounter2("publisher_materialization_drop_tables_total", "Physical tables dropped on delete. Label: outcome ('success'|'failure').");
277215
277269
  var scheduledFireCounter = lazyCounter2("publisher_materialization_scheduled_fires_total", "Standalone-scheduler attempts to fire a package's materialization.schedule. " + "Label: outcome ('fired'|'conflict'|'error').");
277216
277270
  var storageServeRoutingCounter = lazyCounter2("publisher_storage_serve_routing_total", "storage= serve routing decisions. Label: outcome ('storage'|'live_fallback'|'runtime_live_fallback').");
277217
- 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.");
277218
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').");
277219
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).");
277220
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').");
@@ -277255,8 +277310,11 @@ function recordSourceBuildDuration(durationMs, engine) {
277255
277310
  function recordDropTables(outcome, engine) {
277256
277311
  dropTablesCounter().add(1, { outcome, engine });
277257
277312
  }
277258
- function recordStorageBuildFailure(destination) {
277259
- 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 });
277260
277318
  }
277261
277319
  function recordEligibilityRefused(reason) {
277262
277320
  eligibilityRefusedCounter().add(1, { reason });
@@ -278637,6 +278695,10 @@ class Model {
278637
278695
  description: "How long it takes to execute a Malloy model query",
278638
278696
  unit: "ms"
278639
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
+ });
278640
278702
  constructor(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos, runnableNotebookCells, compilationError, filterMap, givens, modelInfo) {
278641
278703
  this.packageName = packageName;
278642
278704
  this.modelPath = modelPath;
@@ -279412,13 +279474,12 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
279412
279474
  } else {
279413
279475
  const endTime = performance.now();
279414
279476
  const executionTime2 = endTime - startTime;
279415
- this.queryExecutionHistogram.record(executionTime2, {
279416
- "malloy.model.path": this.modelPath,
279417
- "malloy.model.query.name": queryName,
279418
- "malloy.model.query.source": sourceName,
279419
- "malloy.model.query.query": query,
279420
- "malloy.model.query.status": "error"
279421
- });
279477
+ this.queryExecutionHistogram.record(executionTime2, this.queryMetricAttributes({
279478
+ environment: queryMetadataInput?.environment,
279479
+ queryName,
279480
+ sourceName,
279481
+ status: "error"
279482
+ }));
279422
279483
  throw new BadRequestError("Invalid query request. (Query AND !sourceName) OR (queryName AND sourceName) must be defined.");
279423
279484
  }
279424
279485
  const isAdHocQuery = !sourceName && !queryName && !!query;
@@ -279505,7 +279566,6 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
279505
279566
  maxRows
279506
279567
  });
279507
279568
  appliedQueryMetadata = this.resolveQueryMetadata(queryMetadataInput, preparedResult.connectionName);
279508
- executionTime = performance.now() - startTime;
279509
279569
  queryResults = await runnable.run({
279510
279570
  rowLimit,
279511
279571
  givens: effectiveGivens,
@@ -279514,19 +279574,19 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
279514
279574
  virtualMap: serveVirtualMap,
279515
279575
  queryMetadata: appliedQueryMetadata
279516
279576
  });
279577
+ executionTime = performance.now() - startTime;
279517
279578
  } catch (error) {
279518
279579
  const canDegradeToLive = !!serveVirtualMap && !!liveRunnable && !abortSignal?.aborted && !String(error?.code ?? "").startsWith("runtime-given-") && serveShapeBindings.length > 0 && serveShapeBindings.every((b) => b.freshnessFallback === "live");
279519
279580
  const failQuery = (err) => {
279520
279581
  const errorEndTime = performance.now();
279521
279582
  const errorExecutionTime = errorEndTime - startTime;
279522
- this.queryExecutionHistogram.record(errorExecutionTime, {
279523
- "malloy.model.path": this.modelPath,
279524
- "malloy.model.query.name": queryName,
279525
- "malloy.model.query.source": sourceName,
279526
- "malloy.model.query.query": query,
279527
- "malloy.model.query.status": "error",
279528
- ...servedFrom ? { "malloy.model.query.served_from": servedFrom } : {}
279529
- });
279583
+ this.queryExecutionHistogram.record(errorExecutionTime, this.queryMetricAttributes({
279584
+ environment: queryMetadataInput?.environment,
279585
+ queryName,
279586
+ sourceName,
279587
+ status: "error",
279588
+ servedFrom
279589
+ }));
279530
279590
  const givenCode = err?.code;
279531
279591
  if (typeof givenCode === "string" && givenCode.startsWith("runtime-given-")) {
279532
279592
  logger.debug("Rejected client-supplied given", {
@@ -279587,17 +279647,20 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
279587
279647
  const wrappedResult = API.util.wrapResult(queryResults);
279588
279648
  const serializedResult = stringifyQueryResponse(responseShape === "compact" ? queryResults.data.value : wrappedResult, queryResults.totalRows, maxBytes, "model_query", responseShape === "compact" ? bigIntReplacer : undefined);
279589
279649
  assertWithinModelByteLimit(serializedResult, maxBytes, "model_query");
279590
- this.queryExecutionHistogram.record(executionTime, {
279591
- "malloy.model.path": this.modelPath,
279592
- "malloy.model.query.name": queryName,
279593
- "malloy.model.query.source": sourceName,
279594
- "malloy.model.query.query": query,
279595
- "malloy.model.query.rows_limit": rowLimit,
279596
- "malloy.model.query.rows_total": queryResults.totalRows,
279597
- "malloy.model.query.connection": queryResults.connectionName,
279598
- "malloy.model.query.status": "success",
279599
- ...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
279600
279658
  });
279659
+ this.queryExecutionHistogram.record(executionTime, metricAttributes);
279660
+ const queryCostBytes = queryResults.runStats?.queryCostBytes;
279661
+ if (queryCostBytes !== undefined) {
279662
+ this.queryScannedBytesCounter.add(queryCostBytes, metricAttributes);
279663
+ }
279601
279664
  return {
279602
279665
  result: wrappedResult,
279603
279666
  serializedResult,
@@ -279606,7 +279669,23 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
279606
279669
  dataStyles: this.dataStyles,
279607
279670
  rowLimit,
279608
279671
  rowLimitSource,
279609
- 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 } : {}
279610
279689
  };
279611
279690
  }
279612
279691
  resolveQueryMetadata(input, connectionName) {
@@ -292992,6 +293071,130 @@ import {
292992
293071
  import { mkdirSync as mkdirSync2, mkdtempSync, rmSync } from "node:fs";
292993
293072
  import os4 from "node:os";
292994
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
292995
293198
  var sharedGateSession;
292996
293199
  var PASSTHROUGH_SOURCE_TYPES = [
292997
293200
  "bigquery",
@@ -293015,6 +293218,157 @@ function wrapPassthrough(sourceType, handle, innerSQL) {
293015
293218
  }
293016
293219
  }
293017
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
+ }
293018
293372
  function passthroughSourceType(sourceConnection) {
293019
293373
  const type = sourceConnection.type;
293020
293374
  if (PASSTHROUGH_SOURCE_TYPES.includes(type ?? "")) {
@@ -293068,22 +293422,30 @@ async function buildSourceIntoStorage(params) {
293068
293422
  sourceConnection,
293069
293423
  buildSQL,
293070
293424
  physicalTableName,
293071
- environmentPath
293425
+ environmentPath,
293426
+ queryMetadata
293072
293427
  } = params;
293073
293428
  assertSupportedDestination(destinationName, destinationConnection);
293074
293429
  const sourceType = passthroughSourceType(sourceConnection);
293075
293430
  const { session, dispose } = createIsolatedBuildSession(`build_${destinationName}`);
293431
+ let federatedHandle;
293076
293432
  try {
293077
293433
  await attachDestinationReadWrite(session, destinationName, destinationConnection, environmentPath);
293078
293434
  if (destinationConnection.type === "ducklake") {
293079
293435
  await session.runSQL("SET ducklake_default_data_inlining_row_limit=0");
293080
293436
  }
293081
293437
  const federated = await federateSourceForPassthrough(session, sourceType, sourceFederationConfig(sourceConnection));
293438
+ await tagSnowflakeSession(session, sourceType, federated.handle, queryMetadata);
293082
293439
  const target = quoteManifestTablePath(`${destinationName}.${physicalTableName}`, "duckdb");
293083
- const passthrough = wrapPassthrough(sourceType, federated.handle, buildSQL);
293084
- const schema = await createTableAndDescribe(session, target, passthrough);
293085
- 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
+ };
293086
293447
  } finally {
293448
+ await clearSnowflakeSessionTag(session, sourceType, federatedHandle);
293087
293449
  await dispose();
293088
293450
  }
293089
293451
  }
@@ -293134,7 +293496,11 @@ async function buildDownstreamIntoStorage(params) {
293134
293496
  const sql = projectToPublicColumns(downstream, downstream.getSQL({ virtualMap }));
293135
293497
  const target = quoteManifestTablePath(`${destinationName}.${physicalTableName}`, "duckdb");
293136
293498
  const schema = await createTableAndDescribe(session, target, sql);
293137
- return { storageDestinationName: destinationName, schema };
293499
+ return {
293500
+ storageDestinationName: destinationName,
293501
+ schema,
293502
+ readCost: null
293503
+ };
293138
293504
  } finally {
293139
293505
  await dispose();
293140
293506
  }
@@ -293933,15 +294299,15 @@ class MaterializationService {
293933
294299
  buildManifest,
293934
294300
  connectionDigests
293935
294301
  });
294302
+ const runOptions = this.buildRunSQLOptions(persistSource, environment, buildMetadata);
293936
294303
  if (isStorageBuild) {
293937
294304
  const dependsOnStorageUpstream = persistSource.getSQL({
293938
294305
  buildManifest: manifest.buildManifest,
293939
294306
  connectionDigests
293940
294307
  }) !== buildSQL;
293941
294308
  const publicBuildSQL = projectToPublicColumns(persistSource, buildSQL);
293942
- return this.buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, publicBuildSQL, builtEntries, dependsOnStorageUpstream);
294309
+ return this.buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, publicBuildSQL, builtEntries, dependsOnStorageUpstream, runOptions.queryMetadata);
293943
294310
  }
293944
- const runOptions = this.buildRunSQLOptions(persistSource, environment, buildMetadata);
293945
294311
  const dialect = persistSource.dialectName;
293946
294312
  const quotedPhysicalPath = quoteTablePath(physicalTableName, dialect);
293947
294313
  const lineage = incremental && contentSourceEntityId ? incrementalLineage({
@@ -293980,8 +294346,10 @@ class MaterializationService {
293980
294346
  }
293981
294347
  const startTime = performance.now();
293982
294348
  await connection.runSQL(`DROP TABLE IF EXISTS ${quotedStaging}`, runOptions);
294349
+ let buildCostBytes;
293983
294350
  try {
293984
- 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;
293985
294353
  await connection.runSQL(`DROP TABLE IF EXISTS ${quotedPhysical}`, runOptions);
293986
294354
  await connection.runSQL(`ALTER TABLE ${quotedStaging} RENAME TO ${quotedBareName}`, runOptions);
293987
294355
  } catch (err) {
@@ -294019,7 +294387,9 @@ class MaterializationService {
294019
294387
  realization: instruction.realization,
294020
294388
  ...ledgerFields(incrementalRefresh?.lineage, seededThrough),
294021
294389
  ...refreshFields(incrementalRefresh ? "full" : undefined),
294022
- rowCount: null
294390
+ rowCount: null,
294391
+ buildDurationMs: durationMs,
294392
+ queryCostBytes: buildCostBytes ?? null
294023
294393
  };
294024
294394
  }
294025
294395
  async refreshOneSourceIncrementally(params) {
@@ -294048,6 +294418,7 @@ class MaterializationService {
294048
294418
  if (step.mode === "seed")
294049
294419
  return;
294050
294420
  const startTime = performance.now();
294421
+ let appliedDurationMs;
294051
294422
  if (step.mode === "delta") {
294052
294423
  await applyDeltaScript(runner, dialect, step.statements);
294053
294424
  await advanceLedger({
@@ -294056,6 +294427,7 @@ class MaterializationService {
294056
294427
  coveredThrough: step.coveredThrough
294057
294428
  });
294058
294429
  const durationMs = Math.round(performance.now() - startTime);
294430
+ appliedDurationMs = durationMs;
294059
294431
  recordSourceBuildDuration(durationMs, "delta");
294060
294432
  reportDeltaApplied({
294061
294433
  packageName: context.packageName,
@@ -294077,11 +294449,13 @@ class MaterializationService {
294077
294449
  connectionName: persistSource.connectionName,
294078
294450
  realization: instruction.realization,
294079
294451
  rowCount: null,
294452
+ buildDurationMs: appliedDurationMs ?? null,
294453
+ queryCostBytes: null,
294080
294454
  ...ledgerFields(lineage, step.coveredThrough),
294081
294455
  ...refreshFields(step.mode === "delta" ? "delta" : "none")
294082
294456
  };
294083
294457
  }
294084
- async buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, buildSQL, builtEntries, dependsOnStorageUpstream) {
294458
+ async buildOneSourceIntoStorage(persistSource, instruction, manifest, environment, buildSQL, builtEntries, dependsOnStorageUpstream, queryMetadata) {
294085
294459
  const sourceEntityId = instruction.sourceEntityId;
294086
294460
  const physicalTableName = instruction.physicalTableName;
294087
294461
  const destinationName = instruction.destination;
@@ -294121,17 +294495,21 @@ class MaterializationService {
294121
294495
  sourceConnection,
294122
294496
  buildSQL,
294123
294497
  physicalTableName,
294124
- environmentPath: environment.getEnvironmentPath()
294498
+ environmentPath: environment.getEnvironmentPath(),
294499
+ queryMetadata
294125
294500
  });
294126
294501
  } catch (err) {
294127
294502
  const safeDetail = redactConnectionSecrets(errMessage(err), sourceConnection, destinationConnection);
294128
- recordStorageBuildFailure(destinationName);
294503
+ const alreadyBilled = err instanceof BilledReadNotCapturedError;
294504
+ recordStorageBuildFailure(destinationName, alreadyBilled ? "billed_read_not_captured" : "build_failed");
294129
294505
  logger.warn("Storage materialization build failed", {
294130
294506
  sourceName: persistSource.name,
294131
294507
  destinationName,
294508
+ alreadyBilled,
294132
294509
  error: safeDetail
294133
294510
  });
294134
- 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 });
294135
294513
  }
294136
294514
  }
294137
294515
  try {
@@ -294167,7 +294545,8 @@ class MaterializationService {
294167
294545
  physicalTableName,
294168
294546
  storageDestinationName: result.storageDestinationName,
294169
294547
  columns: result.schema.length,
294170
- durationMs
294548
+ durationMs,
294549
+ readCost: result.readCost
294171
294550
  });
294172
294551
  return {
294173
294552
  sourceEntityId,
@@ -294178,7 +294557,9 @@ class MaterializationService {
294178
294557
  storageDestinationName: result.storageDestinationName,
294179
294558
  schema: result.schema,
294180
294559
  realization: instruction.realization,
294181
- rowCount: null
294560
+ rowCount: null,
294561
+ buildDurationMs: durationMs,
294562
+ queryCostBytes: result.readCost?.bytesScanned ?? null
294182
294563
  };
294183
294564
  }
294184
294565
  async buildDownstreamViaParents(persistSource, destinationName, destinationConnection, builtEntries, environment, physicalTableName) {