@malloy-publisher/server 0.10.3 → 0.10.5

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.
@@ -6685,8 +6685,11 @@ components:
6685
6685
  `sqlSource` answer 400 for anything else (another statement type, or
6686
6686
  more than one statement), and `sqlTemporaryTable` answers 400 always.
6687
6687
  This is a rule on statements, not on what a SELECT reads: that is
6688
- bounded by the session's storage secret and catalog role. Absent or
6689
- false is the pre-existing behavior.
6688
+ bounded by the catalog role and by the session's storage secret,
6689
+ which on a managed connection attached read-only is scoped to the
6690
+ `main` schema's recorded path, so a SELECT reads only that schema's
6691
+ files; another schema's files, and any other object in the bucket,
6692
+ are unsigned (403). Absent or false is the pre-existing behavior.
6690
6693
  fingerprint:
6691
6694
  type: string
6692
6695
  description: >
@@ -7157,7 +7160,11 @@ components:
7157
7160
  every attach uses the data path the catalog recorded at its
7158
7161
  creation, and no attach ever creates a catalog: the shape for a
7159
7162
  catalog created elsewhere, through the admin API
7160
- (admin-api-doc.yaml) or by another tool.
7163
+ (admin-api-doc.yaml) or by another tool. Either way the storage
7164
+ secret is scoped to the data path, the given one or the
7165
+ recorded one: a read of any other object through this
7166
+ connection is unsigned, and on S3 the host's AWS_* environment
7167
+ never signs it either. A catalog with neither is not attached.
7161
7168
  s3Connection:
7162
7169
  $ref: "#/components/schemas/S3Connection"
7163
7170
  description: AWS S3 connection configuration for data storage
@@ -7990,17 +7997,18 @@ components:
7990
7997
  BuildPlan:
7991
7998
  type: object
7992
7999
  description: |
7993
- The package's persist build plan. Mirrors Malloy's native build plan plus
7994
- the minimal per-source detail a caller needs to assign
7995
- identity/naming/realization. Lineage, policy, and connection capability
7996
- are intentionally omitted until they carry real data.
8000
+ The package's persist build plan, from Malloy's build targets: the order
8001
+ its persist sources build in, the tables each reads, and the minimal
8002
+ per-source detail a caller needs to assign identity/naming/realization.
8003
+ Policy and connection capability are intentionally omitted until they
8004
+ carry real data.
7997
8005
  required: [graphs, sources]
7998
8006
  properties:
7999
8007
  graphs:
8000
8008
  type: array
8001
8009
  description: >-
8002
8010
  Dependency-ordered build graphs. One per connection per model, not one
8003
- per connection: a model contributes a graph for each connection its ROOT
8011
+ per connection: a model contributes a graph for each connection its
8004
8012
  persist sources use, and a package's plan concatenates every model's, so
8005
8013
  the same source appears in the graph of every model that imports it.
8006
8014
  items:
@@ -8143,8 +8151,9 @@ components:
8143
8151
  nodes:
8144
8152
  type: array
8145
8153
  description:
8146
- Leveled build nodes; each inner array is one parallelizable level,
8147
- levels run in order.
8154
+ A single level holding every persist source of the graph, in build
8155
+ order. Each node's `dependsOn` names every source on every table it
8156
+ reads, so the edges are complete without walking anything.
8148
8157
  items:
8149
8158
  type: array
8150
8159
  items:
@@ -8159,7 +8168,11 @@ components:
8159
8168
  description: "sourceName@modelURL"
8160
8169
  dependsOn:
8161
8170
  type: array
8162
- description: Upstream sourceIDs in this graph.
8171
+ description: >-
8172
+ sourceIDs of the sources on the tables this source reads. Sources on
8173
+ one table (a source and its extensions) read none of each other. A
8174
+ source Malloy cannot plan, which is refused rather than built, lists
8175
+ none.
8163
8176
  items:
8164
8177
  type: string
8165
8178
 
Binary file
@@ -164831,7 +164831,7 @@ var init_telemetry = __esm(() => {
164831
164831
  });
164832
164832
 
164833
164833
  // package.json
164834
- var version = "0.10.3";
164834
+ var version = "0.10.5";
164835
164835
  var init_package = () => {};
164836
164836
 
164837
164837
  // src/version.ts
@@ -22216,7 +22216,7 @@ async function compileSynthesizedPreaggregation(args) {
22216
22216
  const { runtime, importBaseURL } = await args.getRuntime(new Map([[synthesizedURL.href, text]]));
22217
22217
  const materializer = runtime.loadModel(synthesizedURL, { importBaseURL });
22218
22218
  const model = await materializer.getModel();
22219
- return { plans, model, materializer, text };
22219
+ return { plans, model, materializer, runtime, text };
22220
22220
  }
22221
22221
  async function tryCompileSynthesizedPreaggregation(args) {
22222
22222
  try {
@@ -23215,27 +23215,47 @@ function composeDeclaredQueryMetadata(layers) {
23215
23215
  const resolved = Object.assign({}, ...ordered);
23216
23216
  return Object.keys(resolved).length > 0 ? resolved : null;
23217
23217
  }
23218
- function flattenDependsOn(node) {
23219
- return node.dependsOn.map((d) => d.sourceID);
23220
- }
23221
- function* iterGraphSources(graph, sources) {
23222
- const seen = new Set;
23223
- function* visit(node) {
23224
- if (seen.has(node.sourceID))
23225
- return;
23226
- for (const dep of node.dependsOn) {
23227
- yield* visit(dep);
23218
+ function sourceBuildOf(build, unbuildable) {
23219
+ const order = [];
23220
+ const dependsOn = {};
23221
+ for (const target of build.targets) {
23222
+ const reads = target.dependsOn.flatMap((t) => t.sources.map((s) => s.sourceID));
23223
+ for (const source of target.sources) {
23224
+ order.push(source.sourceID);
23225
+ dependsOn[source.sourceID] = reads;
23228
23226
  }
23229
- seen.add(node.sourceID);
23230
- const source = sources[node.sourceID];
23227
+ }
23228
+ for (const { source } of unbuildable) {
23229
+ if (source.connectionName !== build.connectionName)
23230
+ continue;
23231
+ order.push(source.sourceID);
23232
+ dependsOn[source.sourceID] = [];
23233
+ }
23234
+ return { connectionName: build.connectionName, order, dependsOn };
23235
+ }
23236
+ function sourceBuildsOf(targets) {
23237
+ const builds = targets.connections.map((c) => sourceBuildOf(c, targets.unbuildable));
23238
+ const planned = new Set(builds.map((b) => b.connectionName));
23239
+ for (const { source } of targets.unbuildable) {
23240
+ if (planned.has(source.connectionName))
23241
+ continue;
23242
+ planned.add(source.connectionName);
23243
+ builds.push(sourceBuildOf({ connectionName: source.connectionName, targets: [] }, targets.unbuildable));
23244
+ }
23245
+ return builds;
23246
+ }
23247
+ function plannedSources(targets) {
23248
+ return [
23249
+ ...targets.connections.flatMap((c) => c.targets.flatMap((t) => t.sources)),
23250
+ ...targets.unbuildable.map((u) => u.source)
23251
+ ];
23252
+ }
23253
+ function* sourcesInBuildOrder(build, sources) {
23254
+ for (const sourceID of build.order) {
23255
+ const source = sources[sourceID];
23231
23256
  if (source)
23232
23257
  yield source;
23233
23258
  }
23234
- for (const level of graph.nodes) {
23235
- for (const node of level) {
23236
- yield* visit(node);
23237
- }
23238
- }
23239
23259
  }
23240
23260
  function computeSourceEntityId(source, connectionDigests) {
23241
23261
  return source.makeBuildId(connectionDigests[source.connectionName], addressPartitionLayout(source, source.getSQL()));
@@ -23324,7 +23344,7 @@ async function classifyPersistSourceGate(persistSource, modelDef, materializer,
23324
23344
  }
23325
23345
  function emptyBuildPlanParts() {
23326
23346
  return {
23327
- graphs: [],
23347
+ builds: [],
23328
23348
  sources: {},
23329
23349
  sourceModelPaths: {},
23330
23350
  droppedPersistSources: [],
@@ -23333,7 +23353,7 @@ function emptyBuildPlanParts() {
23333
23353
  };
23334
23354
  }
23335
23355
  function mergeBuildPlanParts(into, from) {
23336
- into.graphs.push(...from.graphs);
23356
+ into.builds.push(...from.builds);
23337
23357
  Object.assign(into.sources, from.sources);
23338
23358
  Object.assign(into.sourceModelPaths, from.sourceModelPaths);
23339
23359
  into.droppedPersistSources.push(...from.droppedPersistSources);
@@ -23341,7 +23361,14 @@ function mergeBuildPlanParts(into, from) {
23341
23361
  Object.assign(into.sourceGateOutcomes, from.sourceGateOutcomes);
23342
23362
  }
23343
23363
  async function collectModelBuildPlan(parts, args) {
23344
- const { modelPath, packagePath, materializer, malloyModel, getRuntime } = args;
23364
+ const {
23365
+ modelPath,
23366
+ packagePath,
23367
+ materializer,
23368
+ malloyModel,
23369
+ runtime,
23370
+ getRuntime
23371
+ } = args;
23345
23372
  const synthesized = args.synthesized !== undefined ? args.synthesized : await tryCompileSynthesizedPreaggregation({
23346
23373
  packagePath,
23347
23374
  modelPath,
@@ -23349,14 +23376,14 @@ async function collectModelBuildPlan(parts, args) {
23349
23376
  contents: malloyModel._modelDef?.contents ?? {}
23350
23377
  });
23351
23378
  if (synthesized) {
23352
- const rollupPlan = synthesized.model.getBuildPlan();
23353
- const rollupNames = new Set(synthesized.plans.map((p) => p.rollupSourceName));
23354
- parts.graphs.push(...rollupPlan.graphs);
23379
+ const rollupTargets = await synthesized.runtime.getBuildTargets(synthesized.model);
23380
+ parts.builds.push(...sourceBuildsOf(rollupTargets));
23355
23381
  const planByName = new Map(synthesized.plans.map((p) => [p.rollupSourceName, p]));
23356
23382
  let rollupClassificationCtx;
23357
- for (const [sourceID, source] of Object.entries(rollupPlan.sources)) {
23358
- if (!rollupNames.has(source.name))
23383
+ for (const source of plannedSources(rollupTargets)) {
23384
+ if (!planByName.has(source.name))
23359
23385
  continue;
23386
+ const sourceID = source.sourceID;
23360
23387
  parts.sources[sourceID] = source;
23361
23388
  parts.sourceModelPaths[sourceID] = modelPath;
23362
23389
  const rollup = planByName.get(source.name);
@@ -23375,23 +23402,25 @@ async function collectModelBuildPlan(parts, args) {
23375
23402
  const modelTag = malloyModel.modelAnnotations.parseAsTag("!").tag;
23376
23403
  if (!modelTag.has("experimental", "persistence"))
23377
23404
  return;
23378
- const buildPlan = malloyModel.getBuildPlan();
23379
- for (const msg of buildPlan.tagParseLog) {
23405
+ const targets = await runtime.getBuildTargets(malloyModel);
23406
+ for (const msg of targets.tagParseLog) {
23380
23407
  logger.warn("Persist annotation issue", {
23381
23408
  modelPath,
23382
23409
  message: msg.message,
23383
23410
  severity: msg.severity
23384
23411
  });
23385
23412
  }
23386
- const recognizedNames = new Set(Object.values(buildPlan.sources).map((s) => s.name));
23413
+ const planned = plannedSources(targets);
23414
+ const recognizedNames = new Set(planned.map((s) => s.name));
23387
23415
  for (const name of detectDroppedPersistSources(malloyModel, recognizedNames)) {
23388
23416
  parts.droppedPersistSources.push({ name, modelPath });
23389
23417
  }
23390
- if (buildPlan.graphs.length === 0)
23418
+ if (planned.length === 0)
23391
23419
  return;
23392
- parts.graphs.push(...buildPlan.graphs);
23420
+ parts.builds.push(...sourceBuildsOf(targets));
23393
23421
  let classificationCtx;
23394
- for (const [sourceID, source] of Object.entries(buildPlan.sources)) {
23422
+ for (const source of planned) {
23423
+ const sourceID = source.sourceID;
23395
23424
  parts.sources[sourceID] = source;
23396
23425
  parts.sourceModelPaths[sourceID] = modelPath;
23397
23426
  if (!classificationCtx) {
@@ -23404,28 +23433,30 @@ async function collectModelBuildPlan(parts, args) {
23404
23433
  parts.sourceGateOutcomes[sourceID] = await classifyPersistSourceGate(source, classificationCtx.modelDef, materializer, classificationCtx.deps, modelPath);
23405
23434
  }
23406
23435
  }
23407
- async function resolveConnectionDigests(connections, graphs, onSkipped) {
23436
+ async function resolveConnectionDigests(connections, builds, onSkipped) {
23408
23437
  const connectionDigests = {};
23409
- for (const graph of graphs) {
23410
- const conn = connections.get(graph.connectionName);
23438
+ for (const build of builds) {
23439
+ const conn = connections.get(build.connectionName);
23411
23440
  if (!conn) {
23412
- onSkipped(graph.connectionName);
23441
+ onSkipped(build.connectionName);
23413
23442
  continue;
23414
23443
  }
23415
- if (!connectionDigests[graph.connectionName]) {
23416
- connectionDigests[graph.connectionName] = await conn.getDigest();
23444
+ if (!connectionDigests[build.connectionName]) {
23445
+ connectionDigests[build.connectionName] = await conn.getDigest();
23417
23446
  }
23418
23447
  }
23419
23448
  return connectionDigests;
23420
23449
  }
23421
- function deriveBuildPlan(graphs, sources, connectionDigests, sourceNames, sourceModelPaths, packageMaterialization, options) {
23450
+ function deriveBuildPlan(builds, sources, connectionDigests, sourceNames, sourceModelPaths, packageMaterialization, options) {
23422
23451
  const include = sourceNames ? new Set(sourceNames) : null;
23423
- const wireGraphs = graphs.map((graph) => ({
23424
- connectionName: graph.connectionName,
23425
- nodes: graph.nodes.map((level) => level.map((node) => ({
23426
- sourceID: node.sourceID,
23427
- dependsOn: flattenDependsOn(node)
23428
- })))
23452
+ const wireGraphs = builds.map((build) => ({
23453
+ connectionName: build.connectionName,
23454
+ nodes: [
23455
+ build.order.map((sourceID) => ({
23456
+ sourceID,
23457
+ dependsOn: build.dependsOn[sourceID] ?? []
23458
+ }))
23459
+ ]
23429
23460
  }));
23430
23461
  const wireSources = {};
23431
23462
  const refusedSources = {};
@@ -23514,7 +23545,7 @@ function deriveBuildPlan(graphs, sources, connectionDigests, sourceNames, source
23514
23545
  function deriveBuildPlanOutcome(compiled, materializationConfig) {
23515
23546
  const droppedPersistSources = compiled.droppedPersistSources ?? [];
23516
23547
  const incrementalDeclarations = collectIncrementalDeclarations(compiled.sources);
23517
- const plan = compiled.graphs.length === 0 ? null : deriveBuildPlan(compiled.graphs, compiled.sources, compiled.connectionDigests, undefined, compiled.sourceModelPaths, materializationConfig, {
23548
+ const plan = compiled.builds.length === 0 ? null : deriveBuildPlan(compiled.builds, compiled.sources, compiled.connectionDigests, undefined, compiled.sourceModelPaths, materializationConfig, {
23518
23549
  preaggregatePlans: compiled.preaggregatePlans,
23519
23550
  sourceGateOutcomes: compiled.sourceGateOutcomes
23520
23551
  });
@@ -24474,6 +24505,7 @@ async function compileMalloyModel(job, malloyConfig, modelPath, load) {
24474
24505
  modelPath,
24475
24506
  materializer: mm,
24476
24507
  model: compiled,
24508
+ runtime,
24477
24509
  importBaseURL,
24478
24510
  queries,
24479
24511
  sources
@@ -24559,6 +24591,7 @@ async function prepareForMainThread(job, malloyConfig, load, model) {
24559
24591
  packagePath: job.packagePath,
24560
24592
  materializer,
24561
24593
  malloyModel: model.model,
24594
+ runtime: model.runtime,
24562
24595
  getRuntime: companionRuntime,
24563
24596
  synthesized: companion
24564
24597
  });
@@ -24798,8 +24831,8 @@ async function finishWorkerBuildPlan(malloyConfig, modelPaths, load, materializa
24798
24831
  }
24799
24832
  const connections = await resolvePackageConnections({
24800
24833
  getMalloyConnection: (name) => malloyConfig.connections.lookupConnection(name)
24801
- }, parts.graphs.map((g) => g.connectionName));
24802
- const connectionDigests = await resolveConnectionDigests(connections, parts.graphs, (name) => digestSkipped.push(name));
24834
+ }, parts.builds.map((b) => b.connectionName));
24835
+ const connectionDigests = await resolveConnectionDigests(connections, parts.builds, (name) => digestSkipped.push(name));
24803
24836
  const outcome = deriveBuildPlanOutcome({ ...parts, connectionDigests }, materializationConfig);
24804
24837
  return {
24805
24838
  ok: true,
package/dist/server.mjs CHANGED
@@ -164832,7 +164832,7 @@ var init_telemetry = __esm(() => {
164832
164832
  });
164833
164833
 
164834
164834
  // package.json
164835
- var version = "0.10.3";
164835
+ var version = "0.10.5";
164836
164836
  var init_package = () => {};
164837
164837
 
164838
164838
  // src/version.ts
@@ -268349,7 +268349,7 @@ var require_bcrypt_pbkdf = __commonJS((exports, module) => {
268349
268349
 
268350
268350
  // ../../node_modules/cpu-features/build/Release/cpufeatures.node
268351
268351
  var require_cpufeatures = __commonJS((exports, module) => {
268352
- module.exports = __require("./cpufeatures-1yrn0vtw.node");
268352
+ module.exports = __require("./cpufeatures-gknxfsk5.node");
268353
268353
  });
268354
268354
 
268355
268355
  // ../../node_modules/cpu-features/lib/index.js
@@ -268960,6 +268960,11 @@ var require_utils93 = __commonJS((exports, module) => {
268960
268960
  };
268961
268961
  });
268962
268962
 
268963
+ // ../../node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
268964
+ var require_sshcrypto = __commonJS((exports, module) => {
268965
+ module.exports = __require("./sshcrypto-a4xyg5e8.node");
268966
+ });
268967
+
268963
268968
  // ../../node_modules/ssh2/lib/protocol/crypto/poly1305.js
268964
268969
  var require_poly1305 = __commonJS((exports, module) => {
268965
268970
  var __dirname = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto", __filename = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto/poly1305.js";
@@ -269446,7 +269451,7 @@ var require_crypto = __commonJS((exports, module) => {
269446
269451
  var ChaChaPolyDecipher;
269447
269452
  var GenericDecipher;
269448
269453
  try {
269449
- binding = (()=>{throw new Error("Cannot require module "+"./crypto/build/Release/sshcrypto.node");})();
269454
+ binding = require_sshcrypto();
269450
269455
  ({
269451
269456
  AESGCMCipher,
269452
269457
  ChaChaPolyCipher,
@@ -283780,7 +283785,7 @@ function validateMotherDuckShape(connection) {
283780
283785
  throw new Error("MotherDuck access token is required.");
283781
283786
  }
283782
283787
  }
283783
- function validateDuckLakeShape(connection) {
283788
+ function validateDuckLakeShape(connection, origin2) {
283784
283789
  if (!connection.ducklakeConnection) {
283785
283790
  throw new Error("DuckLake connection configuration is missing.");
283786
283791
  }
@@ -283789,7 +283794,7 @@ function validateDuckLakeShape(connection) {
283789
283794
  }
283790
283795
  const storage = connection.ducklakeConnection.storage;
283791
283796
  if (storage?.s3Connection) {
283792
- validateS3ProviderShape(storage.s3Connection, connection.name);
283797
+ validateS3ProviderShape(storage.s3Connection, connection.name, origin2);
283793
283798
  }
283794
283799
  if (connection.ducklakeConnection.catalog.metadataSchema !== undefined) {
283795
283800
  const schema = connection.ducklakeConnection.catalog.metadataSchema;
@@ -283866,7 +283871,7 @@ function validatePublisherShape(connection) {
283866
283871
  throw new Error(`Invalid publisher connection '${connection.name}': connectionUri must use http or https (got '${parsedUri.protocol}'). ` + `Fix: set connectionUri to an absolute https URL like "https://…/connections/${connection.name}".`);
283867
283872
  }
283868
283873
  }
283869
- function validateTypeShape(connection) {
283874
+ function validateTypeShape(connection, origin2) {
283870
283875
  switch (connection.type) {
283871
283876
  case "sqlserver":
283872
283877
  return validateSqlServerShape(connection);
@@ -283877,7 +283882,7 @@ function validateTypeShape(connection) {
283877
283882
  case "motherduck":
283878
283883
  return validateMotherDuckShape(connection);
283879
283884
  case "ducklake":
283880
- return validateDuckLakeShape(connection);
283885
+ return validateDuckLakeShape(connection, origin2);
283881
283886
  case "trino":
283882
283887
  return validateTrinoShape(connection);
283883
283888
  case "databricks":
@@ -283888,7 +283893,7 @@ function validateTypeShape(connection) {
283888
283893
  return validatePublisherShape(connection);
283889
283894
  }
283890
283895
  }
283891
- function validateConnectionShape(connection) {
283896
+ function validateConnectionShape(connection, origin2 = "connection") {
283892
283897
  if (connection.proxy) {
283893
283898
  validateProxyShape(connection, connection.proxy);
283894
283899
  }
@@ -283901,7 +283906,7 @@ function validateConnectionShape(connection) {
283901
283906
  if (connection.postgresConnection) {
283902
283907
  postgresStatementTimeoutMs(connection.name ?? "", connection.postgresConnection);
283903
283908
  }
283904
- validateTypeShape(connection);
283909
+ validateTypeShape(connection, origin2);
283905
283910
  }
283906
283911
  function storageDestinationRoot(environmentPath) {
283907
283912
  return path4.join(environmentPath, STORAGE_DESTINATIONS_DIR);
@@ -283950,7 +283955,7 @@ function validateStorageDestinations(destinations = []) {
283950
283955
  continue;
283951
283956
  }
283952
283957
  try {
283953
- validateConnectionShape(destination);
283958
+ validateConnectionShape(destination, "storage-destination");
283954
283959
  const storage = destination.ducklakeConnection?.storage;
283955
283960
  if (storage?.s3Connection) {
283956
283961
  resolveCloudStorageCredentials({
@@ -284168,7 +284173,7 @@ function buildCoreEntry(connection, environmentPath) {
284168
284173
  throw new Error(`Unsupported connection type: ${connection.type}`);
284169
284174
  }
284170
284175
  }
284171
- function assembleEnvironmentConnections(connections = [], environmentPath = "") {
284176
+ function assembleEnvironmentConnections(connections = [], environmentPath = "", origin2 = "connection") {
284172
284177
  const pojo = { connections: {} };
284173
284178
  const metadata = new Map;
284174
284179
  const apiConnections = [];
@@ -284185,7 +284190,7 @@ function assembleEnvironmentConnections(connections = [], environmentPath = "")
284185
284190
  }
284186
284191
  processedConnections.add(connection.name);
284187
284192
  validateDuckdbApiSurface(connection);
284188
- validateConnectionShape(connection);
284193
+ validateConnectionShape(connection, origin2);
284189
284194
  warnOnConnectionQueryMetadata(connection);
284190
284195
  const apiConnection = cloneApiConnection(connection);
284191
284196
  apiConnection.attributes = getStaticConnectionAttributes(connection.type);
@@ -295166,17 +295171,19 @@ async function attachDuckLakeWithMode(connection, dbName, ducklakeConfig, option
295166
295171
  const hasS3 = !!ducklakeConfig.storage?.s3Connection;
295167
295172
  const hasGCS = !!ducklakeConfig.storage?.gcsConnection;
295168
295173
  if (hasS3) {
295169
- await attachCloudStorage(connection, {
295170
- name: `${dbName}_storage`,
295171
- type: "s3",
295172
- s3Connection: ducklakeConfig.storage.s3Connection
295173
- });
295174
- } else if (hasGCS) {
295175
- await attachCloudStorage(connection, {
295176
- name: `${dbName}_storage`,
295177
- type: "gcs",
295178
- gcsConnection: ducklakeConfig.storage.gcsConnection
295179
- });
295174
+ await connection.runSQL("SET GLOBAL enable_global_s3_configuration=false");
295175
+ }
295176
+ const storage = hasS3 ? {
295177
+ name: `${dbName}_storage`,
295178
+ type: "s3",
295179
+ s3Connection: ducklakeConfig.storage.s3Connection
295180
+ } : hasGCS ? {
295181
+ name: `${dbName}_storage`,
295182
+ type: "gcs",
295183
+ gcsConnection: ducklakeConfig.storage.gcsConnection
295184
+ } : undefined;
295185
+ if (storage) {
295186
+ resolveCloudStorageCredentials(storage, options.origin ?? "connection");
295180
295187
  }
295181
295188
  const pg = ducklakeConfig.catalog.postgresConnection;
295182
295189
  const pgConnString = buildPgConnectionString(pg, {
@@ -295189,8 +295196,8 @@ async function attachDuckLakeWithMode(connection, dbName, ducklakeConfig, option
295189
295196
  logger.debug(`Final escaped connection string: ${redactPgSecrets(escapedPgConnString)}`);
295190
295197
  const dataPath = ducklakeConfig.storage?.bucketUrl;
295191
295198
  const metadataSchema = ducklakeConfig.catalog.metadataSchema;
295192
- const presence = await preflightDuckLakeCatalogFormat(connection, dbName, pgConnString, metadataSchema);
295193
- if (presence === "absent") {
295199
+ const preflight = await preflightDuckLakeCatalogFormat(connection, dbName, pgConnString, metadataSchema);
295200
+ if (preflight === "absent") {
295194
295201
  if (options.skipIfAbsent) {
295195
295202
  return false;
295196
295203
  }
@@ -295215,14 +295222,44 @@ async function attachDuckLakeWithMode(connection, dbName, ducklakeConfig, option
295215
295222
  throw error;
295216
295223
  }
295217
295224
  }
295225
+ if (storage) {
295226
+ try {
295227
+ const scope = await attachedDuckLakeScope(connection, dbName, {
295228
+ configuredDataPath: dataPath,
295229
+ metadataSchema,
295230
+ readOnly: options.readOnly,
295231
+ managed: options.managed === true && options.origin !== "storage-destination"
295232
+ });
295233
+ if (!scope) {
295234
+ throw new Error(`No storage secret could be scoped for DuckLake catalog '${dbName}': ` + `the config states no data path, and the catalog records no data ` + `path (or, for a managed view, no path for its main schema) to ` + `scope it to. The catalog was not attached.`);
295235
+ }
295236
+ await attachCloudStorage(connection, storage, options.origin, scope);
295237
+ } catch (error) {
295238
+ await connection.runSQL(`DETACH ${quoteIdentifier(dbName, "duckdb")};`).catch(() => {});
295239
+ throw error;
295240
+ }
295241
+ }
295218
295242
  return true;
295219
295243
  }
295220
- async function recordedCatalogSettings(connection, alias, metadataSchema) {
295221
- const metadataCatalog = [
295244
+ async function attachedDuckLakeScope(connection, dbName, input) {
295245
+ const recordedDataPath = input.configuredDataPath ? undefined : (await recordedCatalogSettings(connection, dbName, input.metadataSchema)).data_path;
295246
+ const mainSchemaPath = input.managed && input.readOnly ? await recordedSchemaPath(connection, dbName, input.metadataSchema, "main") : undefined;
295247
+ return storageSecretScope({
295248
+ configuredDataPath: input.configuredDataPath,
295249
+ recordedDataPath,
295250
+ mainSchemaPath,
295251
+ readOnly: input.readOnly,
295252
+ managed: input.managed
295253
+ });
295254
+ }
295255
+ function metadataCatalogRef(alias, metadataSchema) {
295256
+ return [
295222
295257
  `__ducklake_metadata_${alias}`,
295223
295258
  ...metadataSchema ? [metadataSchema] : []
295224
295259
  ].map((part) => quoteIdentifier(part, "duckdb")).join(".");
295225
- const rows = runSQLRows(await connection.runSQL(`SELECT key, value FROM ${metadataCatalog}.ducklake_metadata WHERE scope IS NULL`));
295260
+ }
295261
+ async function recordedCatalogSettings(connection, alias, metadataSchema) {
295262
+ const rows = runSQLRows(await connection.runSQL(`SELECT key, value FROM ${metadataCatalogRef(alias, metadataSchema)}.ducklake_metadata WHERE scope IS NULL`));
295226
295263
  const settings = {};
295227
295264
  for (const row of rows) {
295228
295265
  if (typeof row.key === "string" && row.value != null) {
@@ -295231,6 +295268,43 @@ async function recordedCatalogSettings(connection, alias, metadataSchema) {
295231
295268
  }
295232
295269
  return settings;
295233
295270
  }
295271
+ async function recordedSchemaPath(connection, alias, metadataSchema, schemaName) {
295272
+ const row = runSQLRows(await connection.runSQL(`SELECT path, path_is_relative FROM ${metadataCatalogRef(alias, metadataSchema)}.ducklake_schema ` + `WHERE schema_name = '${escapeSQL(schemaName)}' AND end_snapshot IS NULL LIMIT 1`))[0];
295273
+ if (!row || typeof row.path !== "string")
295274
+ return;
295275
+ return { path: row.path, relative: row.path_is_relative !== false };
295276
+ }
295277
+ function stripTrailingSlashes(url3) {
295278
+ let end = url3.length;
295279
+ while (end > 0 && url3[end - 1] === "/")
295280
+ end--;
295281
+ return url3.slice(0, end);
295282
+ }
295283
+ function oneTrailingSlash(url3) {
295284
+ return `${stripTrailingSlashes(url3)}/`;
295285
+ }
295286
+ function isBucketlessUrl(url3) {
295287
+ const at = url3.indexOf("://");
295288
+ return at >= 0 && stripTrailingSlashes(url3.slice(at + 3)) === "";
295289
+ }
295290
+ function storageSecretScope(input) {
295291
+ const root = input.configuredDataPath || input.recordedDataPath;
295292
+ if (!root || isBucketlessUrl(root))
295293
+ return;
295294
+ if (input.managed && input.readOnly) {
295295
+ const main = input.mainSchemaPath;
295296
+ if (!main)
295297
+ return;
295298
+ if (!main.relative) {
295299
+ return isBucketlessUrl(main.path) ? undefined : oneTrailingSlash(main.path);
295300
+ }
295301
+ let start = 0;
295302
+ while (start < main.path.length && main.path[start] === "/")
295303
+ start++;
295304
+ return oneTrailingSlash(`${oneTrailingSlash(root)}${main.path.slice(start)}`);
295305
+ }
295306
+ return oneTrailingSlash(root);
295307
+ }
295234
295308
  function dataPathClauses(dataPath, readOnly) {
295235
295309
  if (dataPath) {
295236
295310
  return `DATA_PATH '${escapeSQL(dataPath)}', OVERRIDE_DATA_PATH true`;
@@ -295250,13 +295324,16 @@ function firstAttachClauses(options) {
295250
295324
  async function attachDuckLakeForCreate(connection, dbName, ducklakeConfig, settings) {
295251
295325
  await attachDuckLakeWithMode(connection, dbName, ducklakeConfig, {
295252
295326
  readOnly: false,
295327
+ origin: "storage-destination",
295253
295328
  encrypted: settings.encrypted,
295254
295329
  dataInliningRowLimit: settings.dataInliningRowLimit
295255
295330
  });
295256
295331
  }
295257
- async function attachDuckLake(connection, dbName, ducklakeConfig) {
295332
+ async function attachDuckLake(connection, dbName, ducklakeConfig, options = {}) {
295258
295333
  await attachDuckLakeWithMode(connection, dbName, ducklakeConfig, {
295259
- readOnly: ducklakeConfig.readOnly !== false
295334
+ readOnly: ducklakeConfig.readOnly !== false,
295335
+ origin: options.origin,
295336
+ managed: options.managed
295260
295337
  });
295261
295338
  }
295262
295339
  function managedSearchPathSql(dbName) {
@@ -295267,17 +295344,21 @@ async function setManagedSearchPath(connection, dbName) {
295267
295344
  await connection.runSQL(managedSearchPathSql(dbName));
295268
295345
  }
295269
295346
  async function attachDuckLakeReadOnly(connection, dbName, ducklakeConfig) {
295270
- await attachDuckLake(connection, dbName, ducklakeConfig);
295347
+ await attachDuckLake(connection, dbName, ducklakeConfig, {
295348
+ origin: "storage-destination"
295349
+ });
295271
295350
  }
295272
295351
  async function attachDuckLakeReadOnlyIfPresent(connection, dbName, ducklakeConfig) {
295273
295352
  return attachDuckLakeWithMode(connection, dbName, ducklakeConfig, {
295274
295353
  readOnly: true,
295275
- skipIfAbsent: true
295354
+ skipIfAbsent: true,
295355
+ origin: "storage-destination"
295276
295356
  });
295277
295357
  }
295278
295358
  async function attachDuckLakeReadWrite(connection, dbName, ducklakeConfig) {
295279
295359
  await attachDuckLakeWithMode(connection, dbName, ducklakeConfig, {
295280
- readOnly: false
295360
+ readOnly: false,
295361
+ origin: "storage-destination"
295281
295362
  });
295282
295363
  }
295283
295364
  async function federateSourceForPassthrough(connection, sourceType, config, deps = {}) {
@@ -295481,13 +295562,15 @@ function buildCloudStorageSecretSQL(secretName, credentials) {
295481
295562
  const isGCS = credentials.type === "gcs";
295482
295563
  const escapedKeyId = escapeSQL(credentials.accessKeyId);
295483
295564
  const escapedSecret = escapeSQL(credentials.secretAccessKey);
295565
+ const scopeClause = credentials.scope ? `SCOPE '${escapeSQL(credentials.scope)}'` : "";
295484
295566
  let createSecretCommand;
295485
295567
  if (isGCS) {
295486
295568
  createSecretCommand = `
295487
295569
  CREATE OR REPLACE SECRET ${secretName} (
295488
295570
  TYPE gcs,
295489
295571
  KEY_ID '${escapedKeyId}',
295490
- SECRET '${escapedSecret}'
295572
+ SECRET '${escapedSecret}'${scopeClause ? `,
295573
+ ${scopeClause}` : ""}
295491
295574
  );
295492
295575
  `;
295493
295576
  } else {
@@ -295523,6 +295606,9 @@ function buildCloudStorageSecretSQL(secretName, credentials) {
295523
295606
  if (credentials.provider !== "credential_chain" && credentials.sessionToken) {
295524
295607
  clauses.push(`SESSION_TOKEN '${escapeSQL(credentials.sessionToken)}'`);
295525
295608
  }
295609
+ if (scopeClause) {
295610
+ clauses.push(scopeClause);
295611
+ }
295526
295612
  createSecretCommand = `
295527
295613
  CREATE OR REPLACE SECRET ${secretName} (
295528
295614
  ${clauses.join(`,
@@ -295532,9 +295618,12 @@ function buildCloudStorageSecretSQL(secretName, credentials) {
295532
295618
  }
295533
295619
  return createSecretCommand;
295534
295620
  }
295535
- async function attachCloudStorage(connection, attachedDb) {
295621
+ async function attachCloudStorage(connection, attachedDb, origin2 = "connection", scope) {
295536
295622
  const storageType = attachedDb.type?.toUpperCase() || "";
295537
- const credentials = resolveCloudStorageCredentials(attachedDb);
295623
+ const credentials = {
295624
+ ...resolveCloudStorageCredentials(attachedDb, origin2),
295625
+ scope
295626
+ };
295538
295627
  await installAndLoadExtension(connection, "httpfs");
295539
295628
  if (credentials.provider === "credential_chain") {
295540
295629
  await installAndLoadExtension(connection, "aws");
@@ -295826,8 +295915,8 @@ function applyConnectionFingerprint(connection, metadata) {
295826
295915
  function isDuckDBConnection(connection) {
295827
295916
  return connection instanceof DuckDBConnection;
295828
295917
  }
295829
- function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", isUpdateConnectionRequest = false) {
295830
- const assembled = assembleEnvironmentConnections(connections, environmentPath);
295918
+ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", isUpdateConnectionRequest = false, { origin: origin2 = "connection" } = {}) {
295919
+ const assembled = assembleEnvironmentConnections(connections, environmentPath, origin2);
295831
295920
  const duckLakeCache = new Map;
295832
295921
  const snowflakeJwtCache = new Map;
295833
295922
  const azureDuckDBCache = new Map;
@@ -295866,7 +295955,10 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
295866
295955
  }
295867
295956
  const connection2 = await connectionPromise;
295868
295957
  if (isUpdateConnectionRequest || !await isDatabaseAttached(connection2, name)) {
295869
- await attachDuckLake(connection2, name, metadata.apiConnection.ducklakeConnection);
295958
+ await attachDuckLake(connection2, name, metadata.apiConnection.ducklakeConnection, {
295959
+ origin: origin2,
295960
+ managed: metadata.apiConnection.managed === true
295961
+ });
295870
295962
  if (metadata.apiConnection.managed === true) {
295871
295963
  await setManagedSearchPath(connection2, name);
295872
295964
  }
@@ -348115,7 +348207,7 @@ async function compileSynthesizedPreaggregation(args) {
348115
348207
  const { runtime, importBaseURL } = await args.getRuntime(new Map([[synthesizedURL.href, text]]));
348116
348208
  const materializer = runtime.loadModel(synthesizedURL, { importBaseURL });
348117
348209
  const model = await materializer.getModel();
348118
- return { plans, model, materializer, text };
348210
+ return { plans, model, materializer, runtime, text };
348119
348211
  }
348120
348212
  async function tryCompileSynthesizedPreaggregation(args) {
348121
348213
  try {
@@ -348302,27 +348394,47 @@ function composeDeclaredQueryMetadata(layers) {
348302
348394
  const resolved = Object.assign({}, ...ordered);
348303
348395
  return Object.keys(resolved).length > 0 ? resolved : null;
348304
348396
  }
348305
- function flattenDependsOn(node) {
348306
- return node.dependsOn.map((d) => d.sourceID);
348307
- }
348308
- function* iterGraphSources(graph, sources) {
348309
- const seen = new Set;
348310
- function* visit(node) {
348311
- if (seen.has(node.sourceID))
348312
- return;
348313
- for (const dep of node.dependsOn) {
348314
- yield* visit(dep);
348397
+ function sourceBuildOf(build, unbuildable) {
348398
+ const order = [];
348399
+ const dependsOn = {};
348400
+ for (const target of build.targets) {
348401
+ const reads = target.dependsOn.flatMap((t2) => t2.sources.map((s2) => s2.sourceID));
348402
+ for (const source of target.sources) {
348403
+ order.push(source.sourceID);
348404
+ dependsOn[source.sourceID] = reads;
348315
348405
  }
348316
- seen.add(node.sourceID);
348317
- const source = sources[node.sourceID];
348406
+ }
348407
+ for (const { source } of unbuildable) {
348408
+ if (source.connectionName !== build.connectionName)
348409
+ continue;
348410
+ order.push(source.sourceID);
348411
+ dependsOn[source.sourceID] = [];
348412
+ }
348413
+ return { connectionName: build.connectionName, order, dependsOn };
348414
+ }
348415
+ function sourceBuildsOf(targets) {
348416
+ const builds = targets.connections.map((c) => sourceBuildOf(c, targets.unbuildable));
348417
+ const planned = new Set(builds.map((b) => b.connectionName));
348418
+ for (const { source } of targets.unbuildable) {
348419
+ if (planned.has(source.connectionName))
348420
+ continue;
348421
+ planned.add(source.connectionName);
348422
+ builds.push(sourceBuildOf({ connectionName: source.connectionName, targets: [] }, targets.unbuildable));
348423
+ }
348424
+ return builds;
348425
+ }
348426
+ function plannedSources(targets) {
348427
+ return [
348428
+ ...targets.connections.flatMap((c) => c.targets.flatMap((t2) => t2.sources)),
348429
+ ...targets.unbuildable.map((u) => u.source)
348430
+ ];
348431
+ }
348432
+ function* sourcesInBuildOrder(build, sources) {
348433
+ for (const sourceID of build.order) {
348434
+ const source = sources[sourceID];
348318
348435
  if (source)
348319
348436
  yield source;
348320
348437
  }
348321
- for (const level of graph.nodes) {
348322
- for (const node of level) {
348323
- yield* visit(node);
348324
- }
348325
- }
348326
348438
  }
348327
348439
  function computeSourceEntityId(source, connectionDigests) {
348328
348440
  return source.makeBuildId(connectionDigests[source.connectionName], addressPartitionLayout(source, source.getSQL()));
@@ -348411,7 +348523,7 @@ async function classifyPersistSourceGate(persistSource, modelDef, materializer,
348411
348523
  }
348412
348524
  function emptyBuildPlanParts() {
348413
348525
  return {
348414
- graphs: [],
348526
+ builds: [],
348415
348527
  sources: {},
348416
348528
  sourceModelPaths: {},
348417
348529
  droppedPersistSources: [],
@@ -348420,7 +348532,7 @@ function emptyBuildPlanParts() {
348420
348532
  };
348421
348533
  }
348422
348534
  function mergeBuildPlanParts(into, from) {
348423
- into.graphs.push(...from.graphs);
348535
+ into.builds.push(...from.builds);
348424
348536
  Object.assign(into.sources, from.sources);
348425
348537
  Object.assign(into.sourceModelPaths, from.sourceModelPaths);
348426
348538
  into.droppedPersistSources.push(...from.droppedPersistSources);
@@ -348428,7 +348540,14 @@ function mergeBuildPlanParts(into, from) {
348428
348540
  Object.assign(into.sourceGateOutcomes, from.sourceGateOutcomes);
348429
348541
  }
348430
348542
  async function collectModelBuildPlan(parts, args) {
348431
- const { modelPath, packagePath, materializer, malloyModel, getRuntime } = args;
348543
+ const {
348544
+ modelPath,
348545
+ packagePath,
348546
+ materializer,
348547
+ malloyModel,
348548
+ runtime,
348549
+ getRuntime
348550
+ } = args;
348432
348551
  const synthesized = args.synthesized !== undefined ? args.synthesized : await tryCompileSynthesizedPreaggregation({
348433
348552
  packagePath,
348434
348553
  modelPath,
@@ -348436,14 +348555,14 @@ async function collectModelBuildPlan(parts, args) {
348436
348555
  contents: malloyModel._modelDef?.contents ?? {}
348437
348556
  });
348438
348557
  if (synthesized) {
348439
- const rollupPlan = synthesized.model.getBuildPlan();
348440
- const rollupNames = new Set(synthesized.plans.map((p) => p.rollupSourceName));
348441
- parts.graphs.push(...rollupPlan.graphs);
348558
+ const rollupTargets = await synthesized.runtime.getBuildTargets(synthesized.model);
348559
+ parts.builds.push(...sourceBuildsOf(rollupTargets));
348442
348560
  const planByName = new Map(synthesized.plans.map((p) => [p.rollupSourceName, p]));
348443
348561
  let rollupClassificationCtx;
348444
- for (const [sourceID, source] of Object.entries(rollupPlan.sources)) {
348445
- if (!rollupNames.has(source.name))
348562
+ for (const source of plannedSources(rollupTargets)) {
348563
+ if (!planByName.has(source.name))
348446
348564
  continue;
348565
+ const sourceID = source.sourceID;
348447
348566
  parts.sources[sourceID] = source;
348448
348567
  parts.sourceModelPaths[sourceID] = modelPath;
348449
348568
  const rollup = planByName.get(source.name);
@@ -348462,23 +348581,25 @@ async function collectModelBuildPlan(parts, args) {
348462
348581
  const modelTag = malloyModel.modelAnnotations.parseAsTag("!").tag;
348463
348582
  if (!modelTag.has("experimental", "persistence"))
348464
348583
  return;
348465
- const buildPlan = malloyModel.getBuildPlan();
348466
- for (const msg of buildPlan.tagParseLog) {
348584
+ const targets = await runtime.getBuildTargets(malloyModel);
348585
+ for (const msg of targets.tagParseLog) {
348467
348586
  logger.warn("Persist annotation issue", {
348468
348587
  modelPath,
348469
348588
  message: msg.message,
348470
348589
  severity: msg.severity
348471
348590
  });
348472
348591
  }
348473
- const recognizedNames = new Set(Object.values(buildPlan.sources).map((s2) => s2.name));
348592
+ const planned = plannedSources(targets);
348593
+ const recognizedNames = new Set(planned.map((s2) => s2.name));
348474
348594
  for (const name of detectDroppedPersistSources(malloyModel, recognizedNames)) {
348475
348595
  parts.droppedPersistSources.push({ name, modelPath });
348476
348596
  }
348477
- if (buildPlan.graphs.length === 0)
348597
+ if (planned.length === 0)
348478
348598
  return;
348479
- parts.graphs.push(...buildPlan.graphs);
348599
+ parts.builds.push(...sourceBuildsOf(targets));
348480
348600
  let classificationCtx;
348481
- for (const [sourceID, source] of Object.entries(buildPlan.sources)) {
348601
+ for (const source of planned) {
348602
+ const sourceID = source.sourceID;
348482
348603
  parts.sources[sourceID] = source;
348483
348604
  parts.sourceModelPaths[sourceID] = modelPath;
348484
348605
  if (!classificationCtx) {
@@ -348491,28 +348612,30 @@ async function collectModelBuildPlan(parts, args) {
348491
348612
  parts.sourceGateOutcomes[sourceID] = await classifyPersistSourceGate(source, classificationCtx.modelDef, materializer, classificationCtx.deps, modelPath);
348492
348613
  }
348493
348614
  }
348494
- async function resolveConnectionDigests(connections, graphs, onSkipped) {
348615
+ async function resolveConnectionDigests(connections, builds, onSkipped) {
348495
348616
  const connectionDigests = {};
348496
- for (const graph of graphs) {
348497
- const conn = connections.get(graph.connectionName);
348617
+ for (const build of builds) {
348618
+ const conn = connections.get(build.connectionName);
348498
348619
  if (!conn) {
348499
- onSkipped(graph.connectionName);
348620
+ onSkipped(build.connectionName);
348500
348621
  continue;
348501
348622
  }
348502
- if (!connectionDigests[graph.connectionName]) {
348503
- connectionDigests[graph.connectionName] = await conn.getDigest();
348623
+ if (!connectionDigests[build.connectionName]) {
348624
+ connectionDigests[build.connectionName] = await conn.getDigest();
348504
348625
  }
348505
348626
  }
348506
348627
  return connectionDigests;
348507
348628
  }
348508
- function deriveBuildPlan(graphs, sources, connectionDigests, sourceNames, sourceModelPaths, packageMaterialization, options) {
348629
+ function deriveBuildPlan(builds, sources, connectionDigests, sourceNames, sourceModelPaths, packageMaterialization, options) {
348509
348630
  const include = sourceNames ? new Set(sourceNames) : null;
348510
- const wireGraphs = graphs.map((graph) => ({
348511
- connectionName: graph.connectionName,
348512
- nodes: graph.nodes.map((level) => level.map((node) => ({
348513
- sourceID: node.sourceID,
348514
- dependsOn: flattenDependsOn(node)
348515
- })))
348631
+ const wireGraphs = builds.map((build) => ({
348632
+ connectionName: build.connectionName,
348633
+ nodes: [
348634
+ build.order.map((sourceID) => ({
348635
+ sourceID,
348636
+ dependsOn: build.dependsOn[sourceID] ?? []
348637
+ }))
348638
+ ]
348516
348639
  }));
348517
348640
  const wireSources = {};
348518
348641
  const refusedSources = {};
@@ -348601,7 +348724,7 @@ function deriveBuildPlan(graphs, sources, connectionDigests, sourceNames, source
348601
348724
  function deriveBuildPlanOutcome(compiled, materializationConfig) {
348602
348725
  const droppedPersistSources = compiled.droppedPersistSources ?? [];
348603
348726
  const incrementalDeclarations = collectIncrementalDeclarations(compiled.sources);
348604
- const plan = compiled.graphs.length === 0 ? null : deriveBuildPlan(compiled.graphs, compiled.sources, compiled.connectionDigests, undefined, compiled.sourceModelPaths, materializationConfig, {
348727
+ const plan = compiled.builds.length === 0 ? null : deriveBuildPlan(compiled.builds, compiled.sources, compiled.connectionDigests, undefined, compiled.sourceModelPaths, materializationConfig, {
348605
348728
  preaggregatePlans: compiled.preaggregatePlans,
348606
348729
  sourceGateOutcomes: compiled.sourceGateOutcomes
348607
348730
  });
@@ -350136,14 +350259,14 @@ async function buildDownstreamIntoStorage(params) {
350136
350259
  message: `Chained build model did not compile over the rebound parents: ${errMessage(err)}`
350137
350260
  });
350138
350261
  }
350139
- const plan = model.getBuildPlan();
350140
- let downstream;
350141
- for (const ps of Object.values(plan.sources)) {
350142
- if (ps.name === downstreamName) {
350143
- downstream = ps;
350144
- break;
350145
- }
350262
+ const targets = await runtime.getBuildTargets(model, { virtualMap });
350263
+ const unbuildable = targets.unbuildable.find((u) => u.source.name === downstreamName);
350264
+ if (unbuildable) {
350265
+ throw new MaterializationEligibilityError({
350266
+ message: `Chained build model could not plan '${downstreamName}' over ` + `the rebound parents: ${unbuildable.problem.message}`
350267
+ });
350146
350268
  }
350269
+ const downstream = plannedSources(targets).find((ps) => ps.name === downstreamName);
350147
350270
  if (!downstream) {
350148
350271
  throw new MaterializationEligibilityError({
350149
350272
  message: `Chained build model did not yield a persist source named ` + `'${downstreamName}' (the downstream references something the ` + `rebound parents don't provide).`
@@ -359860,6 +359983,98 @@ var require_recursive_readdir = __commonJS((exports, module) => {
359860
359983
  module.exports = readdir3;
359861
359984
  });
359862
359985
 
359986
+ // src/service/preaggregation_coverage.ts
359987
+ function rollupMeasuresByMember(plans) {
359988
+ return new Map(plans.map((p2) => [
359989
+ p2.rollupSourceName,
359990
+ new Set(p2.measures.map((m3) => m3.name))
359991
+ ]));
359992
+ }
359993
+ function rollupCoverageViolation(query, rollupMeasures) {
359994
+ const ir2 = query;
359995
+ const member = ir2?.compositeResolvedSourceDef?.as;
359996
+ if (!member)
359997
+ return;
359998
+ const merged = rollupMeasures.get(member);
359999
+ if (!merged)
360000
+ return;
360001
+ const memberFields = new Map((ir2?.compositeResolvedSourceDef?.fields ?? []).map((f5) => [
360002
+ f5.as ?? f5.name,
360003
+ f5
360004
+ ]));
360005
+ const followed = new Set;
360006
+ let violation;
360007
+ const visitRef = (path16) => {
360008
+ if (!path16)
360009
+ return;
360010
+ if (path16.length !== 1) {
360011
+ violation = `reference ${path16.join(".")} reaches past the rollup's own fields`;
360012
+ return;
360013
+ }
360014
+ const name = path16[0];
360015
+ if (merged.has(name) || followed.has(name))
360016
+ return;
360017
+ followed.add(name);
360018
+ visitExpr(memberFields.get(name)?.e);
360019
+ };
360020
+ const visitExpr = (e2) => {
360021
+ if (violation || !e2 || typeof e2 !== "object")
360022
+ return;
360023
+ if (Array.isArray(e2)) {
360024
+ for (const item of e2)
360025
+ visitExpr(item);
360026
+ return;
360027
+ }
360028
+ const n = e2;
360029
+ if (n.node === "aggregate") {
360030
+ violation = `aggregate ${String(n.function)}() is computed over the rollup's rows, not from a stored partial`;
360031
+ return;
360032
+ }
360033
+ if (n.node === "function_call" && n.overload?.returnType?.expressionType === "aggregate") {
360034
+ violation = `aggregate function ${String(n.name)}() is computed over the rollup's rows, not from a stored partial`;
360035
+ return;
360036
+ }
360037
+ if (n.node === "field") {
360038
+ visitRef(n.path);
360039
+ return;
360040
+ }
360041
+ for (const [key, value] of Object.entries(n)) {
360042
+ if (key === "overload" || key === "at")
360043
+ continue;
360044
+ visitExpr(value);
360045
+ }
360046
+ };
360047
+ const visitStage = (stage) => {
360048
+ if (violation)
360049
+ return;
360050
+ if (stage?.type === "project" || stage?.type === "index") {
360051
+ violation = `${stage.type === "project" ? "a select:" : "an index:"} stage over the rollup returns one row per grain group, not the source's rows`;
360052
+ return;
360053
+ }
360054
+ for (const field of stage?.queryFields ?? []) {
360055
+ if (field.type === "fieldref")
360056
+ visitRef(field.path);
360057
+ else if (field.type === "turtle")
360058
+ visitStage(field.pipeline?.[0]);
360059
+ else
360060
+ visitExpr(field.e);
360061
+ }
360062
+ for (const filter2 of stage?.filterList ?? [])
360063
+ visitExpr(filter2.e);
360064
+ };
360065
+ visitStage(ir2?.pipeline?.[0]);
360066
+ return violation ? `${member}: ${violation}` : undefined;
360067
+ }
360068
+ var RollupCoverageError;
360069
+ var init_preaggregation_coverage = __esm(() => {
360070
+ RollupCoverageError = class RollupCoverageError extends Error {
360071
+ constructor(message) {
360072
+ super(message);
360073
+ this.name = "RollupCoverageError";
360074
+ }
360075
+ };
360076
+ });
360077
+
359863
360078
  // src/service/render_tag_targets.ts
359864
360079
  function renderTagTargets(queries, sources) {
359865
360080
  const targets = [];
@@ -363064,6 +363279,11 @@ class StorageServeShape {
363064
363279
  await this.host.ensureSharedQueryModel(this.serveShapeCache.materializer);
363065
363280
  const runnable = this.serveShapeCache.materializer.loadRestrictedQuery(queryString);
363066
363281
  const probeSQL = await runnable.getSQL({ virtualMap, givens });
363282
+ if (groups.length > 0) {
363283
+ const uncovered = rollupCoverageViolation((await runnable.getPreparedQuery())._query, rollupMeasuresByMember(this.host.rollupPlans()));
363284
+ if (uncovered)
363285
+ throw new RollupCoverageError(uncovered);
363286
+ }
363067
363287
  const rollupPaths = [
363068
363288
  ...buildVirtualMap(groups.flatMap((g2) => g2.members))
363069
363289
  ].flatMap(([, byHandle]) => [...byHandle.values()]);
@@ -363264,6 +363484,7 @@ var init_model_serve_shape = __esm(() => {
363264
363484
  init_materialization_metrics();
363265
363485
  init_connection();
363266
363486
  init_materialization_serve_transform();
363487
+ init_preaggregation_coverage();
363267
363488
  init_preaggregation_serve_bindings();
363268
363489
  staleBindingsWithheld = lazyCounter("publisher_storage_bindings_withheld_total", "Storage serve bindings withheld from a routed query because the freshness gate found them stale; each one is read live instead. Until now staleness was visible only as a later live read.");
363269
363490
  });
@@ -363306,6 +363527,7 @@ var init_model = __esm(() => {
363306
363527
  init_config();
363307
363528
  init_preaggregation_synthesis();
363308
363529
  init_preaggregation_compile();
363530
+ init_preaggregation_coverage();
363309
363531
  init_render_tag_targets();
363310
363532
  init_model_info();
363311
363533
  init_logger();
@@ -365487,13 +365709,17 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
365487
365709
  });
365488
365710
  } catch (shapeErr) {
365489
365711
  recordStorageServeRouting("live_fallback");
365490
- const { shapeSources, staleSources } = serveShapeDiagnostics(this.storageServe.serveBindings, this.storageServe.freshServeBindings(Date.now()));
365491
- logger.info("storage serve-shape ineligible for this query; serving live", {
365492
- modelPath: this.modelPath,
365493
- error: shapeErr instanceof Error ? shapeErr.message : String(shapeErr),
365494
- shapeSources,
365495
- staleSources
365496
- });
365712
+ if (shapeErr instanceof RollupCoverageError) {
365713
+ logger.debug("storage rollup cannot answer this query; serving live", { modelPath: this.modelPath, error: shapeErr.message });
365714
+ } else {
365715
+ const { shapeSources, staleSources } = serveShapeDiagnostics(this.storageServe.serveBindings, this.storageServe.freshServeBindings(Date.now()));
365716
+ logger.info("storage serve-shape ineligible for this query; serving live", {
365717
+ modelPath: this.modelPath,
365718
+ error: shapeErr instanceof Error ? shapeErr.message : String(shapeErr),
365719
+ shapeSources,
365720
+ staleSources
365721
+ });
365722
+ }
365497
365723
  }
365498
365724
  }
365499
365725
  const everyRollupIsStorageBound = this.preaggregateRollupPlans.length > 0 && this.preaggregateRollupPlans.every((plan) => !!plan.storage);
@@ -365504,10 +365730,13 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
365504
365730
  givens: querySurfaceGivens,
365505
365731
  buildManifest
365506
365732
  });
365733
+ const uncovered = rollupCoverageViolation((await candidate.getPreparedQuery())._query, rollupMeasuresByMember(this.preaggregateRollupPlans));
365734
+ if (uncovered)
365735
+ throw new RollupCoverageError(uncovered);
365507
365736
  runnable = candidate;
365508
365737
  preaggRouted = true;
365509
365738
  } catch (preaggErr) {
365510
- logger.debug("query does not compile against the pre-aggregation model; serving live", {
365739
+ logger.debug("query cannot be served from the pre-aggregation model; serving live", {
365511
365740
  modelPath: this.modelPath,
365512
365741
  error: preaggErr instanceof Error ? preaggErr.message : String(preaggErr)
365513
365742
  });
@@ -366140,18 +366369,19 @@ async function compilePackageBuildPlan(pkg, signal) {
366140
366369
  packagePath: pkg.getPackagePath(),
366141
366370
  materializer,
366142
366371
  malloyModel,
366372
+ runtime,
366143
366373
  getRuntime: (overlay) => Model.getModelRuntime(pkg.getPackagePath(), modelPath, pkg.getMalloyConfig(), { overlay })
366144
366374
  });
366145
366375
  }
366146
- const connections = await resolvePackageConnections(pkg, parts.graphs.map((g2) => g2.connectionName));
366147
- const connectionDigests = await resolveConnectionDigests(connections, parts.graphs, (connectionName) => {
366376
+ const connections = await resolvePackageConnections(pkg, parts.builds.map((b3) => b3.connectionName));
366377
+ const connectionDigests = await resolveConnectionDigests(connections, parts.builds, (connectionName) => {
366148
366378
  recordConnectionDigestSkipped();
366149
366379
  logger.warn("Skipping connection digest; connection did not resolve", {
366150
366380
  connectionName
366151
366381
  });
366152
366382
  });
366153
366383
  return {
366154
- graphs: parts.graphs,
366384
+ builds: parts.builds,
366155
366385
  sources: parts.sources,
366156
366386
  connectionDigests,
366157
366387
  connections,
@@ -370262,10 +370492,10 @@ class Environment {
370262
370492
  if (this.destinations.length > 0) {
370263
370493
  fs17.mkdirSync(destinationRoot, { recursive: true });
370264
370494
  }
370265
- this.destinationMalloyConfig = buildEnvironmentMalloyConfig(this.destinations, destinationRoot);
370495
+ this.destinationMalloyConfig = buildEnvironmentMalloyConfig(this.destinations, destinationRoot, false, { origin: "storage-destination" });
370266
370496
  } catch (error) {
370267
370497
  logger.error(`Failed to assemble storage destinations for environment ${this.environmentName}; serving without them`, { error });
370268
- this.destinationMalloyConfig = buildEnvironmentMalloyConfig([], storageDestinationRoot(this.environmentPath));
370498
+ this.destinationMalloyConfig = buildEnvironmentMalloyConfig([], storageDestinationRoot(this.environmentPath), false, { origin: "storage-destination" });
370269
370499
  }
370270
370500
  if (previous && previous !== this.destinationMalloyConfig) {
370271
370501
  this.retireConnectionGeneration(`environment ${this.environmentName} destinations`, () => previous.releaseConnections());
@@ -374591,11 +374821,11 @@ function resolveInstruction(index, persistSource, connectionDigests) {
374591
374821
  return;
374592
374822
  return { instruction, orchestrated: false, sourceEntityId };
374593
374823
  }
374594
- function assertNoTableCollisions(graphs, sources, connectionDigests, index) {
374824
+ function assertNoTableCollisions(builds, sources, connectionDigests, index) {
374595
374825
  const claimedBy = new Map;
374596
374826
  const collisions = [];
374597
- for (const graph of graphs) {
374598
- for (const persistSource of iterGraphSources(graph, sources)) {
374827
+ for (const build of builds) {
374828
+ for (const persistSource of sourcesInBuildOrder(build, sources)) {
374599
374829
  let address;
374600
374830
  try {
374601
374831
  address = computeSourceEntityId(persistSource, connectionDigests);
@@ -374605,7 +374835,7 @@ function assertNoTableCollisions(graphs, sources, connectionDigests, index) {
374605
374835
  const instruction = index.bySourceID.get(persistSource.sourceID) ?? index.bySourceEntityId.get(address);
374606
374836
  if (!instruction)
374607
374837
  continue;
374608
- const target = physicalTargetKey(instruction, graph.connectionName);
374838
+ const target = physicalTargetKey(instruction, build.connectionName);
374609
374839
  const claim = claimedBy.get(target);
374610
374840
  if (!claim) {
374611
374841
  claimedBy.set(target, {
@@ -374896,8 +375126,8 @@ class MaterializationService {
374896
375126
  const refusals = [];
374897
375127
  const cleared = new Map;
374898
375128
  const seen = new Set;
374899
- for (const graph of compiled.graphs) {
374900
- for (const persistSource of iterGraphSources(graph, compiled.sources)) {
375129
+ for (const build of compiled.builds) {
375130
+ for (const persistSource of sourcesInBuildOrder(build, compiled.sources)) {
374901
375131
  if (include && !include.has(persistSource.name))
374902
375132
  continue;
374903
375133
  if (getPersistStorageMode() === "off" && declaredStorage(persistSource)) {
@@ -375153,7 +375383,7 @@ class MaterializationService {
375153
375383
  return incrementalRunContext(compiled, run, this.repository);
375154
375384
  }
375155
375385
  async executeInstructedBuild(compiled, environment, instructions, seedEntries, signal, strict = false, owner, buildMetadata, incremental, priorRefusals = [], clearedTiers) {
375156
- const { graphs, sources, connectionDigests, connections } = compiled;
375386
+ const { builds, sources, connectionDigests, connections } = compiled;
375157
375387
  const index = indexInstructions(instructions);
375158
375388
  const run = {
375159
375389
  compiled,
@@ -375173,17 +375403,17 @@ class MaterializationService {
375173
375403
  builtSources: [],
375174
375404
  writtenTargets: new Map
375175
375405
  };
375176
- assertNoTableCollisions(graphs, sources, connectionDigests, index);
375406
+ assertNoTableCollisions(builds, sources, connectionDigests, index);
375177
375407
  try {
375178
- for (const graph of graphs) {
375179
- const connection = connections.get(graph.connectionName);
375408
+ for (const build of builds) {
375409
+ const connection = connections.get(build.connectionName);
375180
375410
  if (!connection) {
375181
- throw new BadRequestError(`Connection '${graph.connectionName}' not found`);
375411
+ throw new BadRequestError(`Connection '${build.connectionName}' not found`);
375182
375412
  }
375183
- for (const persistSource of iterGraphSources(graph, sources)) {
375413
+ for (const persistSource of sourcesInBuildOrder(build, sources)) {
375184
375414
  if (signal.aborted)
375185
375415
  throw new Error("Build cancelled");
375186
- await this.buildInstructedSource(run, persistSource, graph.connectionName, connection);
375416
+ await this.buildInstructedSource(run, persistSource, build.connectionName, connection);
375187
375417
  }
375188
375418
  }
375189
375419
  if (run.builtSources.length === 0 && run.failedReasons.length > 0) {
@@ -376944,7 +377174,7 @@ function readerSql(uri, format, options = {}) {
376944
377174
  return `read_parquet(${path25})`;
376945
377175
  }
376946
377176
  function assertUnderDataPath(uri, dataPath) {
376947
- const root = dataPath.replace(/\/+$/, "");
377177
+ const root = stripTrailingSlashes(dataPath);
376948
377178
  const outside = () => new BadRequestError(`The object must sit under the catalog's data path (${root}/).`);
376949
377179
  if (!root || !uri.startsWith(`${root}/`))
376950
377180
  throw outside();
Binary file
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "snowflake"
20
20
  ],
21
21
  "homepage": "https://docs.malloydata.dev/documentation/user_guides/publishing/mcp_agents",
22
- "version": "0.10.3",
22
+ "version": "0.10.5",
23
23
  "main": "dist/server.mjs",
24
24
  "bin": {
25
25
  "malloy-publisher": "dist/server.mjs"
Binary file