@malloy-publisher/server 0.0.167 → 0.0.169

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 (34) hide show
  1. package/.eslintrc.json +9 -1
  2. package/dist/app/api-doc.yaml +74 -1
  3. package/dist/app/assets/HomePage-BWcnTdSg.js +1 -0
  4. package/dist/app/assets/{MainPage-C9Fr5IN8.js → MainPage-k-BDUTT_.js} +2 -2
  5. package/dist/app/assets/{ModelPage-BkU6HAHA.js → ModelPage-BbFrnQ1A.js} +1 -1
  6. package/dist/app/assets/PackagePage-CCwd7u2-.js +1 -0
  7. package/dist/app/assets/ProjectPage-CDHkneyO.js +1 -0
  8. package/dist/app/assets/RouteError-BZbAeXla.js +1 -0
  9. package/dist/app/assets/{WorkbookPage-D3rUQZj6.js → WorkbookPage-BcuqksYi.js} +1 -1
  10. package/dist/app/assets/{index-lhDwptrQ.js → index-BDS2El9V.js} +216 -216
  11. package/dist/app/assets/{index-BLxl0XLH.js → index-C-0P3N7Y.js} +150 -150
  12. package/dist/app/assets/index-EHh3Tsle.js +1237 -0
  13. package/dist/app/assets/{index.umd-BkXQ-YAe.js → index.umd-ClIgLTxW.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/instrumentation.js +2252 -964
  16. package/dist/server.js +2802 -1140
  17. package/package.json +10 -10
  18. package/src/controller/connection.controller.ts +22 -2
  19. package/src/controller/query.controller.ts +7 -0
  20. package/src/mcp/tools/execute_query_tool.ts +57 -29
  21. package/src/server.ts +5 -1
  22. package/src/service/connection.spec.ts +105 -0
  23. package/src/service/connection.ts +293 -17
  24. package/src/service/db_utils.ts +85 -4
  25. package/src/service/model.ts +11 -8
  26. package/src/service/project.ts +20 -3
  27. package/tests/harness/mcp_test_setup.ts +166 -26
  28. package/tests/unit/duckdb/attached_databases.test.ts +61 -3
  29. package/tests/unit/ducklake/ducklake.test.ts +950 -0
  30. package/dist/app/assets/HomePage-D76UaGFV.js +0 -1
  31. package/dist/app/assets/PackagePage-BhE9Wi7b.js +0 -1
  32. package/dist/app/assets/ProjectPage-BatZLVap.js +0 -1
  33. package/dist/app/assets/RouteError-Bo5zJ8Xa.js +0 -1
  34. package/dist/app/assets/index-hkABoiMV.js +0 -1259
@@ -4,7 +4,7 @@ import { MySQLConnection } from "@malloydata/db-mysql";
4
4
  import { PostgresConnection } from "@malloydata/db-postgres";
5
5
  import { SnowflakeConnection } from "@malloydata/db-snowflake";
6
6
  import { TrinoConnection } from "@malloydata/db-trino";
7
- import { Connection } from "@malloydata/malloy";
7
+ import { Connection, TableSourceDef } from "@malloydata/malloy";
8
8
  import { BaseConnection } from "@malloydata/malloy/connection";
9
9
  import { AxiosError } from "axios";
10
10
  import fs from "fs/promises";
@@ -29,6 +29,7 @@ export type InternalConnection = ApiConnection & {
29
29
  trinoConnection?: components["schemas"]["TrinoConnection"];
30
30
  mysqlConnection?: components["schemas"]["MysqlConnection"];
31
31
  duckdbConnection?: components["schemas"]["DuckdbConnection"];
32
+ ducklakeConnection?: components["schemas"]["DucklakeConnection"];
32
33
  };
33
34
 
34
35
  function validateAndBuildTrinoConfig(
@@ -345,6 +346,40 @@ async function attachSnowflake(
345
346
  logger.info(`Successfully attached Snowflake database: ${attachedDb.name}`);
346
347
  }
347
348
 
349
+ function buildPgConnectionString(
350
+ pg: components["schemas"]["PostgresConnection"],
351
+ ): string {
352
+ if (pg.connectionString) {
353
+ return pg.connectionString;
354
+ }
355
+
356
+ const parts: string[] = [];
357
+ if (pg.host) parts.push(`host=${pg.host}`);
358
+ if (pg.port) parts.push(`port=${pg.port}`);
359
+ if (pg.databaseName) parts.push(`dbname=${pg.databaseName}`);
360
+ if (pg.userName) parts.push(`user=${pg.userName}`);
361
+ if (pg.password) parts.push(`password=${pg.password}`);
362
+
363
+ const pgSSLMode = process.env.PGSSLMODE;
364
+
365
+ if (pgSSLMode) {
366
+ const mapping: Record<string, string> = {
367
+ "no-verify": "disable",
368
+ disable: "disable",
369
+ allow: "allow",
370
+ prefer: "prefer",
371
+ require: "require",
372
+ "verify-ca": "verify-ca",
373
+ "verify-full": "verify-full",
374
+ };
375
+
376
+ const sslmode = mapping[pgSSLMode.toLowerCase()];
377
+ if (sslmode) parts.push(`sslmode=${sslmode}`);
378
+ }
379
+
380
+ return parts.join(" ");
381
+ }
382
+
348
383
  async function attachPostgres(
349
384
  connection: DuckDBConnection,
350
385
  attachedDb: AttachedDatabase,
@@ -358,26 +393,82 @@ async function attachPostgres(
358
393
  await installAndLoadExtension(connection, "postgres");
359
394
 
360
395
  const config = attachedDb.postgresConnection;
361
- let attachString: string;
362
-
363
- if (config.connectionString) {
364
- attachString = config.connectionString;
365
- } else {
366
- const parts: string[] = [];
367
- if (config.host) parts.push(`host=${config.host}`);
368
- if (config.port) parts.push(`port=${config.port}`);
369
- if (config.databaseName) parts.push(`dbname=${config.databaseName}`);
370
- if (config.userName) parts.push(`user=${config.userName}`);
371
- if (config.password) parts.push(`password=${config.password}`);
372
- if (process.env.PGSSLMODE === "no-verify") parts.push(`sslmode=disable`);
373
- attachString = parts.join(" ");
374
- }
396
+ const attachString: string = buildPgConnectionString(config);
375
397
 
376
- const attachCommand = `ATTACH '${attachString}' AS ${attachedDb.name} (TYPE postgres, READ_ONLY);`;
398
+ const attachCommand = `ATTACH '${escapeSQL(attachString)}' AS ${attachedDb.name} (TYPE postgres, READ_ONLY);`;
377
399
  await connection.runSQL(attachCommand);
378
400
  logger.info(`Successfully attached PostgreSQL database: ${attachedDb.name}`);
379
401
  }
380
402
 
403
+ async function attachDuckLake(
404
+ connection: DuckDBConnection,
405
+ dbName: string,
406
+ ducklakeConfig: components["schemas"]["DucklakeConnection"],
407
+ ): Promise<void> {
408
+ await installAndLoadExtension(connection, "ducklake");
409
+ await installAndLoadExtension(connection, "postgres");
410
+ await installAndLoadExtension(connection, "aws");
411
+ await installAndLoadExtension(connection, "httpfs");
412
+ if (!ducklakeConfig.catalog?.postgresConnection) {
413
+ throw new Error(
414
+ `PostgreSQL connection configuration is required for DuckLake catalog: ${dbName}`,
415
+ );
416
+ }
417
+
418
+ if (!ducklakeConfig.storage?.bucketUrl) {
419
+ throw new Error(`Storage bucketUrl is required for DuckLake: ${dbName}`);
420
+ }
421
+
422
+ // Set up cloud storage secret so DuckDB can access S3/GCS
423
+ const hasS3 = !!ducklakeConfig.storage.s3Connection;
424
+ const hasGCS = !!ducklakeConfig.storage.gcsConnection;
425
+
426
+ if (hasS3) {
427
+ await attachCloudStorage(connection, {
428
+ name: `${dbName}_storage`,
429
+ type: "s3",
430
+ s3Connection: ducklakeConfig.storage.s3Connection,
431
+ });
432
+ } else if (hasGCS) {
433
+ await attachCloudStorage(connection, {
434
+ name: `${dbName}_storage`,
435
+ type: "gcs",
436
+ gcsConnection: ducklakeConfig.storage.gcsConnection,
437
+ });
438
+ }
439
+
440
+ const pg = ducklakeConfig.catalog.postgresConnection;
441
+ const pgConnString: string = buildPgConnectionString(pg);
442
+ // Attach DuckLake with Postgres catalog and cloud storage data path in READ_ONLY mode
443
+ // The client manages metadata - we only read from the catalogs
444
+ logger.info(`pgConnString: ${pgConnString}`);
445
+ const escapedPgConnString = escapeSQL(pgConnString);
446
+ logger.info(`Final escaped connection string: ${escapedPgConnString}`);
447
+ const escapedBucketUrl = escapeSQL(ducklakeConfig.storage.bucketUrl);
448
+ logger.info(`escapedBucketUrl: ${escapedBucketUrl}`);
449
+ const attachCommand = `ATTACH OR REPLACE 'ducklake:postgres:${escapedPgConnString}' AS ${dbName} (DATA_PATH '${escapedBucketUrl}', OVERRIDE_DATA_PATH true, READ_ONLY true);`;
450
+ logger.info(`Attaching DuckLake database using command: ${attachCommand}`);
451
+ try {
452
+ await connection.runSQL(attachCommand);
453
+ logger.info(
454
+ `Successfully attached DuckLake database in READ_ONLY mode: ${dbName}`,
455
+ );
456
+ } catch (error) {
457
+ // Handle case where DuckLake database is already attached
458
+ if (
459
+ error instanceof Error &&
460
+ (error.message.includes("already exists") ||
461
+ error.message.includes("already attached"))
462
+ ) {
463
+ logger.info(
464
+ `DuckLake database ${dbName} is already attached, skipping`,
465
+ );
466
+ } else {
467
+ throw error;
468
+ }
469
+ }
470
+ }
471
+
381
472
  async function attachCloudStorage(
382
473
  connection: DuckDBConnection,
383
474
  attachedDb: AttachedDatabase,
@@ -488,11 +579,30 @@ async function attachCloudStorage(
488
579
  }
489
580
  }
490
581
 
582
+ if (await doesSecretExistInDuckDB(connection, secretName)) {
583
+ // Force refresh attachments using this storage
584
+ await connection.runSQL(`DETACH ${attachedDb.name};`).catch(() => {});
585
+ }
491
586
  await connection.runSQL(createSecretCommand);
587
+
492
588
  logger.info(`Created ${storageType} secret: ${secretName}`);
493
589
  logger.info(`${storageType} connection configured for: ${attachedDb.name}`);
494
590
  }
495
591
 
592
+ async function doesSecretExistInDuckDB(
593
+ connection: DuckDBConnection,
594
+ secretName: string,
595
+ ): Promise<boolean> {
596
+ const escapedSecretName = escapeSQL(secretName);
597
+ const result = await connection.runSQL(`
598
+ SELECT COUNT(*) AS count
599
+ FROM duckdb_secrets()
600
+ WHERE name = '${escapedSecretName}';
601
+ `);
602
+ const rows = result.rows;
603
+ return Number(rows?.[0]?.count ?? 0) > 0;
604
+ }
605
+
496
606
  // Main attachment function
497
607
  async function attachDatabasesToDuckDB(
498
608
  duckdbConnection: DuckDBConnection,
@@ -540,9 +650,95 @@ async function attachDatabasesToDuckDB(
540
650
  }
541
651
  }
542
652
 
653
+ class DuckLakeConnection extends DuckDBConnection {
654
+ private connectionName: string;
655
+
656
+ constructor(
657
+ connectionName: string,
658
+ databasePath: string,
659
+ workingDirectory: string,
660
+ ) {
661
+ super(connectionName, databasePath, workingDirectory);
662
+
663
+ // Validate that this is a DuckLake connection by checking the database path pattern
664
+ if (!databasePath.endsWith("_ducklake.duckdb")) {
665
+ throw new Error(
666
+ `DuckLakeConnection should only be used for DuckLake connections. ` +
667
+ `Expected database path ending with '_ducklake.duckdb', got: ${databasePath}`,
668
+ );
669
+ }
670
+
671
+ this.connectionName = connectionName;
672
+ }
673
+
674
+ async fetchTableSchema(
675
+ tableKey: string,
676
+ tablePath: string,
677
+ ): Promise<TableSourceDef> {
678
+ // DuckLake-specific logic: prefix table path with connection name if needed
679
+ const parts = tablePath.split(".");
680
+ if (
681
+ !tablePath.startsWith(this.connectionName) &&
682
+ (parts.length === 1 || parts.length === 2)
683
+ ) {
684
+ const prefixedPath = `${this.connectionName}.${tablePath}`;
685
+ logger.debug("Prefixing DuckLake table path", {
686
+ original: tablePath,
687
+ prefixed: prefixedPath,
688
+ connectionName: this.connectionName,
689
+ });
690
+ const result = await super.fetchTableSchema(tableKey, prefixedPath);
691
+ if (!result) {
692
+ throw new Error(
693
+ `Table ${prefixedPath} not found in connection ${this.connectionName}`,
694
+ );
695
+ }
696
+ return result;
697
+ }
698
+ // If already prefixed or has 3+ parts, use as-is;
699
+ // For attached databases, in the future
700
+ const result = await super.fetchTableSchema(tableKey, tablePath);
701
+ if (!result) {
702
+ throw new Error(
703
+ `Table ${tablePath} not found in connection ${this.connectionName}`,
704
+ );
705
+ }
706
+ return result;
707
+ }
708
+ }
709
+
710
+ export async function deleteDuckLakeConnectionFile(
711
+ connectionName: string,
712
+ projectPath: string,
713
+ ): Promise<void> {
714
+ const ducklakePath = path.join(
715
+ projectPath,
716
+ `${connectionName}_ducklake.duckdb`,
717
+ );
718
+ try {
719
+ await fs.access(ducklakePath);
720
+ await fs.rm(ducklakePath);
721
+ logger.info(
722
+ `Removed DuckLake connection file ${connectionName}_ducklake.duckdb from ${projectPath}`,
723
+ );
724
+ } catch (error) {
725
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
726
+ logger.debug(
727
+ `DuckLake connection file ${connectionName}_ducklake.duckdb does not exist, skipping deletion`,
728
+ );
729
+ } else {
730
+ logger.error(
731
+ `Failed to remove DuckLake connection file ${connectionName}_ducklake.duckdb from ${projectPath}`,
732
+ { error },
733
+ );
734
+ }
735
+ }
736
+ }
737
+
543
738
  export async function createProjectConnections(
544
739
  connections: ApiConnection[] = [],
545
740
  projectPath: string = "",
741
+ isUpdateConnectionRequest: boolean = false,
546
742
  ): Promise<{
547
743
  malloyConnections: Map<string, BaseConnection>;
548
744
  apiConnections: InternalConnection[];
@@ -826,6 +1022,40 @@ export async function createProjectConnections(
826
1022
  break;
827
1023
  }
828
1024
 
1025
+ case "ducklake": {
1026
+ if (!connection.ducklakeConnection) {
1027
+ throw new Error("DuckLake connection configuration is missing.");
1028
+ }
1029
+
1030
+ // Creating one Connection per DuckLake connection to avoid conflicts with other connections and better isolation.
1031
+ const ducklakeDuckdbConnection = new DuckLakeConnection(
1032
+ connection.name,
1033
+ path.join(projectPath, `${connection.name}_ducklake.duckdb`),
1034
+ projectPath,
1035
+ );
1036
+
1037
+ // Only attach DuckLake if it's not already attached or is it an update connection request
1038
+ if (
1039
+ isUpdateConnectionRequest ||
1040
+ !(await isDatabaseAttached(
1041
+ ducklakeDuckdbConnection,
1042
+ connection.name,
1043
+ ))
1044
+ ) {
1045
+ await attachDuckLake(
1046
+ ducklakeDuckdbConnection,
1047
+ connection.name,
1048
+ connection.ducklakeConnection,
1049
+ );
1050
+ }
1051
+
1052
+ connectionMap.set(connection.name, ducklakeDuckdbConnection);
1053
+ connection.attributes = getConnectionAttributes(
1054
+ ducklakeDuckdbConnection,
1055
+ );
1056
+ break;
1057
+ }
1058
+
829
1059
  default: {
830
1060
  throw new Error(`Unsupported connection type: ${connection.type}`);
831
1061
  }
@@ -958,6 +1188,7 @@ async function testDuckDBConnection(
958
1188
  export async function testConnectionConfig(
959
1189
  connectionConfig: ApiConnection,
960
1190
  ): Promise<ApiConnectionStatus> {
1191
+ let malloyConnections: Map<string, BaseConnection> | null = null;
961
1192
  try {
962
1193
  // Validate that connection name is provided
963
1194
  if (!connectionConfig.name) {
@@ -965,9 +1196,10 @@ export async function testConnectionConfig(
965
1196
  }
966
1197
 
967
1198
  // Use createProjectConnections to create the connection, then test it
968
- const { malloyConnections } = await createProjectConnections(
1199
+ const result = await createProjectConnections(
969
1200
  [connectionConfig], // Pass the single connection config
970
1201
  );
1202
+ malloyConnections = result.malloyConnections;
971
1203
 
972
1204
  // Get the created connection
973
1205
  const connection = malloyConnections.get(connectionConfig.name);
@@ -983,6 +1215,25 @@ export async function testConnectionConfig(
983
1215
  connection as DuckDBConnection,
984
1216
  connectionConfig as InternalConnection,
985
1217
  );
1218
+ } else if (connectionConfig.type === "ducklake") {
1219
+ // DuckLake uses DuckDB internally — verify the database is attached
1220
+ const duckConn = connection as DuckDBConnection;
1221
+ const attached = await isDatabaseAttached(
1222
+ duckConn,
1223
+ connectionConfig.name as string,
1224
+ );
1225
+ if (!attached) {
1226
+ throw new Error(
1227
+ `DuckLake connection test failed: Error attaching database '${connectionConfig.name}'`,
1228
+ );
1229
+ }
1230
+ await duckConn.runSQL(
1231
+ `SELECT schema_name FROM information_schema.schemata WHERE catalog_name = '${connectionConfig.name}' LIMIT 1`,
1232
+ );
1233
+
1234
+ logger.info(
1235
+ `DuckLake connection test passed: ${connectionConfig.name}`,
1236
+ );
986
1237
  } else {
987
1238
  // Test other connection types using their test() method
988
1239
  await (
@@ -1010,5 +1261,30 @@ export async function testConnectionConfig(
1010
1261
  status: "failed",
1011
1262
  errorMessage: (error as Error).message,
1012
1263
  };
1264
+ } finally {
1265
+ // Cleanup: close all connections and remove ducklake files created during testing
1266
+ if (malloyConnections) {
1267
+ for (const [connName, conn] of malloyConnections) {
1268
+ try {
1269
+ // Close the connection
1270
+ if (
1271
+ conn &&
1272
+ typeof (conn as DuckDBConnection).close === "function"
1273
+ ) {
1274
+ await (conn as DuckDBConnection).close();
1275
+ }
1276
+ } catch (closeError) {
1277
+ logger.warn(
1278
+ `Error closing connection ${connName} during test cleanup`,
1279
+ { error: closeError },
1280
+ );
1281
+ } finally {
1282
+ // Remove ducklake files created during testing (only for ducklake connections)
1283
+ if (connectionConfig.type === "ducklake") {
1284
+ await deleteDuckLakeConnectionFile(connName, process.cwd());
1285
+ }
1286
+ }
1287
+ }
1288
+ }
1013
1289
  }
1014
1290
  }
@@ -397,6 +397,38 @@ export async function getSchemasForConnection(
397
397
  `Failed to get schemas for MotherDuck connection ${connection.name}: ${(error as Error).message}`,
398
398
  );
399
399
  }
400
+ } else if (connection.type === "ducklake") {
401
+ try {
402
+ // Filter by catalog_name to only get schemas from the attached DuckLake catalog
403
+ // The catalog is attached with the connection name (see attachDuckLake in connection.ts)
404
+ const catalogName = connection.name;
405
+ const result = await malloyConnection.runSQL(
406
+ `SELECT schema_name FROM information_schema.schemata WHERE catalog_name = '${catalogName}' ORDER BY schema_name`,
407
+ { rowLimit: 1000 },
408
+ );
409
+ const rows = standardizeRunSQLResult(result);
410
+
411
+ return rows.map((row: unknown) => {
412
+ const typedRow = row as Record<string, unknown>;
413
+ const schemaName = typedRow.schema_name as string;
414
+
415
+ const shouldShow = schemaName === "main" || schemaName === "public";
416
+
417
+ return {
418
+ name: schemaName,
419
+ isHidden: !shouldShow,
420
+ isDefault: false,
421
+ };
422
+ });
423
+ } catch (error) {
424
+ logger.error(
425
+ `Error getting schemas for DuckLake connection ${connection.name}`,
426
+ { error },
427
+ );
428
+ throw new Error(
429
+ `Failed to get schemas for DuckLake connection ${connection.name}: ${(error as Error).message}`,
430
+ );
431
+ }
400
432
  } else {
401
433
  throw new Error(`Unsupported connection type: ${connection.type}`);
402
434
  }
@@ -442,8 +474,13 @@ export async function getTablesForSchema(
442
474
  bucketName,
443
475
  fileKeys,
444
476
  );
477
+ } else if (connection.type === "ducklake") {
478
+ if (schemaName.split(".").length == 2) {
479
+ schemaName = `${connection.name}.${schemaName}`;
480
+ } else if (schemaName.split(".").length === 1) {
481
+ schemaName = `${connection.name}.${schemaName}`;
482
+ }
445
483
  }
446
-
447
484
  const tableNames = await listTablesForSchema(
448
485
  connection,
449
486
  schemaName,
@@ -454,7 +491,6 @@ export async function getTablesForSchema(
454
491
  const tableSourcePromises = tableNames.map(async (tableName) => {
455
492
  try {
456
493
  let tablePath: string;
457
-
458
494
  if (connection.type === "trino") {
459
495
  if (connection.trinoConnection?.catalog) {
460
496
  tablePath = `${connection.trinoConnection?.catalog}.${schemaName}.${tableName}`;
@@ -462,6 +498,10 @@ export async function getTablesForSchema(
462
498
  // Catalog name is included in the schema name
463
499
  tablePath = `${schemaName}.${tableName}`;
464
500
  }
501
+ } else if (connection.type === "ducklake") {
502
+ // For ducklake, schemaName already includes connection name prefix from above
503
+ // So tablePath should be schemaName.tableName (which is connectionName.schemaName.tableName)
504
+ tablePath = `${schemaName}.${tableName}`;
465
505
  } else {
466
506
  tablePath = `${schemaName}.${tableName}`;
467
507
  }
@@ -475,14 +515,13 @@ export async function getTablesForSchema(
475
515
  tableName,
476
516
  tablePath,
477
517
  );
478
-
479
518
  return {
480
519
  resource: tablePath,
481
520
  columns: tableSource.columns,
482
521
  };
483
522
  } catch (error) {
484
523
  logger.warn(`Failed to get schema for table ${tableName}`, {
485
- error,
524
+ error: extractErrorDataFromError(error),
486
525
  schemaName,
487
526
  tableName,
488
527
  });
@@ -795,7 +834,49 @@ export async function listTablesForSchema(
795
834
  `Failed to get tables for MotherDuck schema ${schemaName} in connection ${connection.name}: ${(error as Error).message}`,
796
835
  );
797
836
  }
837
+ } else if (connection.type === "ducklake") {
838
+ const catalogName = schemaName.split(".")[0];
839
+ const actualSchemaName = schemaName.split(".")[1];
840
+ console.error("catalogName", catalogName);
841
+ console.error("actualSchemaName", actualSchemaName);
842
+ try {
843
+ const result = await malloyConnection.runSQL(
844
+ `SELECT table_name FROM information_schema.tables WHERE table_schema = '${actualSchemaName}' AND table_catalog = '${catalogName}' ORDER BY table_name`,
845
+ { rowLimit: 1000 },
846
+ );
847
+ const rows = standardizeRunSQLResult(result);
848
+ return rows.map((row: unknown) => {
849
+ const typedRow = row as Record<string, unknown>;
850
+ return typedRow.table_name as string;
851
+ });
852
+ } catch (error) {
853
+ logger.error(
854
+ `Error getting tables for DuckLake schema ${schemaName} in connection ${connection.name}`,
855
+ { error },
856
+ );
857
+ throw new Error(
858
+ `Failed to get tables for DuckLake schema ${schemaName} in connection ${connection.name}: ${(error as Error).message}`,
859
+ );
860
+ }
798
861
  } else {
799
862
  throw new Error(`Unsupported connection type: ${connection.type}`);
800
863
  }
801
864
  }
865
+
866
+ export function extractErrorDataFromError(error: unknown): {
867
+ error: string;
868
+ stack?: string;
869
+ task?: unknown;
870
+ } {
871
+ const errorMessage = error instanceof Error ? error.message : String(error);
872
+ const errorData: { error: string; stack?: string; task?: unknown } = {
873
+ error: errorMessage,
874
+ };
875
+ if (error instanceof Error && logger.level === "debug") {
876
+ errorData.stack = error.stack;
877
+ }
878
+ if (error && typeof error === "object" && "task" in error) {
879
+ errorData.task = (error as { task?: unknown }).task;
880
+ }
881
+ return errorData;
882
+ }
@@ -10,7 +10,7 @@ import {
10
10
  modelDefToModelInfo,
11
11
  ModelMaterializer,
12
12
  NamedModelObject,
13
- NamedQuery,
13
+ NamedQueryDef,
14
14
  QueryData,
15
15
  QueryMaterializer,
16
16
  Runtime,
@@ -532,7 +532,7 @@ export class Model {
532
532
  ROW_LIMIT;
533
533
  const result = await cell.runnable.run({ rowLimit });
534
534
  const query = (await cell.runnable.getPreparedQuery())._query;
535
- queryName = (query as NamedQuery).as || query.name;
535
+ queryName = (query as NamedQueryDef).as || query.name;
536
536
  queryResult =
537
537
  result?._queryResult &&
538
538
  this.modelInfo &&
@@ -621,11 +621,12 @@ export class Model {
621
621
  modelPath: string,
622
622
  modelDef: ModelDef,
623
623
  ): ApiQuery[] {
624
- const isNamedQuery = (object: NamedModelObject): object is NamedQuery =>
625
- object.type === "query";
624
+ const isNamedQuery = (
625
+ object: NamedModelObject,
626
+ ): object is NamedQueryDef => object.type === "query";
626
627
  return Object.values(modelDef.contents)
627
628
  .filter(isNamedQuery)
628
- .map((queryObj: NamedQuery) => ({
629
+ .map((queryObj: NamedQueryDef) => ({
629
630
  name: queryObj.as || queryObj.name,
630
631
  // What to do when the source is not a string?
631
632
  sourceName:
@@ -633,8 +634,10 @@ export class Model {
633
634
  ? queryObj.structRef
634
635
  : undefined,
635
636
  annotations: queryObj?.annotation?.blockNotes
636
- ?.filter((note) => note.at.url.includes(modelPath))
637
- .map((note) => note.text),
637
+ ?.filter((note: { at: { url: string } }) =>
638
+ note.at.url.includes(modelPath),
639
+ )
640
+ .map((note: { text: string }) => note.text),
638
641
  }));
639
642
  }
640
643
 
@@ -828,7 +831,7 @@ export class Model {
828
831
  let queryInfo: Malloy.QueryInfo | undefined = undefined;
829
832
  try {
830
833
  const preparedQuery = await runnable.getPreparedQuery();
831
- const query = preparedQuery._query as NamedQuery;
834
+ const query = preparedQuery._query as NamedQueryDef;
832
835
  const queryName = query.as || query.name;
833
836
  const anonymousQuery =
834
837
  currentModelInfo.anonymous_queries[
@@ -13,7 +13,11 @@ import {
13
13
  } from "../errors";
14
14
  import { logger } from "../logger";
15
15
  import { URL_READER } from "../utils";
16
- import { createProjectConnections, InternalConnection } from "./connection";
16
+ import {
17
+ createProjectConnections,
18
+ deleteDuckLakeConnectionFile,
19
+ InternalConnection,
20
+ } from "./connection";
17
21
  import { ApiConnection } from "./model";
18
22
  import { Package } from "./package";
19
23
 
@@ -86,12 +90,13 @@ export class Project {
86
90
  logger.info(
87
91
  `Updating ${payload.connections.length} connections for project ${this.projectName}`,
88
92
  );
89
-
93
+ const isUpdateConnectionRequest = true;
90
94
  // Reload connections with full config
91
95
  const { malloyConnections, apiConnections } =
92
96
  await createProjectConnections(
93
97
  payload.connections,
94
98
  this.projectPath,
99
+ isUpdateConnectionRequest,
95
100
  );
96
101
 
97
102
  // Update the project's connection maps
@@ -536,8 +541,11 @@ export class Project {
536
541
  (conn) => conn.name === connectionName,
537
542
  );
538
543
 
539
- if (this.apiConnections[index]?.type === "duckdb") {
544
+ const connectionType = this.apiConnections[index]?.type;
545
+ if (connectionType === "duckdb") {
540
546
  await this.deleteDuckDBConnection(connectionName);
547
+ } else if (connectionType === "ducklake") {
548
+ await this.deleteDuckLakeConnection(connectionName);
541
549
  }
542
550
 
543
551
  if (index !== -1) {
@@ -615,6 +623,15 @@ export class Project {
615
623
  );
616
624
  });
617
625
  }
626
+
627
+ public async deleteDuckLakeConnection(
628
+ connectionName: string,
629
+ ): Promise<void> {
630
+ await deleteDuckLakeConnectionFile(connectionName, this.projectPath);
631
+ logger.info(
632
+ `Removed DuckLake connection ${connectionName} from project ${this.projectName}`,
633
+ );
634
+ }
618
635
  }
619
636
 
620
637
  /**