@malloy-publisher/server 0.0.196-dev → 0.0.196

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 (103) hide show
  1. package/dist/app/api-doc.yaml +213 -214
  2. package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +1 -0
  3. package/dist/app/assets/HomePage-DMop21VG.js +1 -0
  4. package/dist/app/assets/MainPage-BbE8ETz1.js +2 -0
  5. package/dist/app/assets/ModelPage-D2jvfe3t.js +1 -0
  6. package/dist/app/assets/PackagePage-BbnhGoD3.js +1 -0
  7. package/dist/app/assets/{RouteError-DefbDO7F.js → RouteError-D3LGEZ3i.js} +1 -1
  8. package/dist/app/assets/WorkbookPage-DttVIj4u.js +1 -0
  9. package/dist/app/assets/{core-BrfQApxh.es-DnvCX4oH.js → core-w79IMXAG.es-Bd0UlzOL.js} +1 -1
  10. package/dist/app/assets/{index-Bu0ub036.js → index-5K9YjIxF.js} +117 -117
  11. package/dist/app/assets/{index-CkzK3JIl.js → index-C513UodQ.js} +1 -1
  12. package/dist/app/assets/{index-CoA6HIGS.js → index-DIgzgp69.js} +1 -1
  13. package/dist/app/assets/{index.umd-B6Ms2PpL.js → index.umd-BMeMPq_9.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.mjs +1954 -1318
  16. package/package.json +1 -1
  17. package/publisher.config.json +2 -2
  18. package/src/config.spec.ts +181 -66
  19. package/src/config.ts +68 -47
  20. package/src/controller/compile.controller.ts +10 -7
  21. package/src/controller/connection.controller.ts +79 -58
  22. package/src/controller/database.controller.ts +10 -7
  23. package/src/controller/manifest.controller.ts +23 -14
  24. package/src/controller/materialization.controller.ts +14 -14
  25. package/src/controller/model.controller.ts +35 -20
  26. package/src/controller/package.controller.ts +83 -49
  27. package/src/controller/query.controller.ts +11 -8
  28. package/src/controller/watch-mode.controller.ts +35 -29
  29. package/src/errors.ts +2 -2
  30. package/src/mcp/error_messages.ts +2 -2
  31. package/src/mcp/handler_utils.ts +23 -20
  32. package/src/mcp/mcp_constants.ts +1 -1
  33. package/src/mcp/prompts/handlers.ts +3 -3
  34. package/src/mcp/prompts/prompt_service.ts +5 -5
  35. package/src/mcp/prompts/utils.ts +12 -12
  36. package/src/mcp/resource_metadata.ts +3 -3
  37. package/src/mcp/resources/environment_resource.ts +187 -0
  38. package/src/mcp/resources/model_resource.ts +19 -17
  39. package/src/mcp/resources/notebook_resource.ts +13 -13
  40. package/src/mcp/resources/package_resource.ts +30 -27
  41. package/src/mcp/resources/query_resource.ts +15 -10
  42. package/src/mcp/resources/source_resource.ts +10 -10
  43. package/src/mcp/resources/view_resource.ts +11 -11
  44. package/src/mcp/server.ts +16 -14
  45. package/src/mcp/tools/discovery_tools.ts +67 -49
  46. package/src/mcp/tools/execute_query_tool.ts +14 -14
  47. package/src/server-old.ts +1119 -0
  48. package/src/server.ts +191 -159
  49. package/src/service/connection.spec.ts +158 -133
  50. package/src/service/connection.ts +42 -39
  51. package/src/service/connection_config.spec.ts +13 -11
  52. package/src/service/connection_config.ts +28 -19
  53. package/src/service/connection_service.spec.ts +63 -43
  54. package/src/service/connection_service.ts +106 -89
  55. package/src/service/{project.ts → environment.ts} +92 -77
  56. package/src/service/{project_compile.spec.ts → environment_compile.spec.ts} +1 -1
  57. package/src/service/{project_store.spec.ts → environment_store.spec.ts} +99 -85
  58. package/src/service/{project_store.ts → environment_store.ts} +368 -326
  59. package/src/service/manifest_service.spec.ts +15 -15
  60. package/src/service/manifest_service.ts +26 -21
  61. package/src/service/materialization_service.spec.ts +93 -59
  62. package/src/service/materialization_service.ts +71 -62
  63. package/src/service/materialized_table_gc.spec.ts +15 -15
  64. package/src/service/materialized_table_gc.ts +3 -3
  65. package/src/service/model.ts +2 -2
  66. package/src/service/package.spec.ts +2 -2
  67. package/src/service/package.ts +23 -21
  68. package/src/service/resolve_environment.ts +15 -0
  69. package/src/storage/DatabaseInterface.ts +34 -25
  70. package/src/storage/StorageManager.mock.ts +3 -3
  71. package/src/storage/StorageManager.ts +24 -23
  72. package/src/storage/duckdb/ConnectionRepository.ts +13 -11
  73. package/src/storage/duckdb/DuckDBConnection.ts +1 -1
  74. package/src/storage/duckdb/DuckDBManifestStore.ts +6 -6
  75. package/src/storage/duckdb/DuckDBRepository.ts +47 -47
  76. package/src/storage/duckdb/{ProjectRepository.ts → EnvironmentRepository.ts} +35 -35
  77. package/src/storage/duckdb/ManifestRepository.ts +21 -20
  78. package/src/storage/duckdb/MaterializationRepository.ts +31 -28
  79. package/src/storage/duckdb/PackageRepository.ts +11 -11
  80. package/src/storage/duckdb/manifest_store.spec.ts +2 -2
  81. package/src/storage/duckdb/schema.ts +61 -20
  82. package/src/storage/ducklake/DuckLakeManifestStore.ts +14 -14
  83. package/tests/fixtures/publisher.config.json +1 -1
  84. package/tests/harness/e2e.ts +1 -1
  85. package/tests/harness/mcp_test_setup.ts +1 -1
  86. package/tests/harness/mocks.ts +10 -8
  87. package/tests/harness/rest_e2e.ts +2 -2
  88. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +259 -0
  89. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +4 -4
  90. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +27 -48
  91. package/tests/integration/mcp/mcp_resource.integration.spec.ts +26 -35
  92. package/tests/unit/duckdb/attached_databases.test.ts +51 -33
  93. package/tests/unit/duckdb/legacy_schema_migration.test.ts +194 -0
  94. package/tests/unit/ducklake/ducklake.test.ts +24 -22
  95. package/tests/unit/mcp/prompt_happy.test.ts +8 -8
  96. package/dist/app/assets/HomePage-DbZS0N7G.js +0 -1
  97. package/dist/app/assets/MainPage-CBuWkbmr.js +0 -2
  98. package/dist/app/assets/ModelPage-Bt37smot.js +0 -1
  99. package/dist/app/assets/PackagePage-DLZe50WG.js +0 -1
  100. package/dist/app/assets/ProjectPage-FQTEPXP4.js +0 -1
  101. package/dist/app/assets/WorkbookPage-CkAo16ar.js +0 -1
  102. package/src/mcp/resources/project_resource.ts +0 -184
  103. package/src/service/resolve_project.ts +0 -13
@@ -12,7 +12,7 @@ import fs from "fs/promises";
12
12
  import os from "os";
13
13
  import path from "path";
14
14
  import type { components } from "../../../src/api";
15
- import { createProjectConnections } from "../../../src/service/connection";
15
+ import { createEnvironmentConnections } from "../../../src/service/connection";
16
16
 
17
17
  type ApiConnection = components["schemas"]["Connection"];
18
18
 
@@ -355,7 +355,7 @@ describe("DuckDB Attached Databases", () => {
355
355
  });
356
356
  });
357
357
 
358
- describe("createProjectConnections - DuckDB", () => {
358
+ describe("createEnvironmentConnections - DuckDB", () => {
359
359
  const PROJECT_TEST_DIR = path.join(os.tmpdir(), "duckdb-project-tests");
360
360
  let createdConnections: Map<string, unknown> = new Map();
361
361
 
@@ -402,7 +402,7 @@ describe("createProjectConnections - DuckDB", () => {
402
402
  ];
403
403
 
404
404
  const { malloyConnections, apiConnections } =
405
- await createProjectConnections(connections, PROJECT_TEST_DIR);
405
+ await createEnvironmentConnections(connections, PROJECT_TEST_DIR);
406
406
 
407
407
  createdConnections = malloyConnections;
408
408
 
@@ -433,7 +433,7 @@ describe("createProjectConnections - DuckDB", () => {
433
433
  ];
434
434
 
435
435
  const { malloyConnections, apiConnections } =
436
- await createProjectConnections(connections, PROJECT_TEST_DIR);
436
+ await createEnvironmentConnections(connections, PROJECT_TEST_DIR);
437
437
 
438
438
  createdConnections = malloyConnections;
439
439
 
@@ -462,7 +462,7 @@ describe("createProjectConnections - DuckDB", () => {
462
462
  ];
463
463
 
464
464
  const { malloyConnections, apiConnections } =
465
- await createProjectConnections(connections, PROJECT_TEST_DIR);
465
+ await createEnvironmentConnections(connections, PROJECT_TEST_DIR);
466
466
 
467
467
  createdConnections = malloyConnections;
468
468
 
@@ -502,7 +502,7 @@ describe("createProjectConnections - DuckDB", () => {
502
502
  },
503
503
  ];
504
504
 
505
- const { malloyConnections } = await createProjectConnections(
505
+ const { malloyConnections } = await createEnvironmentConnections(
506
506
  connections,
507
507
  PROJECT_TEST_DIR,
508
508
  );
@@ -533,7 +533,7 @@ describe("createProjectConnections - DuckDB", () => {
533
533
  },
534
534
  ];
535
535
 
536
- const { malloyConnections } = await createProjectConnections(
536
+ const { malloyConnections } = await createEnvironmentConnections(
537
537
  connections,
538
538
  PROJECT_TEST_DIR,
539
539
  );
@@ -591,7 +591,7 @@ describe("createProjectConnections - DuckDB", () => {
591
591
  },
592
592
  ];
593
593
 
594
- const { malloyConnections } = await createProjectConnections(
594
+ const { malloyConnections } = await createEnvironmentConnections(
595
595
  connections,
596
596
  PROJECT_TEST_DIR,
597
597
  );
@@ -624,7 +624,7 @@ describe("createProjectConnections - DuckDB", () => {
624
624
  ];
625
625
 
626
626
  await expect(
627
- createProjectConnections(connections, PROJECT_TEST_DIR),
627
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
628
628
  ).rejects.toThrow();
629
629
  });
630
630
 
@@ -637,7 +637,7 @@ describe("createProjectConnections - DuckDB", () => {
637
637
  ] as ApiConnection[];
638
638
 
639
639
  await expect(
640
- createProjectConnections(connections, PROJECT_TEST_DIR),
640
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
641
641
  ).rejects.toThrow("DuckDB connection configuration is missing");
642
642
  });
643
643
 
@@ -658,7 +658,7 @@ describe("createProjectConnections - DuckDB", () => {
658
658
  ];
659
659
 
660
660
  await expect(
661
- createProjectConnections(connections, PROJECT_TEST_DIR),
661
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
662
662
  ).rejects.toThrow("Unsupported database type");
663
663
  });
664
664
 
@@ -682,7 +682,7 @@ describe("createProjectConnections - DuckDB", () => {
682
682
  ];
683
683
 
684
684
  await expect(
685
- createProjectConnections(connections, PROJECT_TEST_DIR),
685
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
686
686
  ).rejects.toThrow("service account key required");
687
687
  });
688
688
 
@@ -704,7 +704,7 @@ describe("createProjectConnections - DuckDB", () => {
704
704
  ] as ApiConnection[];
705
705
 
706
706
  await expect(
707
- createProjectConnections(connections, PROJECT_TEST_DIR),
707
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
708
708
  ).rejects.toThrow("keyId and secret are required");
709
709
  });
710
710
 
@@ -726,7 +726,7 @@ describe("createProjectConnections - DuckDB", () => {
726
726
  ] as ApiConnection[];
727
727
 
728
728
  await expect(
729
- createProjectConnections(connections, PROJECT_TEST_DIR),
729
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
730
730
  ).rejects.toThrow("accessKeyId and secretAccessKey are required");
731
731
  });
732
732
 
@@ -748,7 +748,7 @@ describe("createProjectConnections - DuckDB", () => {
748
748
  ] as ApiConnection[];
749
749
 
750
750
  await expect(
751
- createProjectConnections(connections, PROJECT_TEST_DIR),
751
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
752
752
  ).rejects.toThrow("PostgreSQL connection configuration is required");
753
753
  });
754
754
 
@@ -761,7 +761,7 @@ describe("createProjectConnections - DuckDB", () => {
761
761
  ] as ApiConnection[];
762
762
 
763
763
  await expect(
764
- createProjectConnections(connections, PROJECT_TEST_DIR),
764
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
765
765
  ).rejects.toThrow("DuckLake connection configuration is missing");
766
766
  });
767
767
 
@@ -785,7 +785,7 @@ describe("createProjectConnections - DuckDB", () => {
785
785
  ];
786
786
 
787
787
  await expect(
788
- createProjectConnections(connections, PROJECT_TEST_DIR),
788
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
789
789
  ).rejects.toThrow("username is required");
790
790
  });
791
791
 
@@ -806,7 +806,7 @@ describe("createProjectConnections - DuckDB", () => {
806
806
  ];
807
807
 
808
808
  await expect(
809
- createProjectConnections(connections, PROJECT_TEST_DIR),
809
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
810
810
  ).rejects.toThrow("PostgreSQL connection configuration missing");
811
811
  });
812
812
 
@@ -836,7 +836,7 @@ describe("createProjectConnections - DuckDB", () => {
836
836
  ];
837
837
 
838
838
  await expect(
839
- createProjectConnections(connections, PROJECT_TEST_DIR),
839
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
840
840
  ).rejects.toThrow(
841
841
  "DuckDB attached database names cannot conflict with connection name",
842
842
  );
@@ -854,7 +854,7 @@ describe("createProjectConnections - DuckDB", () => {
854
854
  ];
855
855
 
856
856
  await expect(
857
- createProjectConnections(connections, PROJECT_TEST_DIR),
857
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
858
858
  ).rejects.toThrow("DuckDB connection name cannot be 'duckdb'");
859
859
  });
860
860
 
@@ -884,10 +884,28 @@ describe("createProjectConnections - DuckDB", () => {
884
884
  ];
885
885
 
886
886
  await expect(
887
- createProjectConnections(connections, PROJECT_TEST_DIR),
887
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
888
888
  ).rejects.toThrow("DuckDB connection name cannot be 'duckdb'");
889
889
  });
890
890
 
891
+ it("should throw when DuckDB connection has no attached databases", async () => {
892
+ const connections: ApiConnection[] = [
893
+ {
894
+ name: "no_attached_db",
895
+ type: "duckdb",
896
+ duckdbConnection: {
897
+ attachedDatabases: [],
898
+ },
899
+ },
900
+ ];
901
+
902
+ await expect(
903
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
904
+ ).rejects.toThrow(
905
+ "DuckDB connection must have at least one attached database",
906
+ );
907
+ });
908
+
891
909
  it("should throw on unsupported connection type", async () => {
892
910
  const connections = [
893
911
  {
@@ -897,7 +915,7 @@ describe("createProjectConnections - DuckDB", () => {
897
915
  ] as ApiConnection[];
898
916
 
899
917
  await expect(
900
- createProjectConnections(connections, PROJECT_TEST_DIR),
918
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
901
919
  ).rejects.toThrow("Unsupported connection type");
902
920
  });
903
921
 
@@ -921,13 +939,13 @@ describe("createProjectConnections - DuckDB", () => {
921
939
  ] as ApiConnection[];
922
940
 
923
941
  await expect(
924
- createProjectConnections(connections, PROJECT_TEST_DIR),
942
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
925
943
  ).rejects.toThrow();
926
944
  });
927
945
  });
928
946
  });
929
947
 
930
- describe("createProjectConnections - Other Connection Types", () => {
948
+ describe("createEnvironmentConnections - Other Connection Types", () => {
931
949
  const PROJECT_TEST_DIR = path.join(
932
950
  os.tmpdir(),
933
951
  "connection-validation-tests",
@@ -970,7 +988,7 @@ describe("createProjectConnections - Other Connection Types", () => {
970
988
  ] as ApiConnection[];
971
989
 
972
990
  await expect(
973
- createProjectConnections(connections, PROJECT_TEST_DIR),
991
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
974
992
  ).rejects.toThrow("Snowflake connection configuration is missing");
975
993
  });
976
994
 
@@ -988,7 +1006,7 @@ describe("createProjectConnections - Other Connection Types", () => {
988
1006
  ];
989
1007
 
990
1008
  await expect(
991
- createProjectConnections(connections, PROJECT_TEST_DIR),
1009
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
992
1010
  ).rejects.toThrow("Snowflake account is required");
993
1011
  });
994
1012
 
@@ -1006,7 +1024,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1006
1024
  ];
1007
1025
 
1008
1026
  await expect(
1009
- createProjectConnections(connections, PROJECT_TEST_DIR),
1027
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1010
1028
  ).rejects.toThrow("Snowflake username is required");
1011
1029
  });
1012
1030
 
@@ -1024,7 +1042,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1024
1042
  ];
1025
1043
 
1026
1044
  await expect(
1027
- createProjectConnections(connections, PROJECT_TEST_DIR),
1045
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1028
1046
  ).rejects.toThrow(
1029
1047
  "Snowflake password or private key or private key path is required",
1030
1048
  );
@@ -1044,7 +1062,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1044
1062
  ];
1045
1063
 
1046
1064
  await expect(
1047
- createProjectConnections(connections, PROJECT_TEST_DIR),
1065
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1048
1066
  ).rejects.toThrow("Snowflake warehouse is required");
1049
1067
  });
1050
1068
  });
@@ -1059,7 +1077,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1059
1077
  ] as ApiConnection[];
1060
1078
 
1061
1079
  await expect(
1062
- createProjectConnections(connections, PROJECT_TEST_DIR),
1080
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1063
1081
  ).rejects.toThrow("Trino connection configuration is missing");
1064
1082
  });
1065
1083
 
@@ -1076,7 +1094,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1076
1094
  ];
1077
1095
 
1078
1096
  await expect(
1079
- createProjectConnections(connections, PROJECT_TEST_DIR),
1097
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1080
1098
  ).rejects.toThrow(
1081
1099
  'Invalid Trino connection: expected "http://server:port" or "https://server:port"',
1082
1100
  );
@@ -1093,7 +1111,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1093
1111
  ] as ApiConnection[];
1094
1112
 
1095
1113
  await expect(
1096
- createProjectConnections(connections, PROJECT_TEST_DIR),
1114
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1097
1115
  ).rejects.toThrow("MotherDuck connection configuration is missing");
1098
1116
  });
1099
1117
 
@@ -1107,7 +1125,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1107
1125
  ];
1108
1126
 
1109
1127
  await expect(
1110
- createProjectConnections(connections, PROJECT_TEST_DIR),
1128
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1111
1129
  ).rejects.toThrow("MotherDuck access token is required");
1112
1130
  });
1113
1131
  });
@@ -0,0 +1,194 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ // TODO: Remove this during projects cleanup
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
6
+ import fs from "fs/promises";
7
+ import os from "os";
8
+ import path from "path";
9
+ import { DuckDBConnection } from "../../../src/storage/duckdb/DuckDBConnection";
10
+ import { initializeSchema } from "../../../src/storage/duckdb/schema";
11
+
12
+ const TEST_DB_DIR = path.join(os.tmpdir(), "duckdb-legacy-migration-tests");
13
+
14
+ // Seed a pre-rename schema on an *already-open* connection. We deliberately
15
+ // avoid opening, closing, and reopening the same DuckDB file within a test:
16
+ // on Windows runners the second `duckdb.Database(path)` call sometimes fails
17
+ // with `Invalid Error` because the OS hasn't released the file handle yet.
18
+ // Sharing one connection between seed and assertion sidesteps that entirely.
19
+ async function seedLegacySchema(db: DuckDBConnection): Promise<void> {
20
+ // Seed a pre-rename schema: parent table named `projects` and child
21
+ // tables with `project_id` foreign-key columns. Mirrors what an existing
22
+ // installation looked like before the projects→environments rename.
23
+ await db.run(`
24
+ CREATE TABLE projects (
25
+ id VARCHAR PRIMARY KEY,
26
+ name VARCHAR NOT NULL UNIQUE,
27
+ path VARCHAR NOT NULL,
28
+ description VARCHAR,
29
+ metadata JSON,
30
+ created_at TIMESTAMP NOT NULL,
31
+ updated_at TIMESTAMP NOT NULL
32
+ )
33
+ `);
34
+ await db.run(`
35
+ CREATE TABLE packages (
36
+ id VARCHAR PRIMARY KEY,
37
+ project_id VARCHAR NOT NULL,
38
+ name VARCHAR NOT NULL,
39
+ description VARCHAR,
40
+ manifest_path VARCHAR NOT NULL,
41
+ metadata JSON,
42
+ created_at TIMESTAMP NOT NULL,
43
+ updated_at TIMESTAMP NOT NULL,
44
+ FOREIGN KEY (project_id) REFERENCES projects(id)
45
+ )
46
+ `);
47
+ await db.run(`
48
+ CREATE TABLE connections (
49
+ id VARCHAR PRIMARY KEY,
50
+ project_id VARCHAR NOT NULL,
51
+ name VARCHAR NOT NULL,
52
+ type VARCHAR NOT NULL,
53
+ config JSON NOT NULL,
54
+ created_at TIMESTAMP NOT NULL,
55
+ updated_at TIMESTAMP NOT NULL,
56
+ FOREIGN KEY (project_id) REFERENCES projects(id)
57
+ )
58
+ `);
59
+ await db.run(`
60
+ CREATE TABLE materializations (
61
+ id VARCHAR PRIMARY KEY,
62
+ project_id VARCHAR NOT NULL,
63
+ package_name VARCHAR NOT NULL,
64
+ status VARCHAR NOT NULL,
65
+ active_key VARCHAR,
66
+ started_at TIMESTAMP,
67
+ completed_at TIMESTAMP,
68
+ error TEXT,
69
+ metadata JSON,
70
+ created_at TIMESTAMP NOT NULL,
71
+ updated_at TIMESTAMP NOT NULL,
72
+ FOREIGN KEY (project_id) REFERENCES projects(id)
73
+ )
74
+ `);
75
+ await db.run(`
76
+ CREATE TABLE build_manifests (
77
+ id VARCHAR PRIMARY KEY,
78
+ project_id VARCHAR NOT NULL,
79
+ package_name VARCHAR NOT NULL,
80
+ build_id VARCHAR NOT NULL,
81
+ table_name VARCHAR NOT NULL,
82
+ source_name VARCHAR NOT NULL,
83
+ connection_name VARCHAR NOT NULL,
84
+ created_at TIMESTAMP NOT NULL,
85
+ updated_at TIMESTAMP NOT NULL,
86
+ FOREIGN KEY (project_id) REFERENCES projects(id)
87
+ )
88
+ `);
89
+
90
+ await db.run(
91
+ `INSERT INTO projects VALUES ('p1', 'proj-one', '/p1', 'd1', NULL,
92
+ TIMESTAMP '2024-01-01 00:00:00', TIMESTAMP '2024-01-01 00:00:00')`,
93
+ );
94
+ await db.run(
95
+ `INSERT INTO packages VALUES ('pkg1', 'p1', 'pkg-one', NULL, '/m', NULL,
96
+ TIMESTAMP '2024-01-01 00:00:00', TIMESTAMP '2024-01-01 00:00:00')`,
97
+ );
98
+ }
99
+
100
+ describe("DuckDB legacy projects schema cleanup", () => {
101
+ beforeEach(async () => {
102
+ await fs.mkdir(TEST_DB_DIR, { recursive: true });
103
+ });
104
+
105
+ afterEach(async () => {
106
+ try {
107
+ await fs.rm(TEST_DB_DIR, { recursive: true, force: true });
108
+ } catch {
109
+ // ignore
110
+ }
111
+ });
112
+
113
+ it("drops legacy projects schema and creates the new environments schema cleanly", async () => {
114
+ const dbPath = path.join(TEST_DB_DIR, "legacy.duckdb");
115
+ const db = new DuckDBConnection(dbPath);
116
+ await db.initialize();
117
+
118
+ // Seed the legacy schema on the same connection, then run the
119
+ // production schema-init path. This mirrors a server upgrade
120
+ // (legacy data on disk, new code starting up) without forcing a
121
+ // close+reopen, which is unreliable on Windows runners.
122
+ await seedLegacySchema(db);
123
+ await initializeSchema(db);
124
+
125
+ // Legacy parent table is gone.
126
+ const legacyProjects = await db.all<{ name: string }>(
127
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='projects'",
128
+ );
129
+ expect(legacyProjects.length).toBe(0);
130
+
131
+ // New `environments` table exists and is empty (legacy data dropped).
132
+ const envs = await db.all<{ id: string }>("SELECT id FROM environments");
133
+ expect(envs.length).toBe(0);
134
+
135
+ // Child tables are queryable by `environment_id` (the new column),
136
+ // proving they were recreated with the new schema rather than left on
137
+ // the old `project_id` column.
138
+ const pkgs = await db.all<{ id: string }>(
139
+ "SELECT id FROM packages WHERE environment_id = ?",
140
+ ["p1"],
141
+ );
142
+ expect(pkgs.length).toBe(0);
143
+ const conns = await db.all<{ id: string }>(
144
+ "SELECT id FROM connections WHERE environment_id = ?",
145
+ ["p1"],
146
+ );
147
+ expect(conns.length).toBe(0);
148
+ const mats = await db.all<{ id: string }>(
149
+ "SELECT id FROM materializations WHERE environment_id = ?",
150
+ ["p1"],
151
+ );
152
+ expect(mats.length).toBe(0);
153
+ const manifests = await db.all<{ id: string }>(
154
+ "SELECT id FROM build_manifests WHERE environment_id = ?",
155
+ ["p1"],
156
+ );
157
+ expect(manifests.length).toBe(0);
158
+
159
+ await db.close();
160
+ });
161
+
162
+ it("is idempotent: running initializeSchema twice on a migrated DB is a no-op", async () => {
163
+ const dbPath = path.join(TEST_DB_DIR, "legacy_idempotent.duckdb");
164
+ const db = new DuckDBConnection(dbPath);
165
+ await db.initialize();
166
+
167
+ await seedLegacySchema(db);
168
+ await initializeSchema(db);
169
+ // Second call should hit the early-return path (isInitialized() === true).
170
+ await initializeSchema(db);
171
+
172
+ const envs = await db.all<{ id: string }>("SELECT id FROM environments");
173
+ expect(envs.length).toBe(0);
174
+
175
+ await db.close();
176
+ });
177
+
178
+ it("creates a fresh schema unchanged when no legacy projects table is present", async () => {
179
+ const dbPath = path.join(TEST_DB_DIR, "fresh.duckdb");
180
+ const db = new DuckDBConnection(dbPath);
181
+ await db.initialize();
182
+ await initializeSchema(db);
183
+
184
+ const envs = await db.all<{ id: string }>("SELECT id FROM environments");
185
+ expect(envs.length).toBe(0);
186
+
187
+ const legacy = await db.all<{ name: string }>(
188
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='projects'",
189
+ );
190
+ expect(legacy.length).toBe(0);
191
+
192
+ await db.close();
193
+ });
194
+ });
@@ -4,7 +4,7 @@ import fs from "fs/promises";
4
4
  import path from "path";
5
5
  import { components } from "../../../src/api";
6
6
  import {
7
- createProjectConnections,
7
+ createEnvironmentConnections,
8
8
  deleteDuckLakeConnectionFile,
9
9
  testConnectionConfig,
10
10
  } from "../../../src/service/connection";
@@ -143,7 +143,7 @@ describe("DuckLake Connection Tests", () => {
143
143
  };
144
144
 
145
145
  const { malloyConnections, apiConnections } =
146
- await createProjectConnections(
146
+ await createEnvironmentConnections(
147
147
  [ducklakeConnection],
148
148
  testProjectPath,
149
149
  );
@@ -212,7 +212,7 @@ describe("DuckLake Connection Tests", () => {
212
212
  },
213
213
  };
214
214
 
215
- const { malloyConnections } = await createProjectConnections(
215
+ const { malloyConnections } = await createEnvironmentConnections(
216
216
  [ducklakeConnection],
217
217
  testProjectPath,
218
218
  );
@@ -268,7 +268,7 @@ describe("DuckLake Connection Tests", () => {
268
268
  },
269
269
  };
270
270
 
271
- const { malloyConnections } = await createProjectConnections(
271
+ const { malloyConnections } = await createEnvironmentConnections(
272
272
  [ducklakeConnection],
273
273
  testProjectPath,
274
274
  );
@@ -333,7 +333,7 @@ describe("DuckLake Connection Tests", () => {
333
333
  },
334
334
  };
335
335
 
336
- const { malloyConnections } = await createProjectConnections(
336
+ const { malloyConnections } = await createEnvironmentConnections(
337
337
  [ducklakeConnection],
338
338
  testProjectPath,
339
339
  );
@@ -400,7 +400,7 @@ describe("DuckLake Connection Tests", () => {
400
400
  },
401
401
  };
402
402
 
403
- const { malloyConnections } = await createProjectConnections(
403
+ const { malloyConnections } = await createEnvironmentConnections(
404
404
  [ducklakeConnection],
405
405
  testProjectPath,
406
406
  );
@@ -470,7 +470,7 @@ describe("DuckLake Connection Tests", () => {
470
470
  },
471
471
  };
472
472
 
473
- const { malloyConnections } = await createProjectConnections(
473
+ const { malloyConnections } = await createEnvironmentConnections(
474
474
  [ducklakeConnection],
475
475
  testProjectPath,
476
476
  );
@@ -529,7 +529,7 @@ describe("DuckLake Connection Tests", () => {
529
529
  },
530
530
  };
531
531
 
532
- const { malloyConnections } = await createProjectConnections(
532
+ const { malloyConnections } = await createEnvironmentConnections(
533
533
  [ducklakeConnection],
534
534
  testProjectPath,
535
535
  );
@@ -553,7 +553,7 @@ describe("DuckLake Connection Tests", () => {
553
553
  describe("Error Handling", () => {
554
554
  it("should throw error if DuckLake catalog connection is missing", async () => {
555
555
  await expect(
556
- createProjectConnections(
556
+ createEnvironmentConnections(
557
557
  [
558
558
  {
559
559
  name: "ducklake_no_catalog",
@@ -576,7 +576,7 @@ describe("DuckLake Connection Tests", () => {
576
576
 
577
577
  it("should throw error if DuckLake storage bucketUrl is missing", async () => {
578
578
  await expect(
579
- createProjectConnections(
579
+ createEnvironmentConnections(
580
580
  [
581
581
  {
582
582
  name: "ducklake_no_bucket",
@@ -607,7 +607,7 @@ describe("DuckLake Connection Tests", () => {
607
607
 
608
608
  it("should throw error if DuckLake connection config is missing", async () => {
609
609
  await expect(
610
- createProjectConnections(
610
+ createEnvironmentConnections(
611
611
  [
612
612
  {
613
613
  name: "ducklake_missing_config",
@@ -653,19 +653,21 @@ describe("DuckLake Connection Tests", () => {
653
653
  };
654
654
 
655
655
  // Create connection twice - second should handle already attached gracefully
656
- const { malloyConnections: conn1 } = await createProjectConnections(
657
- [ducklakeConnection],
658
- testProjectPath,
659
- );
656
+ const { malloyConnections: conn1 } =
657
+ await createEnvironmentConnections(
658
+ [ducklakeConnection],
659
+ testProjectPath,
660
+ );
660
661
  const connection1 = conn1.get(
661
662
  "ducklake_duplicate_test",
662
663
  ) as DuckDBConnection;
663
664
  createdConnections.push(connection1);
664
665
 
665
- const { malloyConnections: conn2 } = await createProjectConnections(
666
- [ducklakeConnection],
667
- testProjectPath,
668
- );
666
+ const { malloyConnections: conn2 } =
667
+ await createEnvironmentConnections(
668
+ [ducklakeConnection],
669
+ testProjectPath,
670
+ );
669
671
  const connection2 = conn2.get(
670
672
  "ducklake_duplicate_test",
671
673
  ) as DuckDBConnection;
@@ -714,7 +716,7 @@ describe("DuckLake Connection Tests", () => {
714
716
  },
715
717
  };
716
718
 
717
- const { malloyConnections } = await createProjectConnections(
719
+ const { malloyConnections } = await createEnvironmentConnections(
718
720
  [ducklakeConnection],
719
721
  testProjectPath,
720
722
  );
@@ -771,7 +773,7 @@ describe("DuckLake Connection Tests", () => {
771
773
  },
772
774
  };
773
775
 
774
- const { malloyConnections } = await createProjectConnections(
776
+ const { malloyConnections } = await createEnvironmentConnections(
775
777
  [ducklakeConnection],
776
778
  testProjectPath,
777
779
  );
@@ -903,7 +905,7 @@ describe("DuckLake Connection Tests", () => {
903
905
  return;
904
906
  }
905
907
 
906
- const { apiConnections } = await createProjectConnections(
908
+ const { apiConnections } = await createEnvironmentConnections(
907
909
  [
908
910
  {
909
911
  name: "ducklake_attrs_test",