@malloy-publisher/server 0.0.167 → 0.0.168

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.
Files changed (29) hide show
  1. package/.eslintrc.json +9 -1
  2. package/dist/app/api-doc.yaml +36 -1
  3. package/dist/app/assets/HomePage-D2tUw_9U.js +1 -0
  4. package/dist/app/assets/{MainPage-C9Fr5IN8.js → MainPage-DBQW76L7.js} +2 -2
  5. package/dist/app/assets/{ModelPage-BkU6HAHA.js → ModelPage-BnfOKuhQ.js} +1 -1
  6. package/dist/app/assets/PackagePage-zPhE-rDg.js +1 -0
  7. package/dist/app/assets/ProjectPage-BpSTvuW6.js +1 -0
  8. package/dist/app/assets/RouteError-Cp9-yCK5.js +1 -0
  9. package/dist/app/assets/{WorkbookPage-D3rUQZj6.js → WorkbookPage-FD_gmxeE.js} +1 -1
  10. package/dist/app/assets/{index-BLxl0XLH.js → index-D5QBYuLK.js} +150 -150
  11. package/dist/app/assets/{index-lhDwptrQ.js → index-DNCvL_5f.js} +1 -1
  12. package/dist/app/assets/{index-hkABoiMV.js → index-x9S1fsYn.js} +1 -1
  13. package/dist/app/assets/{index.umd-BkXQ-YAe.js → index.umd-CTYdFEHH.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.js +261 -27
  16. package/package.json +1 -1
  17. package/src/controller/connection.controller.ts +22 -2
  18. package/src/server.ts +5 -1
  19. package/src/service/connection.spec.ts +105 -0
  20. package/src/service/connection.ts +293 -17
  21. package/src/service/db_utils.ts +85 -4
  22. package/src/service/project.ts +20 -3
  23. package/tests/harness/mcp_test_setup.ts +166 -26
  24. package/tests/unit/duckdb/attached_databases.test.ts +61 -3
  25. package/tests/unit/ducklake/ducklake.test.ts +950 -0
  26. package/dist/app/assets/HomePage-D76UaGFV.js +0 -1
  27. package/dist/app/assets/PackagePage-BhE9Wi7b.js +0 -1
  28. package/dist/app/assets/ProjectPage-BatZLVap.js +0 -1
  29. package/dist/app/assets/RouteError-Bo5zJ8Xa.js +0 -1
@@ -12,7 +12,7 @@
12
12
  href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
13
13
  />
14
14
  <title>Malloy Publisher</title>
15
- <script type="module" crossorigin src="/assets/index-BLxl0XLH.js"></script>
15
+ <script type="module" crossorigin src="/assets/index-D5QBYuLK.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/assets/index-CMlGQMcl.css">
17
17
  </head>
18
18
  <body>
package/dist/server.js CHANGED
@@ -219260,35 +219260,95 @@ async function attachSnowflake(connection, attachedDb) {
219260
219260
  await connection.runSQL(attachCommand);
219261
219261
  logger.info(`Successfully attached Snowflake database: ${attachedDb.name}`);
219262
219262
  }
219263
+ function buildPgConnectionString(pg) {
219264
+ if (pg.connectionString) {
219265
+ return pg.connectionString;
219266
+ }
219267
+ const parts = [];
219268
+ if (pg.host)
219269
+ parts.push(`host=${pg.host}`);
219270
+ if (pg.port)
219271
+ parts.push(`port=${pg.port}`);
219272
+ if (pg.databaseName)
219273
+ parts.push(`dbname=${pg.databaseName}`);
219274
+ if (pg.userName)
219275
+ parts.push(`user=${pg.userName}`);
219276
+ if (pg.password)
219277
+ parts.push(`password=${pg.password}`);
219278
+ const pgSSLMode = process.env.PGSSLMODE;
219279
+ if (pgSSLMode) {
219280
+ const mapping = {
219281
+ "no-verify": "disable",
219282
+ disable: "disable",
219283
+ allow: "allow",
219284
+ prefer: "prefer",
219285
+ require: "require",
219286
+ "verify-ca": "verify-ca",
219287
+ "verify-full": "verify-full"
219288
+ };
219289
+ const sslmode = mapping[pgSSLMode.toLowerCase()];
219290
+ if (sslmode)
219291
+ parts.push(`sslmode=${sslmode}`);
219292
+ }
219293
+ return parts.join(" ");
219294
+ }
219263
219295
  async function attachPostgres(connection, attachedDb) {
219264
219296
  if (!attachedDb.postgresConnection) {
219265
219297
  throw new Error(`PostgreSQL connection configuration missing for: ${attachedDb.name}`);
219266
219298
  }
219267
219299
  await installAndLoadExtension(connection, "postgres");
219268
219300
  const config = attachedDb.postgresConnection;
219269
- let attachString;
219270
- if (config.connectionString) {
219271
- attachString = config.connectionString;
219272
- } else {
219273
- const parts = [];
219274
- if (config.host)
219275
- parts.push(`host=${config.host}`);
219276
- if (config.port)
219277
- parts.push(`port=${config.port}`);
219278
- if (config.databaseName)
219279
- parts.push(`dbname=${config.databaseName}`);
219280
- if (config.userName)
219281
- parts.push(`user=${config.userName}`);
219282
- if (config.password)
219283
- parts.push(`password=${config.password}`);
219284
- if (process.env.PGSSLMODE === "no-verify")
219285
- parts.push(`sslmode=disable`);
219286
- attachString = parts.join(" ");
219287
- }
219288
- const attachCommand = `ATTACH '${attachString}' AS ${attachedDb.name} (TYPE postgres, READ_ONLY);`;
219301
+ const attachString = buildPgConnectionString(config);
219302
+ const attachCommand = `ATTACH '${escapeSQL(attachString)}' AS ${attachedDb.name} (TYPE postgres, READ_ONLY);`;
219289
219303
  await connection.runSQL(attachCommand);
219290
219304
  logger.info(`Successfully attached PostgreSQL database: ${attachedDb.name}`);
219291
219305
  }
219306
+ async function attachDuckLake(connection, dbName, ducklakeConfig) {
219307
+ await installAndLoadExtension(connection, "ducklake");
219308
+ await installAndLoadExtension(connection, "postgres");
219309
+ await installAndLoadExtension(connection, "aws");
219310
+ await installAndLoadExtension(connection, "httpfs");
219311
+ if (!ducklakeConfig.catalog?.postgresConnection) {
219312
+ throw new Error(`PostgreSQL connection configuration is required for DuckLake catalog: ${dbName}`);
219313
+ }
219314
+ if (!ducklakeConfig.storage?.bucketUrl) {
219315
+ throw new Error(`Storage bucketUrl is required for DuckLake: ${dbName}`);
219316
+ }
219317
+ const hasS3 = !!ducklakeConfig.storage.s3Connection;
219318
+ const hasGCS = !!ducklakeConfig.storage.gcsConnection;
219319
+ if (hasS3) {
219320
+ await attachCloudStorage(connection, {
219321
+ name: `${dbName}_storage`,
219322
+ type: "s3",
219323
+ s3Connection: ducklakeConfig.storage.s3Connection
219324
+ });
219325
+ } else if (hasGCS) {
219326
+ await attachCloudStorage(connection, {
219327
+ name: `${dbName}_storage`,
219328
+ type: "gcs",
219329
+ gcsConnection: ducklakeConfig.storage.gcsConnection
219330
+ });
219331
+ }
219332
+ const pg = ducklakeConfig.catalog.postgresConnection;
219333
+ const pgConnString = buildPgConnectionString(pg);
219334
+ logger.info(`pgConnString: ${pgConnString}`);
219335
+ const escapedPgConnString = escapeSQL(pgConnString);
219336
+ logger.info(`Final escaped connection string: ${escapedPgConnString}`);
219337
+ const escapedBucketUrl = escapeSQL(ducklakeConfig.storage.bucketUrl);
219338
+ logger.info(`escapedBucketUrl: ${escapedBucketUrl}`);
219339
+ const attachCommand = `ATTACH OR REPLACE 'ducklake:postgres:${escapedPgConnString}' AS ${dbName} (DATA_PATH '${escapedBucketUrl}', OVERRIDE_DATA_PATH true, READ_ONLY true);`;
219340
+ logger.info(`Attaching DuckLake database using command: ${attachCommand}`);
219341
+ try {
219342
+ await connection.runSQL(attachCommand);
219343
+ logger.info(`Successfully attached DuckLake database in READ_ONLY mode: ${dbName}`);
219344
+ } catch (error) {
219345
+ if (error instanceof Error && (error.message.includes("already exists") || error.message.includes("already attached"))) {
219346
+ logger.info(`DuckLake database ${dbName} is already attached, skipping`);
219347
+ } else {
219348
+ throw error;
219349
+ }
219350
+ }
219351
+ }
219292
219352
  async function attachCloudStorage(connection, attachedDb) {
219293
219353
  const isGCS = attachedDb.type === "gcs";
219294
219354
  const isS3 = attachedDb.type === "s3";
@@ -219374,10 +219434,23 @@ async function attachCloudStorage(connection, attachedDb) {
219374
219434
  `;
219375
219435
  }
219376
219436
  }
219437
+ if (await doesSecretExistInDuckDB(connection, secretName)) {
219438
+ await connection.runSQL(`DETACH ${attachedDb.name};`).catch(() => {});
219439
+ }
219377
219440
  await connection.runSQL(createSecretCommand);
219378
219441
  logger.info(`Created ${storageType} secret: ${secretName}`);
219379
219442
  logger.info(`${storageType} connection configured for: ${attachedDb.name}`);
219380
219443
  }
219444
+ async function doesSecretExistInDuckDB(connection, secretName) {
219445
+ const escapedSecretName = escapeSQL(secretName);
219446
+ const result = await connection.runSQL(`
219447
+ SELECT COUNT(*) AS count
219448
+ FROM duckdb_secrets()
219449
+ WHERE name = '${escapedSecretName}';
219450
+ `);
219451
+ const rows = result.rows;
219452
+ return Number(rows?.[0]?.count ?? 0) > 0;
219453
+ }
219381
219454
  async function attachDatabasesToDuckDB(duckdbConnection, attachedDatabases) {
219382
219455
  const attachHandlers = {
219383
219456
  bigquery: attachBigQuery,
@@ -219407,7 +219480,53 @@ async function attachDatabasesToDuckDB(duckdbConnection, attachedDatabases) {
219407
219480
  }
219408
219481
  }
219409
219482
  }
219410
- async function createProjectConnections(connections = [], projectPath = "") {
219483
+
219484
+ class DuckLakeConnection extends import_db_duckdb.DuckDBConnection {
219485
+ connectionName;
219486
+ constructor(connectionName, databasePath, workingDirectory) {
219487
+ super(connectionName, databasePath, workingDirectory);
219488
+ if (!databasePath.endsWith("_ducklake.duckdb")) {
219489
+ throw new Error(`DuckLakeConnection should only be used for DuckLake connections. ` + `Expected database path ending with '_ducklake.duckdb', got: ${databasePath}`);
219490
+ }
219491
+ this.connectionName = connectionName;
219492
+ }
219493
+ async fetchTableSchema(tableKey, tablePath) {
219494
+ const parts = tablePath.split(".");
219495
+ if (!tablePath.startsWith(this.connectionName) && (parts.length === 1 || parts.length === 2)) {
219496
+ const prefixedPath = `${this.connectionName}.${tablePath}`;
219497
+ logger.debug("Prefixing DuckLake table path", {
219498
+ original: tablePath,
219499
+ prefixed: prefixedPath,
219500
+ connectionName: this.connectionName
219501
+ });
219502
+ const result2 = await super.fetchTableSchema(tableKey, prefixedPath);
219503
+ if (!result2) {
219504
+ throw new Error(`Table ${prefixedPath} not found in connection ${this.connectionName}`);
219505
+ }
219506
+ return result2;
219507
+ }
219508
+ const result = await super.fetchTableSchema(tableKey, tablePath);
219509
+ if (!result) {
219510
+ throw new Error(`Table ${tablePath} not found in connection ${this.connectionName}`);
219511
+ }
219512
+ return result;
219513
+ }
219514
+ }
219515
+ async function deleteDuckLakeConnectionFile(connectionName, projectPath) {
219516
+ const ducklakePath = import_path.default.join(projectPath, `${connectionName}_ducklake.duckdb`);
219517
+ try {
219518
+ await import_promises.default.access(ducklakePath);
219519
+ await import_promises.default.rm(ducklakePath);
219520
+ logger.info(`Removed DuckLake connection file ${connectionName}_ducklake.duckdb from ${projectPath}`);
219521
+ } catch (error) {
219522
+ if (error.code === "ENOENT") {
219523
+ logger.debug(`DuckLake connection file ${connectionName}_ducklake.duckdb does not exist, skipping deletion`);
219524
+ } else {
219525
+ logger.error(`Failed to remove DuckLake connection file ${connectionName}_ducklake.duckdb from ${projectPath}`, { error });
219526
+ }
219527
+ }
219528
+ }
219529
+ async function createProjectConnections(connections = [], projectPath = "", isUpdateConnectionRequest = false) {
219411
219530
  const connectionMap = new Map;
219412
219531
  const processedConnections = new Set;
219413
219532
  const apiConnections = [];
@@ -219577,6 +219696,18 @@ async function createProjectConnections(connections = [], projectPath = "") {
219577
219696
  connection.attributes = getConnectionAttributes(motherduckConnection);
219578
219697
  break;
219579
219698
  }
219699
+ case "ducklake": {
219700
+ if (!connection.ducklakeConnection) {
219701
+ throw new Error("DuckLake connection configuration is missing.");
219702
+ }
219703
+ const ducklakeDuckdbConnection = new DuckLakeConnection(connection.name, import_path.default.join(projectPath, `${connection.name}_ducklake.duckdb`), projectPath);
219704
+ if (isUpdateConnectionRequest || !await isDatabaseAttached(ducklakeDuckdbConnection, connection.name)) {
219705
+ await attachDuckLake(ducklakeDuckdbConnection, connection.name, connection.ducklakeConnection);
219706
+ }
219707
+ connectionMap.set(connection.name, ducklakeDuckdbConnection);
219708
+ connection.attributes = getConnectionAttributes(ducklakeDuckdbConnection);
219709
+ break;
219710
+ }
219580
219711
  default: {
219581
219712
  throw new Error(`Unsupported connection type: ${connection.type}`);
219582
219713
  }
@@ -219656,17 +219787,27 @@ ${failedAttachments.join(`
219656
219787
  }
219657
219788
  }
219658
219789
  async function testConnectionConfig(connectionConfig) {
219790
+ let malloyConnections = null;
219659
219791
  try {
219660
219792
  if (!connectionConfig.name) {
219661
219793
  throw new Error("Connection name is required");
219662
219794
  }
219663
- const { malloyConnections } = await createProjectConnections([connectionConfig]);
219795
+ const result = await createProjectConnections([connectionConfig]);
219796
+ malloyConnections = result.malloyConnections;
219664
219797
  const connection = malloyConnections.get(connectionConfig.name);
219665
219798
  if (!connection) {
219666
219799
  throw new Error(`Failed to create connection: ${connectionConfig.name}`);
219667
219800
  }
219668
219801
  if (connectionConfig.type === "duckdb") {
219669
219802
  await testDuckDBConnection(connection, connectionConfig);
219803
+ } else if (connectionConfig.type === "ducklake") {
219804
+ const duckConn = connection;
219805
+ const attached = await isDatabaseAttached(duckConn, connectionConfig.name);
219806
+ if (!attached) {
219807
+ throw new Error(`DuckLake connection test failed: Error attaching database '${connectionConfig.name}'`);
219808
+ }
219809
+ await duckConn.runSQL(`SELECT schema_name FROM information_schema.schemata WHERE catalog_name = '${connectionConfig.name}' LIMIT 1`);
219810
+ logger.info(`DuckLake connection test passed: ${connectionConfig.name}`);
219670
219811
  } else {
219671
219812
  await connection.test();
219672
219813
  }
@@ -219684,6 +219825,22 @@ async function testConnectionConfig(connectionConfig) {
219684
219825
  status: "failed",
219685
219826
  errorMessage: error.message
219686
219827
  };
219828
+ } finally {
219829
+ if (malloyConnections) {
219830
+ for (const [connName, conn] of malloyConnections) {
219831
+ try {
219832
+ if (conn && typeof conn.close === "function") {
219833
+ await conn.close();
219834
+ }
219835
+ } catch (closeError) {
219836
+ logger.warn(`Error closing connection ${connName} during test cleanup`, { error: closeError });
219837
+ } finally {
219838
+ if (connectionConfig.type === "ducklake") {
219839
+ await deleteDuckLakeConnectionFile(connName, process.cwd());
219840
+ }
219841
+ }
219842
+ }
219843
+ }
219687
219844
  }
219688
219845
  }
219689
219846
 
@@ -220246,6 +220403,25 @@ async function getSchemasForConnection(connection, malloyConnection) {
220246
220403
  console.error(`Error getting schemas for MotherDuck connection ${connection.name}:`, error);
220247
220404
  throw new Error(`Failed to get schemas for MotherDuck connection ${connection.name}: ${error.message}`);
220248
220405
  }
220406
+ } else if (connection.type === "ducklake") {
220407
+ try {
220408
+ const catalogName = connection.name;
220409
+ const result = await malloyConnection.runSQL(`SELECT schema_name FROM information_schema.schemata WHERE catalog_name = '${catalogName}' ORDER BY schema_name`, { rowLimit: 1000 });
220410
+ const rows = standardizeRunSQLResult2(result);
220411
+ return rows.map((row) => {
220412
+ const typedRow = row;
220413
+ const schemaName = typedRow.schema_name;
220414
+ const shouldShow = schemaName === "main" || schemaName === "public";
220415
+ return {
220416
+ name: schemaName,
220417
+ isHidden: !shouldShow,
220418
+ isDefault: false
220419
+ };
220420
+ });
220421
+ } catch (error) {
220422
+ logger.error(`Error getting schemas for DuckLake connection ${connection.name}`, { error });
220423
+ throw new Error(`Failed to get schemas for DuckLake connection ${connection.name}: ${error.message}`);
220424
+ }
220249
220425
  } else {
220250
220426
  throw new Error(`Unsupported connection type: ${connection.type}`);
220251
220427
  }
@@ -220265,6 +220441,12 @@ async function getTablesForSchema(connection, schemaName, malloyConnection) {
220265
220441
  }
220266
220442
  const fileKeys = await listDataFilesInDirectory(credentials, bucketName, directoryPath);
220267
220443
  return await getCloudTablesWithColumns(malloyConnection, credentials, bucketName, fileKeys);
220444
+ } else if (connection.type === "ducklake") {
220445
+ if (schemaName.split(".").length == 2) {
220446
+ schemaName = `${connection.name}.${schemaName}`;
220447
+ } else if (schemaName.split(".").length === 1) {
220448
+ schemaName = `${connection.name}.${schemaName}`;
220449
+ }
220268
220450
  }
220269
220451
  const tableNames = await listTablesForSchema(connection, schemaName, malloyConnection);
220270
220452
  const tableSourcePromises = tableNames.map(async (tableName) => {
@@ -220276,6 +220458,8 @@ async function getTablesForSchema(connection, schemaName, malloyConnection) {
220276
220458
  } else {
220277
220459
  tablePath = `${schemaName}.${tableName}`;
220278
220460
  }
220461
+ } else if (connection.type === "ducklake") {
220462
+ tablePath = `${schemaName}.${tableName}`;
220279
220463
  } else {
220280
220464
  tablePath = `${schemaName}.${tableName}`;
220281
220465
  }
@@ -220287,7 +220471,7 @@ async function getTablesForSchema(connection, schemaName, malloyConnection) {
220287
220471
  };
220288
220472
  } catch (error) {
220289
220473
  logger.warn(`Failed to get schema for table ${tableName}`, {
220290
- error,
220474
+ error: extractErrorDataFromError(error),
220291
220475
  schemaName,
220292
220476
  tableName
220293
220477
  });
@@ -220476,10 +220660,39 @@ async function listTablesForSchema(connection, schemaName, malloyConnection) {
220476
220660
  logger.error(`Error getting tables for MotherDuck schema ${schemaName} in connection ${connection.name}`, { error });
220477
220661
  throw new Error(`Failed to get tables for MotherDuck schema ${schemaName} in connection ${connection.name}: ${error.message}`);
220478
220662
  }
220663
+ } else if (connection.type === "ducklake") {
220664
+ const catalogName = schemaName.split(".")[0];
220665
+ const actualSchemaName = schemaName.split(".")[1];
220666
+ console.error("catalogName", catalogName);
220667
+ console.error("actualSchemaName", actualSchemaName);
220668
+ try {
220669
+ const result = await malloyConnection.runSQL(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${actualSchemaName}' AND table_catalog = '${catalogName}' ORDER BY table_name`, { rowLimit: 1000 });
220670
+ const rows = standardizeRunSQLResult2(result);
220671
+ return rows.map((row) => {
220672
+ const typedRow = row;
220673
+ return typedRow.table_name;
220674
+ });
220675
+ } catch (error) {
220676
+ logger.error(`Error getting tables for DuckLake schema ${schemaName} in connection ${connection.name}`, { error });
220677
+ throw new Error(`Failed to get tables for DuckLake schema ${schemaName} in connection ${connection.name}: ${error.message}`);
220678
+ }
220479
220679
  } else {
220480
220680
  throw new Error(`Unsupported connection type: ${connection.type}`);
220481
220681
  }
220482
220682
  }
220683
+ function extractErrorDataFromError(error) {
220684
+ const errorMessage = error instanceof Error ? error.message : String(error);
220685
+ const errorData = {
220686
+ error: errorMessage
220687
+ };
220688
+ if (error instanceof Error && logger.level === "debug") {
220689
+ errorData.stack = error.stack;
220690
+ }
220691
+ if (error && typeof error === "object" && "task" in error) {
220692
+ errorData.task = error.task;
220693
+ }
220694
+ return errorData;
220695
+ }
220483
220696
 
220484
220697
  // src/controller/connection.controller.ts
220485
220698
  class ConnectionController {
@@ -220547,9 +220760,21 @@ class ConnectionController {
220547
220760
  const malloyConnection = await this.getMalloyConnection(projectName, connectionName);
220548
220761
  return getConnectionTableSource(malloyConnection, tableKey, tablePath);
220549
220762
  }
220550
- async getTable(projectName, connectionName, _schemaName, tablePath) {
220763
+ async getTable(projectName, connectionName, schemaName, tablePath) {
220551
220764
  const malloyConnection = await this.getMalloyConnection(projectName, connectionName);
220552
- const tableSource = await getConnectionTableSource(malloyConnection, tablePath.split(".").pop(), tablePath);
220765
+ const connection = await this.getConnection(projectName, connectionName);
220766
+ if (connection.type === "ducklake") {
220767
+ if (tablePath.split(".").length === 1) {
220768
+ tablePath = `${connectionName}.${schemaName}.${tablePath}`;
220769
+ } else if (tablePath.split(".").length === 2 && !tablePath.startsWith(connectionName)) {
220770
+ tablePath = `${connectionName}.${tablePath}`;
220771
+ }
220772
+ }
220773
+ const tableKey = tablePath.split(".").pop();
220774
+ if (!tableKey) {
220775
+ throw new Error(`Invalid tablePath: ${tablePath}`);
220776
+ }
220777
+ const tableSource = await getConnectionTableSource(malloyConnection, tableKey, tablePath);
220553
220778
  return {
220554
220779
  resource: tablePath,
220555
220780
  columns: tableSource.columns,
@@ -228339,7 +228564,8 @@ class Project {
228339
228564
  }
228340
228565
  if (payload.connections) {
228341
228566
  logger.info(`Updating ${payload.connections.length} connections for project ${this.projectName}`);
228342
- const { malloyConnections, apiConnections } = await createProjectConnections(payload.connections, this.projectPath);
228567
+ const isUpdateConnectionRequest = true;
228568
+ const { malloyConnections, apiConnections } = await createProjectConnections(payload.connections, this.projectPath, isUpdateConnectionRequest);
228343
228569
  this.malloyConnections = malloyConnections;
228344
228570
  this.apiConnections = apiConnections;
228345
228571
  logger.info(`Successfully updated connections for project ${this.projectName}`, {
@@ -228609,8 +228835,11 @@ ${source}` : source;
228609
228835
  this.malloyConnections.get(connectionName)?.close();
228610
228836
  const isDeleted = this.malloyConnections.delete(connectionName);
228611
228837
  const index = this.apiConnections.findIndex((conn) => conn.name === connectionName);
228612
- if (this.apiConnections[index]?.type === "duckdb") {
228838
+ const connectionType = this.apiConnections[index]?.type;
228839
+ if (connectionType === "duckdb") {
228613
228840
  await this.deleteDuckDBConnection(connectionName);
228841
+ } else if (connectionType === "ducklake") {
228842
+ await this.deleteDuckLakeConnection(connectionName);
228614
228843
  }
228615
228844
  if (index !== -1) {
228616
228845
  this.apiConnections.splice(index, 1);
@@ -228653,6 +228882,10 @@ ${source}` : source;
228653
228882
  logger.error(`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`, { error });
228654
228883
  });
228655
228884
  }
228885
+ async deleteDuckLakeConnection(connectionName) {
228886
+ await deleteDuckLakeConnectionFile(connectionName, this.projectPath);
228887
+ logger.info(`Removed DuckLake connection ${connectionName} from project ${this.projectName}`);
228888
+ }
228656
228889
  }
228657
228890
  async function extractPreamble(modelPath) {
228658
228891
  try {
@@ -233541,6 +233774,7 @@ var databaseController = new DatabaseController(projectStore);
233541
233774
  var queryController = new QueryController(projectStore);
233542
233775
  var compileController = new CompileController(projectStore);
233543
233776
  var mcpApp = import_express.default();
233777
+ registerHealthEndpoints(mcpApp);
233544
233778
  mcpApp.use(MCP_ENDPOINT, import_express.default.json());
233545
233779
  mcpApp.use(MCP_ENDPOINT, import_cors.default());
233546
233780
  mcpApp.all(MCP_ENDPOINT, async (req, res) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.0.167",
4
+ "version": "0.0.168",
5
5
  "main": "dist/server.js",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.js"
@@ -155,17 +155,37 @@ export class ConnectionController {
155
155
  public async getTable(
156
156
  projectName: string,
157
157
  connectionName: string,
158
- _schemaName: string,
158
+ schemaName: string,
159
159
  tablePath: string,
160
160
  ): Promise<ApiTable> {
161
161
  const malloyConnection = await this.getMalloyConnection(
162
162
  projectName,
163
163
  connectionName,
164
164
  );
165
+ const connection = await this.getConnection(projectName, connectionName);
166
+
167
+ if (connection.type === "ducklake") {
168
+ if (tablePath.split(".").length === 1) {
169
+ // tablePath is just the table name, construct full path
170
+ tablePath = `${connectionName}.${schemaName}.${tablePath}`;
171
+ } else if (
172
+ tablePath.split(".").length === 2 &&
173
+ !tablePath.startsWith(connectionName)
174
+ ) {
175
+ // tablePath is schemaName.tableName but missing connection prefix
176
+ tablePath = `${connectionName}.${tablePath}`;
177
+ }
178
+ // If tablePath already has 3+ parts or starts with connection name, use as-is
179
+ }
180
+
181
+ const tableKey = tablePath.split(".").pop();
182
+ if (!tableKey) {
183
+ throw new Error(`Invalid tablePath: ${tablePath}`);
184
+ }
165
185
 
166
186
  const tableSource = await getConnectionTableSource(
167
187
  malloyConnection,
168
- tablePath.split(".").pop()!, // tableKey is the table name
188
+ tableKey, // tableKey is the table name
169
189
  tablePath,
170
190
  );
171
191
 
package/src/server.ts CHANGED
@@ -131,6 +131,9 @@ const compileController = new CompileController(projectStore);
131
131
 
132
132
  export const mcpApp = express();
133
133
 
134
+ // Register health endpoints on mcpApp (for E2E tests)
135
+ registerHealthEndpoints(mcpApp);
136
+
134
137
  mcpApp.use(MCP_ENDPOINT, express.json());
135
138
  mcpApp.use(MCP_ENDPOINT, cors());
136
139
 
@@ -253,7 +256,8 @@ app.use(
253
256
  );
254
257
  app.use(bodyParser.json());
255
258
 
256
- // Register health check endpoints
259
+ // Register health check endpoints on main app:
260
+ // - Required for production/Kubernetes monitoring (main server on PUBLISHER_PORT)
257
261
  registerHealthEndpoints(app);
258
262
 
259
263
  // Register Prometheus metrics endpoint
@@ -975,6 +975,111 @@ describe("connection integration tests", () => {
975
975
  });
976
976
 
977
977
  describe("error handling", () => {
978
+ describe("DuckLake connection type", () => {
979
+ it(
980
+ "should create DuckLake connection",
981
+ async () => {
982
+ if (!hasPostgresCredentials() || !hasS3Credentials()) {
983
+ console.log(
984
+ "Skipping: PostgreSQL and S3 credentials not configured",
985
+ );
986
+ return;
987
+ }
988
+
989
+ const { malloyConnections } = await createProjectConnections(
990
+ [
991
+ {
992
+ name: "ducklake_test",
993
+ type: "ducklake",
994
+ ducklakeConnection: {
995
+ catalog: {
996
+ postgresConnection: {
997
+ host: process.env.POSTGRES_TEST_HOST,
998
+ port: parseInt(
999
+ process.env.POSTGRES_TEST_PORT || "5432",
1000
+ ),
1001
+ userName: process.env.POSTGRES_TEST_USER!,
1002
+ password:
1003
+ process.env.POSTGRES_TEST_PASSWORD!,
1004
+ databaseName:
1005
+ process.env.POSTGRES_TEST_DATABASE,
1006
+ },
1007
+ },
1008
+ storage: {
1009
+ bucketUrl:
1010
+ process.env.S3_TEST_BUCKET_URL ||
1011
+ "s3://test-bucket",
1012
+ s3Connection: {
1013
+ accessKeyId:
1014
+ process.env.S3_TEST_ACCESS_KEY_ID!,
1015
+ secretAccessKey:
1016
+ process.env.S3_TEST_SECRET_ACCESS_KEY!,
1017
+ },
1018
+ },
1019
+ },
1020
+ },
1021
+ ],
1022
+ testProjectPath,
1023
+ );
1024
+
1025
+ const connection = malloyConnections.get(
1026
+ "ducklake_test",
1027
+ ) as DuckDBConnection;
1028
+ createdConnections.push(connection);
1029
+ expect(connection).toBeDefined();
1030
+
1031
+ // Verify DuckLake database is attached
1032
+ const databases = await connection.runSQL("SHOW DATABASES");
1033
+ const dbNames = databases.rows.map(
1034
+ (row) => Object.values(row)[0],
1035
+ );
1036
+ expect(dbNames).toContain("ducklake_test");
1037
+ },
1038
+ { timeout: 30000 },
1039
+ );
1040
+
1041
+ it("should throw error if DuckLake catalog connection is missing", async () => {
1042
+ await expect(
1043
+ createProjectConnections(
1044
+ [
1045
+ {
1046
+ name: "ducklake_no_catalog",
1047
+ type: "ducklake",
1048
+ ducklakeConnection: {
1049
+ storage: {
1050
+ bucketUrl: "s3://test-bucket",
1051
+ s3Connection: {
1052
+ accessKeyId: "test",
1053
+ secretAccessKey: "test",
1054
+ },
1055
+ },
1056
+ },
1057
+ } as ApiConnection,
1058
+ ],
1059
+ testProjectPath,
1060
+ ),
1061
+ ).rejects.toThrow(
1062
+ /PostgreSQL connection configuration is required/,
1063
+ );
1064
+ });
1065
+
1066
+ it("should throw error if DuckLake connection config is missing", async () => {
1067
+ await expect(
1068
+ createProjectConnections(
1069
+ [
1070
+ {
1071
+ name: "ducklake_missing_config",
1072
+ type: "ducklake",
1073
+ },
1074
+ ],
1075
+ testProjectPath,
1076
+ ),
1077
+ ).rejects.toThrow(
1078
+ /DuckLake connection configuration is missing/,
1079
+ );
1080
+ });
1081
+ });
1082
+
978
1083
  it("should throw error if DuckDB connection name conflicts with attached database", async () => {
979
1084
  await expect(
980
1085
  createProjectConnections(