@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/app/api-doc.yaml +135 -8
- 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 +464 -83
- package/package.json +1 -1
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
|
|
266663
|
+
for (const [field, value] of Object.entries({
|
|
266661
266664
|
account: sf.account,
|
|
266662
|
-
username: sf.username
|
|
266663
|
-
|
|
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
|
-
|
|
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
|
|
267916
|
-
|
|
267917
|
-
|
|
267918
|
-
|
|
267919
|
-
|
|
267920
|
-
|
|
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.
|
|
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
|
-
|
|
279417
|
-
|
|
279418
|
-
|
|
279419
|
-
"
|
|
279420
|
-
|
|
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
|
-
|
|
279524
|
-
|
|
279525
|
-
|
|
279526
|
-
"
|
|
279527
|
-
|
|
279528
|
-
|
|
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.
|
|
279591
|
-
|
|
279592
|
-
|
|
279593
|
-
|
|
279594
|
-
"
|
|
279595
|
-
|
|
279596
|
-
|
|
279597
|
-
|
|
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
|
|
293084
|
-
const schema = await createTableAndDescribe(session, target,
|
|
293085
|
-
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
|
+
};
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|