@malloy-publisher/server 0.0.204 → 0.0.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.ts +10 -1
- package/dist/app/api-doc.yaml +494 -397
- package/dist/app/assets/{EnvironmentPage-CX06cjOF.js → EnvironmentPage-BYwBeC2F.js} +1 -1
- package/dist/app/assets/HomePage-ivu4vdpj.js +1 -0
- package/dist/app/assets/{MainPage-nUJ9YatG.js → MainPage-B2DnHEDU.js} +2 -2
- package/dist/app/assets/MaterializationsPage-BZEuwF9P.js +1 -0
- package/dist/app/assets/ModelPage-Dpu3bfPg.js +1 -0
- package/dist/app/assets/{PackagePage-BaEVdEAG.js → PackagePage-B8PwcRHt.js} +1 -1
- package/dist/app/assets/{RouteError-BShQjZio.js → RouteError-BhbywAeC.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CBn6ZjJW.js → WorkbookPage-C-JXsJG0.js} +1 -1
- package/dist/app/assets/{core-DECXYL4E.es-OaRfXwuQ.js → core-pPlPr7jK.es-CNEOlxKB.js} +1 -1
- package/dist/app/assets/{index-BLfPC1gy.js → index-BHEm8Egc.js} +1 -1
- package/dist/app/assets/{index-Dy3YhAZQ.js → index-BsvDrV14.js} +1 -1
- package/dist/app/assets/{index-DqiJ0bWp.js → index-ChR1fKR2.js} +134 -134
- package/dist/app/assets/{index.umd-DAN9K8yC.js → index.umd-BVLPYNuj.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +18 -8
- package/dist/package_load_worker.mjs +19 -2
- package/dist/runtime/publisher.js +318 -0
- package/dist/server.mjs +1703 -1443
- package/package.json +5 -4
- package/scripts/bake-duckdb-extensions.js +104 -0
- package/src/constants.ts +12 -0
- package/src/controller/materialization.controller.ts +79 -35
- package/src/controller/package.controller.spec.ts +179 -0
- package/src/controller/package.controller.ts +60 -73
- package/src/controller/watch-mode.controller.ts +176 -46
- package/src/dto/package.dto.ts +16 -1
- package/src/errors.spec.ts +33 -0
- package/src/errors.ts +18 -0
- package/src/health.spec.ts +34 -1
- package/src/instrumentation.ts +33 -17
- package/src/materialization_metrics.ts +66 -0
- package/src/mcp/error_messages.spec.ts +35 -0
- package/src/mcp/error_messages.ts +14 -1
- package/src/mcp/handler_utils.ts +12 -0
- package/src/package_load/package_load_pool.ts +7 -1
- package/src/package_load/package_load_worker.ts +44 -4
- package/src/package_load/protocol.ts +7 -1
- package/src/runtime/publisher.js +318 -0
- package/src/server-old.ts +7 -149
- package/src/server.ts +488 -190
- package/src/service/authorize_integration.spec.ts +163 -2
- package/src/service/compile_authorize.spec.ts +85 -0
- package/src/service/environment.ts +270 -12
- package/src/service/environment_store.spec.ts +0 -81
- package/src/service/environment_store.ts +142 -34
- package/src/service/explore_visibility.spec.ts +434 -0
- package/src/service/exports_probe.spec.ts +107 -0
- package/src/service/manifest_loader.spec.ts +99 -0
- package/src/service/manifest_loader.ts +95 -0
- package/src/service/materialization_service.spec.ts +324 -512
- package/src/service/materialization_service.ts +816 -656
- package/src/service/model.ts +444 -13
- package/src/service/package.spec.ts +14 -2
- package/src/service/package.ts +271 -20
- package/src/service/package_rollback.spec.ts +190 -0
- package/src/service/package_worker_path.spec.ts +223 -0
- package/src/service/query_boundary.spec.ts +470 -0
- package/src/storage/DatabaseInterface.ts +35 -57
- package/src/storage/StorageManager.mock.ts +0 -9
- package/src/storage/StorageManager.ts +7 -290
- package/src/storage/duckdb/DuckDBConnection.ts +70 -124
- package/src/storage/duckdb/DuckDBRepository.ts +2 -35
- package/src/storage/duckdb/MaterializationRepository.ts +52 -27
- package/src/storage/duckdb/schema.ts +4 -20
- package/tests/fixtures/authorize-compile/model.malloy +9 -0
- package/tests/fixtures/authorize-compile/publisher.json +4 -0
- package/tests/fixtures/html-pages-nopublic/model.malloy +1 -0
- package/tests/fixtures/html-pages-nopublic/publisher.json +5 -0
- package/tests/fixtures/html-pages-test/data.csv +3 -0
- package/tests/fixtures/html-pages-test/public/assets/app.css +3 -0
- package/tests/fixtures/html-pages-test/public/data.json +1 -0
- package/tests/fixtures/html-pages-test/public/index.html +9 -0
- package/tests/fixtures/html-pages-test/public/sub/page2.html +9 -0
- package/tests/fixtures/html-pages-test/publisher.json +5 -0
- package/tests/fixtures/html-pages-test/report.malloy +1 -0
- package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +92 -0
- package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +138 -0
- package/tests/integration/html_pages/html_pages.integration.spec.ts +378 -0
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +175 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +277 -266
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +421 -0
- package/tests/unit/duckdb/attached_databases.test.ts +111 -0
- package/tests/unit/duckdb/duckdb_connection.test.ts +181 -0
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +7 -4
- package/tests/unit/duckdb/repositories.test.ts +208 -0
- package/dist/app/assets/HomePage-CNFt_eUU.js +0 -1
- package/dist/app/assets/MaterializationsPage-B5goxVXW.js +0 -1
- package/dist/app/assets/ModelPage-Ba7Xh4lL.js +0 -1
- package/src/controller/manifest.controller.ts +0 -38
- package/src/service/manifest_service.spec.ts +0 -206
- package/src/service/manifest_service.ts +0 -117
- package/src/service/materialized_table_gc.spec.ts +0 -384
- package/src/service/materialized_table_gc.ts +0 -231
- package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
- package/src/storage/duckdb/ManifestRepository.ts +0 -120
- package/src/storage/duckdb/manifest_store.spec.ts +0 -133
- package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -155
- package/tests/unit/storage/StorageManager.test.ts +0 -166
|
@@ -1,41 +1,77 @@
|
|
|
1
|
-
import type { Connection } from "@malloydata/malloy";
|
|
2
1
|
import { beforeEach, describe, expect, it } from "bun:test";
|
|
3
2
|
import * as sinon from "sinon";
|
|
4
3
|
import {
|
|
4
|
+
BadRequestError,
|
|
5
5
|
EnvironmentNotFoundError,
|
|
6
6
|
InvalidStateTransitionError,
|
|
7
7
|
MaterializationConflictError,
|
|
8
8
|
MaterializationNotFoundError,
|
|
9
9
|
} from "../errors";
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
BuildInstruction,
|
|
12
|
+
BuildPlan,
|
|
12
13
|
Materialization,
|
|
13
|
-
MaterializationStatus,
|
|
14
14
|
ResourceRepository,
|
|
15
15
|
} from "../storage/DatabaseInterface";
|
|
16
16
|
import { DuplicateActiveMaterializationError } from "../storage/duckdb/MaterializationRepository";
|
|
17
17
|
import { EnvironmentStore } from "./environment_store";
|
|
18
|
-
import { ManifestService } from "./manifest_service";
|
|
19
18
|
import {
|
|
20
|
-
manifestTableKey,
|
|
21
19
|
MaterializationService,
|
|
22
|
-
|
|
20
|
+
stagingSuffix,
|
|
23
21
|
} from "./materialization_service";
|
|
24
22
|
|
|
25
|
-
function
|
|
23
|
+
function makeMaterialization(
|
|
26
24
|
overrides: Partial<Materialization> = {},
|
|
27
25
|
): Materialization {
|
|
28
26
|
return {
|
|
29
|
-
id: "
|
|
30
|
-
environmentId: "
|
|
27
|
+
id: "mat-1",
|
|
28
|
+
environmentId: "env-1",
|
|
31
29
|
packageName: "pkg",
|
|
30
|
+
pauseBetweenPhases: false,
|
|
32
31
|
status: "PENDING",
|
|
32
|
+
buildPlan: null,
|
|
33
|
+
manifest: null,
|
|
33
34
|
startedAt: null,
|
|
34
35
|
completedAt: null,
|
|
35
36
|
error: null,
|
|
36
37
|
metadata: null,
|
|
37
|
-
createdAt: new Date("
|
|
38
|
-
updatedAt: new Date("
|
|
38
|
+
createdAt: new Date("2026-01-01"),
|
|
39
|
+
updatedAt: new Date("2026-01-01"),
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeBuildPlan(overrides: Partial<BuildPlan> = {}): BuildPlan {
|
|
45
|
+
return {
|
|
46
|
+
graphs: [
|
|
47
|
+
{
|
|
48
|
+
connectionName: "duckdb",
|
|
49
|
+
nodes: [[{ sourceID: "orders@m.malloy", dependsOn: [] }]],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
sources: {
|
|
53
|
+
"orders@m.malloy": {
|
|
54
|
+
name: "orders",
|
|
55
|
+
sourceID: "orders@m.malloy",
|
|
56
|
+
connectionName: "duckdb",
|
|
57
|
+
dialect: "duckdb",
|
|
58
|
+
buildId: "build-orders",
|
|
59
|
+
sql: "SELECT 1",
|
|
60
|
+
columns: [],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeInstruction(
|
|
68
|
+
overrides: Partial<BuildInstruction> = {},
|
|
69
|
+
): BuildInstruction {
|
|
70
|
+
return {
|
|
71
|
+
buildId: "build-orders",
|
|
72
|
+
materializedTableId: "mt-1",
|
|
73
|
+
physicalTableName: '"orders_v1"',
|
|
74
|
+
realization: "COPY",
|
|
39
75
|
...overrides,
|
|
40
76
|
};
|
|
41
77
|
}
|
|
@@ -45,69 +81,40 @@ type MockRepo = sinon.SinonStubbedInstance<ResourceRepository>;
|
|
|
45
81
|
function createMocks() {
|
|
46
82
|
const sandbox = sinon.createSandbox();
|
|
47
83
|
|
|
48
|
-
const repository
|
|
49
|
-
listEnvironments: sandbox.stub(),
|
|
50
|
-
getEnvironmentById: sandbox.stub(),
|
|
84
|
+
const repository = {
|
|
51
85
|
getEnvironmentByName: sandbox.stub(),
|
|
52
|
-
createEnvironment: sandbox.stub(),
|
|
53
|
-
updateEnvironment: sandbox.stub(),
|
|
54
|
-
deleteEnvironment: sandbox.stub(),
|
|
55
|
-
listPackages: sandbox.stub(),
|
|
56
|
-
getPackageById: sandbox.stub(),
|
|
57
|
-
getPackageByName: sandbox.stub(),
|
|
58
|
-
createPackage: sandbox.stub(),
|
|
59
|
-
updatePackage: sandbox.stub(),
|
|
60
|
-
deletePackage: sandbox.stub(),
|
|
61
|
-
listConnections: sandbox.stub(),
|
|
62
|
-
getConnectionById: sandbox.stub(),
|
|
63
|
-
getConnectionByName: sandbox.stub(),
|
|
64
|
-
createConnection: sandbox.stub(),
|
|
65
|
-
updateConnection: sandbox.stub(),
|
|
66
|
-
deleteConnection: sandbox.stub(),
|
|
67
86
|
listMaterializations: sandbox.stub(),
|
|
68
87
|
getMaterializationById: sandbox.stub(),
|
|
69
88
|
getActiveMaterialization: sandbox.stub(),
|
|
70
89
|
createMaterialization: sandbox.stub(),
|
|
71
90
|
updateMaterialization: sandbox.stub(),
|
|
72
91
|
deleteMaterialization: sandbox.stub(),
|
|
73
|
-
listManifestEntries: sandbox.stub(),
|
|
74
|
-
upsertManifestEntry: sandbox.stub(),
|
|
75
|
-
deleteManifestEntry: sandbox.stub(),
|
|
76
92
|
} as unknown as MockRepo;
|
|
77
93
|
|
|
78
|
-
const storageManager = {
|
|
79
|
-
getRepository: () => repository,
|
|
80
|
-
getManifestStore: sandbox.stub(),
|
|
81
|
-
};
|
|
94
|
+
const storageManager = { getRepository: () => repository };
|
|
82
95
|
|
|
83
96
|
const environmentStore = {
|
|
84
97
|
storageManager,
|
|
85
98
|
getEnvironment: sandbox.stub(),
|
|
86
99
|
} as unknown as EnvironmentStore;
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
getManifest: sandbox.stub().resolves({ entries: {}, strict: false }),
|
|
90
|
-
writeEntry: sandbox.stub().resolves(),
|
|
91
|
-
deleteEntry: sandbox.stub().resolves(),
|
|
92
|
-
reloadManifest: sandbox.stub().resolves({ entries: {}, strict: false }),
|
|
93
|
-
listEntries: sandbox.stub().resolves([]),
|
|
94
|
-
} as unknown as sinon.SinonStubbedInstance<ManifestService>;
|
|
95
|
-
|
|
96
|
-
const service = new MaterializationService(
|
|
97
|
-
environmentStore,
|
|
98
|
-
manifestService as unknown as ManifestService,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Default: resolveEnvironmentId succeeds
|
|
101
|
+
// Default: environment resolves and yields a package.
|
|
102
102
|
repository.getEnvironmentByName.resolves({
|
|
103
|
-
id: "
|
|
104
|
-
name: "my-
|
|
103
|
+
id: "env-1",
|
|
104
|
+
name: "my-env",
|
|
105
105
|
path: "/test",
|
|
106
106
|
createdAt: new Date(),
|
|
107
107
|
updatedAt: new Date(),
|
|
108
108
|
});
|
|
109
|
+
(environmentStore.getEnvironment as sinon.SinonStub).resolves({
|
|
110
|
+
getPackage: sinon.stub().resolves({}),
|
|
111
|
+
withPackageLock: async (_name: string, fn: () => Promise<unknown>) =>
|
|
112
|
+
fn(),
|
|
113
|
+
});
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
const service = new MaterializationService(environmentStore);
|
|
116
|
+
|
|
117
|
+
return { sandbox, repository, environmentStore, service };
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
describe("MaterializationService", () => {
|
|
@@ -117,572 +124,377 @@ describe("MaterializationService", () => {
|
|
|
117
124
|
ctx = createMocks();
|
|
118
125
|
});
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
describe("resolveEnvironmentId (via listMaterializations)", () => {
|
|
123
|
-
it("should throw EnvironmentNotFoundError when environment is not in DB", async () => {
|
|
127
|
+
describe("resolveEnvironmentId", () => {
|
|
128
|
+
it("throws EnvironmentNotFoundError when the environment is missing", async () => {
|
|
124
129
|
ctx.repository.getEnvironmentByName.resolves(null);
|
|
125
|
-
|
|
126
130
|
await expect(
|
|
127
131
|
ctx.service.listMaterializations("unknown", "pkg"),
|
|
128
132
|
).rejects.toThrow(EnvironmentNotFoundError);
|
|
129
133
|
});
|
|
130
134
|
});
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"my-
|
|
141
|
-
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
expect(result).toEqual(builds);
|
|
136
|
+
describe("queries", () => {
|
|
137
|
+
it("lists materializations for a package", async () => {
|
|
138
|
+
const rows = [
|
|
139
|
+
makeMaterialization(),
|
|
140
|
+
makeMaterialization({ id: "m2" }),
|
|
141
|
+
];
|
|
142
|
+
ctx.repository.listMaterializations.resolves(rows);
|
|
143
|
+
expect(
|
|
144
|
+
await ctx.service.listMaterializations("my-env", "pkg"),
|
|
145
|
+
).toEqual(rows);
|
|
145
146
|
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("getMaterialization", () => {
|
|
149
|
-
it("should return a specific build", async () => {
|
|
150
|
-
const exec = makeExecution();
|
|
151
|
-
ctx.repository.getMaterializationById.resolves(exec);
|
|
152
147
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect(result).toEqual(exec);
|
|
148
|
+
it("returns a specific materialization", async () => {
|
|
149
|
+
const m = makeMaterialization();
|
|
150
|
+
ctx.repository.getMaterializationById.resolves(m);
|
|
151
|
+
expect(
|
|
152
|
+
await ctx.service.getMaterialization("my-env", "pkg", "mat-1"),
|
|
153
|
+
).toEqual(m);
|
|
160
154
|
});
|
|
161
155
|
|
|
162
|
-
it("
|
|
156
|
+
it("throws when the materialization is missing", async () => {
|
|
163
157
|
ctx.repository.getMaterializationById.resolves(null);
|
|
164
|
-
|
|
165
158
|
await expect(
|
|
166
|
-
ctx.service.getMaterialization("my-
|
|
159
|
+
ctx.service.getMaterialization("my-env", "pkg", "nope"),
|
|
167
160
|
).rejects.toThrow(MaterializationNotFoundError);
|
|
168
161
|
});
|
|
169
162
|
|
|
170
|
-
it("
|
|
163
|
+
it("throws when the materialization belongs to another package", async () => {
|
|
171
164
|
ctx.repository.getMaterializationById.resolves(
|
|
172
|
-
|
|
165
|
+
makeMaterialization({ packageName: "other" }),
|
|
173
166
|
);
|
|
174
|
-
|
|
175
167
|
await expect(
|
|
176
|
-
ctx.service.getMaterialization("my-
|
|
168
|
+
ctx.service.getMaterialization("my-env", "pkg", "mat-1"),
|
|
177
169
|
).rejects.toThrow(MaterializationNotFoundError);
|
|
178
170
|
});
|
|
179
171
|
});
|
|
180
172
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
describe("state transitions", () => {
|
|
184
|
-
const validTransitions: [MaterializationStatus, MaterializationStatus][] =
|
|
185
|
-
[
|
|
186
|
-
["PENDING", "RUNNING"],
|
|
187
|
-
["PENDING", "CANCELLED"],
|
|
188
|
-
["RUNNING", "SUCCESS"],
|
|
189
|
-
["RUNNING", "FAILED"],
|
|
190
|
-
["RUNNING", "CANCELLED"],
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
const invalidTransitions: [
|
|
194
|
-
MaterializationStatus,
|
|
195
|
-
MaterializationStatus,
|
|
196
|
-
][] = [
|
|
197
|
-
["PENDING", "SUCCESS"],
|
|
198
|
-
["PENDING", "FAILED"],
|
|
199
|
-
["RUNNING", "PENDING"],
|
|
200
|
-
["SUCCESS", "RUNNING"],
|
|
201
|
-
["SUCCESS", "FAILED"],
|
|
202
|
-
["FAILED", "RUNNING"],
|
|
203
|
-
["FAILED", "SUCCESS"],
|
|
204
|
-
["CANCELLED", "RUNNING"],
|
|
205
|
-
["CANCELLED", "SUCCESS"],
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
for (const [from, to] of validTransitions) {
|
|
209
|
-
it(`should allow ${from} -> ${to}`, async () => {
|
|
210
|
-
const exec = makeExecution({ status: from });
|
|
211
|
-
ctx.repository.getMaterializationById.resolves(exec);
|
|
212
|
-
ctx.repository.updateMaterialization.resolves(
|
|
213
|
-
makeExecution({ status: to }),
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// Trigger via stopMaterialization for RUNNING->CANCELLED (orphaned path)
|
|
217
|
-
if (from === "RUNNING" && to === "CANCELLED") {
|
|
218
|
-
const result = await ctx.service.stopMaterialization(
|
|
219
|
-
"my-environment",
|
|
220
|
-
"pkg",
|
|
221
|
-
exec.id,
|
|
222
|
-
);
|
|
223
|
-
expect(result).not.toBeNull();
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for (const [from, to] of invalidTransitions) {
|
|
229
|
-
it(`should reject ${from} -> ${to}`, async () => {
|
|
230
|
-
const exec = makeExecution({ status: from });
|
|
231
|
-
ctx.repository.getMaterializationById.resolves(exec);
|
|
232
|
-
|
|
233
|
-
await expect(
|
|
234
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
235
|
-
(ctx.service as any).transitionExecution(exec.id, to),
|
|
236
|
-
).rejects.toThrow(InvalidStateTransitionError);
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// ==================== CREATE / START / STOP ====================
|
|
242
|
-
|
|
243
|
-
describe("createMaterialization", () => {
|
|
244
|
-
it("should create a PENDING build", async () => {
|
|
245
|
-
(ctx.environmentStore.getEnvironment as sinon.SinonStub).resolves({
|
|
246
|
-
getPackage: sinon.stub().resolves({}),
|
|
247
|
-
});
|
|
173
|
+
describe("createMaterialization (Round 1)", () => {
|
|
174
|
+
it("creates a PENDING record and persists options", async () => {
|
|
248
175
|
ctx.repository.getActiveMaterialization.resolves(null);
|
|
249
|
-
const pending =
|
|
250
|
-
status: "PENDING",
|
|
251
|
-
metadata: { forceRefresh: false, autoLoadManifest: true },
|
|
252
|
-
});
|
|
176
|
+
const pending = makeMaterialization({ status: "PENDING" });
|
|
253
177
|
ctx.repository.createMaterialization.resolves(pending);
|
|
254
178
|
|
|
255
179
|
const result = await ctx.service.createMaterialization(
|
|
256
|
-
"my-
|
|
180
|
+
"my-env",
|
|
257
181
|
"pkg",
|
|
258
182
|
{
|
|
259
|
-
|
|
183
|
+
forceRefresh: true,
|
|
184
|
+
sourceNames: ["orders"],
|
|
185
|
+
pauseBetweenPhases: true,
|
|
260
186
|
},
|
|
261
187
|
);
|
|
262
188
|
|
|
263
189
|
expect(result.status).toBe("PENDING");
|
|
264
|
-
expect(
|
|
265
|
-
(result.metadata as Record<string, unknown>)?.autoLoadManifest,
|
|
266
|
-
).toBe(true);
|
|
267
|
-
// Options persist on the INSERT itself; no follow-up update.
|
|
268
|
-
expect(ctx.repository.createMaterialization.calledOnce).toBe(true);
|
|
269
190
|
expect(ctx.repository.createMaterialization.firstCall.args).toEqual([
|
|
270
|
-
"
|
|
191
|
+
"env-1",
|
|
271
192
|
"pkg",
|
|
272
193
|
"PENDING",
|
|
273
|
-
{
|
|
194
|
+
{
|
|
195
|
+
forceRefresh: true,
|
|
196
|
+
sourceNames: ["orders"],
|
|
197
|
+
pauseBetweenPhases: true,
|
|
198
|
+
},
|
|
274
199
|
]);
|
|
275
|
-
expect(ctx.repository.updateMaterialization.called).toBe(false);
|
|
276
200
|
});
|
|
277
201
|
|
|
278
|
-
it("
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
ctx.repository.getActiveMaterialization.resolves(
|
|
283
|
-
makeExecution({ id: "existing", status: "RUNNING" }),
|
|
202
|
+
it("defaults pauseBetweenPhases to false (auto-run) in metadata", async () => {
|
|
203
|
+
ctx.repository.getActiveMaterialization.resolves(null);
|
|
204
|
+
ctx.repository.createMaterialization.resolves(
|
|
205
|
+
makeMaterialization({ status: "PENDING" }),
|
|
284
206
|
);
|
|
285
207
|
|
|
208
|
+
await ctx.service.createMaterialization("my-env", "pkg", {});
|
|
209
|
+
|
|
210
|
+
expect(ctx.repository.createMaterialization.firstCall.args[3]).toEqual(
|
|
211
|
+
{
|
|
212
|
+
forceRefresh: false,
|
|
213
|
+
sourceNames: null,
|
|
214
|
+
pauseBetweenPhases: false,
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("rejects when an active materialization already exists", async () => {
|
|
220
|
+
ctx.repository.getActiveMaterialization.resolves(
|
|
221
|
+
makeMaterialization({ id: "existing", status: "BUILD_PLAN_READY" }),
|
|
222
|
+
);
|
|
286
223
|
await expect(
|
|
287
|
-
ctx.service.createMaterialization("my-
|
|
224
|
+
ctx.service.createMaterialization("my-env", "pkg"),
|
|
288
225
|
).rejects.toThrow(MaterializationConflictError);
|
|
289
226
|
});
|
|
290
227
|
|
|
291
|
-
it("
|
|
292
|
-
(ctx.environmentStore.getEnvironment as sinon.SinonStub).resolves({
|
|
293
|
-
getPackage: sinon.stub().resolves({}),
|
|
294
|
-
});
|
|
295
|
-
// The pre-check finds nothing (race is still possible), but the
|
|
296
|
-
// atomic insert loses to a concurrent create and the repository
|
|
297
|
-
// raises DuplicateActiveMaterializationError.
|
|
228
|
+
it("translates a lost create race into a conflict", async () => {
|
|
298
229
|
ctx.repository.getActiveMaterialization
|
|
299
230
|
.onFirstCall()
|
|
300
231
|
.resolves(null)
|
|
301
232
|
.onSecondCall()
|
|
302
|
-
.resolves(
|
|
233
|
+
.resolves(makeMaterialization({ id: "winner" }));
|
|
303
234
|
ctx.repository.createMaterialization.rejects(
|
|
304
|
-
new DuplicateActiveMaterializationError("
|
|
235
|
+
new DuplicateActiveMaterializationError("env-1", "pkg"),
|
|
305
236
|
);
|
|
306
|
-
|
|
307
237
|
await expect(
|
|
308
|
-
ctx.service.createMaterialization("my-
|
|
238
|
+
ctx.service.createMaterialization("my-env", "pkg"),
|
|
309
239
|
).rejects.toThrow(/winner/);
|
|
310
240
|
});
|
|
311
241
|
});
|
|
312
242
|
|
|
313
|
-
describe("
|
|
314
|
-
it("
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
ctx.repository.getActiveMaterialization.resolves(null);
|
|
322
|
-
ctx.repository.updateMaterialization.resolves(running);
|
|
323
|
-
|
|
324
|
-
const result = await ctx.service.startMaterialization(
|
|
325
|
-
"my-environment",
|
|
326
|
-
"pkg",
|
|
327
|
-
"exec-1",
|
|
243
|
+
describe("buildMaterialization (Round 2)", () => {
|
|
244
|
+
it("rejects action=build on an auto-run materialization", async () => {
|
|
245
|
+
ctx.repository.getMaterializationById.resolves(
|
|
246
|
+
makeMaterialization({
|
|
247
|
+
status: "BUILD_PLAN_READY",
|
|
248
|
+
pauseBetweenPhases: false,
|
|
249
|
+
buildPlan: makeBuildPlan(),
|
|
250
|
+
}),
|
|
328
251
|
);
|
|
329
|
-
|
|
330
|
-
expect(result.status).toBe("RUNNING");
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("should reject non-PENDING builds", async () => {
|
|
334
|
-
const running = makeExecution({ status: "RUNNING" });
|
|
335
|
-
ctx.repository.getMaterializationById.resolves(running);
|
|
336
|
-
|
|
337
252
|
await expect(
|
|
338
|
-
ctx.service.
|
|
253
|
+
ctx.service.buildMaterialization("my-env", "pkg", "mat-1", [
|
|
254
|
+
makeInstruction(),
|
|
255
|
+
]),
|
|
339
256
|
).rejects.toThrow(InvalidStateTransitionError);
|
|
340
257
|
});
|
|
341
258
|
|
|
342
|
-
it("
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
ctx.repository.getActiveMaterialization.resolves(
|
|
349
|
-
makeExecution({ id: "other", status: "RUNNING" }),
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
await expect(
|
|
353
|
-
ctx.service.startMaterialization("my-environment", "pkg", "exec-1"),
|
|
354
|
-
).rejects.toThrow(MaterializationConflictError);
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
describe("stopMaterialization", () => {
|
|
359
|
-
it("should cancel a PENDING build", async () => {
|
|
360
|
-
const pending = makeExecution({ status: "PENDING" });
|
|
361
|
-
ctx.repository.getMaterializationById.resolves(pending);
|
|
362
|
-
ctx.repository.updateMaterialization.resolves(
|
|
363
|
-
makeExecution({ status: "CANCELLED" }),
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
const result = await ctx.service.stopMaterialization(
|
|
367
|
-
"my-environment",
|
|
368
|
-
"pkg",
|
|
369
|
-
"exec-1",
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
expect(result.status).toBe("CANCELLED");
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("should force-cancel an orphaned RUNNING build", async () => {
|
|
376
|
-
const running = makeExecution({ id: "orphan", status: "RUNNING" });
|
|
377
|
-
ctx.repository.getMaterializationById.resolves(running);
|
|
378
|
-
ctx.repository.updateMaterialization.resolves(
|
|
379
|
-
makeExecution({ id: "orphan", status: "CANCELLED" }),
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
const result = await ctx.service.stopMaterialization(
|
|
383
|
-
"my-environment",
|
|
384
|
-
"pkg",
|
|
385
|
-
"orphan",
|
|
259
|
+
it("rejects when not at BUILD_PLAN_READY", async () => {
|
|
260
|
+
ctx.repository.getMaterializationById.resolves(
|
|
261
|
+
makeMaterialization({
|
|
262
|
+
status: "PENDING",
|
|
263
|
+
pauseBetweenPhases: true,
|
|
264
|
+
}),
|
|
386
265
|
);
|
|
387
|
-
|
|
388
|
-
expect(result.status).toBe("CANCELLED");
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("should reject stopping a terminal build", async () => {
|
|
392
|
-
const succeeded = makeExecution({ status: "SUCCESS" });
|
|
393
|
-
ctx.repository.getMaterializationById.resolves(succeeded);
|
|
394
|
-
|
|
395
266
|
await expect(
|
|
396
|
-
ctx.service.
|
|
267
|
+
ctx.service.buildMaterialization("my-env", "pkg", "mat-1", [
|
|
268
|
+
makeInstruction(),
|
|
269
|
+
]),
|
|
397
270
|
).rejects.toThrow(InvalidStateTransitionError);
|
|
398
271
|
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// ==================== DELETE ====================
|
|
402
|
-
|
|
403
|
-
describe("deleteMaterialization", () => {
|
|
404
|
-
it("should delete a SUCCESS materialization", async () => {
|
|
405
|
-
const succeeded = makeExecution({ status: "SUCCESS" });
|
|
406
|
-
ctx.repository.getMaterializationById.resolves(succeeded);
|
|
407
|
-
ctx.repository.deleteMaterialization.resolves();
|
|
408
|
-
|
|
409
|
-
await ctx.service.deleteMaterialization(
|
|
410
|
-
"my-environment",
|
|
411
|
-
"pkg",
|
|
412
|
-
"exec-1",
|
|
413
|
-
);
|
|
414
272
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
273
|
+
it("rejects instructions that reference an unknown buildId", async () => {
|
|
274
|
+
ctx.repository.getMaterializationById.resolves(
|
|
275
|
+
makeMaterialization({
|
|
276
|
+
status: "BUILD_PLAN_READY",
|
|
277
|
+
pauseBetweenPhases: true,
|
|
278
|
+
buildPlan: makeBuildPlan(),
|
|
279
|
+
}),
|
|
418
280
|
);
|
|
281
|
+
await expect(
|
|
282
|
+
ctx.service.buildMaterialization("my-env", "pkg", "mat-1", [
|
|
283
|
+
makeInstruction({ buildId: "ghost" }),
|
|
284
|
+
]),
|
|
285
|
+
).rejects.toThrow(BadRequestError);
|
|
419
286
|
});
|
|
420
287
|
|
|
421
|
-
it("
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
"pkg",
|
|
429
|
-
"exec-1",
|
|
288
|
+
it("rejects SNAPSHOT when the connection does not support it", async () => {
|
|
289
|
+
ctx.repository.getMaterializationById.resolves(
|
|
290
|
+
makeMaterialization({
|
|
291
|
+
status: "BUILD_PLAN_READY",
|
|
292
|
+
pauseBetweenPhases: true,
|
|
293
|
+
buildPlan: makeBuildPlan(),
|
|
294
|
+
}),
|
|
430
295
|
);
|
|
431
|
-
|
|
432
|
-
|
|
296
|
+
await expect(
|
|
297
|
+
ctx.service.buildMaterialization("my-env", "pkg", "mat-1", [
|
|
298
|
+
makeInstruction({ realization: "SNAPSHOT" }),
|
|
299
|
+
]),
|
|
300
|
+
).rejects.toThrow(BadRequestError);
|
|
433
301
|
});
|
|
434
302
|
|
|
435
|
-
it("
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
303
|
+
it("accepts a valid build and returns the record (202)", async () => {
|
|
304
|
+
const m = makeMaterialization({
|
|
305
|
+
status: "BUILD_PLAN_READY",
|
|
306
|
+
pauseBetweenPhases: true,
|
|
307
|
+
buildPlan: makeBuildPlan(),
|
|
308
|
+
});
|
|
309
|
+
ctx.repository.getMaterializationById.resolves(m);
|
|
310
|
+
const result = await ctx.service.buildMaterialization(
|
|
311
|
+
"my-env",
|
|
442
312
|
"pkg",
|
|
443
|
-
"
|
|
313
|
+
"mat-1",
|
|
314
|
+
[makeInstruction()],
|
|
444
315
|
);
|
|
445
|
-
|
|
446
|
-
expect(ctx.repository.deleteMaterialization.calledOnce).toBe(true);
|
|
316
|
+
expect(result.id).toBe("mat-1");
|
|
447
317
|
});
|
|
318
|
+
});
|
|
448
319
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
await expect(
|
|
467
|
-
ctx.service.deleteMaterialization(
|
|
468
|
-
"my-environment",
|
|
320
|
+
describe("stopMaterialization", () => {
|
|
321
|
+
const cancellable = [
|
|
322
|
+
"PENDING",
|
|
323
|
+
"BUILD_PLAN_READY",
|
|
324
|
+
"MANIFEST_ROWS_READY",
|
|
325
|
+
] as const;
|
|
326
|
+
|
|
327
|
+
for (const status of cancellable) {
|
|
328
|
+
it(`cancels a ${status} materialization`, async () => {
|
|
329
|
+
ctx.repository.getMaterializationById.resolves(
|
|
330
|
+
makeMaterialization({ status }),
|
|
331
|
+
);
|
|
332
|
+
ctx.repository.updateMaterialization.resolves(
|
|
333
|
+
makeMaterialization({ status: "CANCELLED" }),
|
|
334
|
+
);
|
|
335
|
+
const result = await ctx.service.stopMaterialization(
|
|
336
|
+
"my-env",
|
|
469
337
|
"pkg",
|
|
470
|
-
"
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
it("should throw when materialization not found", async () => {
|
|
476
|
-
ctx.repository.getMaterializationById.resolves(null);
|
|
338
|
+
"mat-1",
|
|
339
|
+
);
|
|
340
|
+
expect(result.status).toBe("CANCELLED");
|
|
341
|
+
});
|
|
342
|
+
}
|
|
477
343
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
|
|
344
|
+
const terminal = ["MANIFEST_FILE_READY", "FAILED", "CANCELLED"] as const;
|
|
345
|
+
for (const status of terminal) {
|
|
346
|
+
it(`rejects stopping a ${status} materialization`, async () => {
|
|
347
|
+
ctx.repository.getMaterializationById.resolves(
|
|
348
|
+
makeMaterialization({ status }),
|
|
349
|
+
);
|
|
350
|
+
await expect(
|
|
351
|
+
ctx.service.stopMaterialization("my-env", "pkg", "mat-1"),
|
|
352
|
+
).rejects.toThrow(InvalidStateTransitionError);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
486
355
|
});
|
|
487
356
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
357
|
+
describe("deleteMaterialization", () => {
|
|
358
|
+
const terminal = ["MANIFEST_FILE_READY", "FAILED", "CANCELLED"] as const;
|
|
359
|
+
for (const status of terminal) {
|
|
360
|
+
it(`deletes a ${status} materialization`, async () => {
|
|
361
|
+
ctx.repository.getMaterializationById.resolves(
|
|
362
|
+
makeMaterialization({ status }),
|
|
363
|
+
);
|
|
364
|
+
await ctx.service.deleteMaterialization("my-env", "pkg", "mat-1");
|
|
365
|
+
expect(
|
|
366
|
+
ctx.repository.deleteMaterialization.calledOnceWith("mat-1"),
|
|
367
|
+
).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
495
370
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
371
|
+
const active = [
|
|
372
|
+
"PENDING",
|
|
373
|
+
"BUILD_PLAN_READY",
|
|
374
|
+
"MANIFEST_ROWS_READY",
|
|
375
|
+
] as const;
|
|
376
|
+
for (const status of active) {
|
|
377
|
+
it(`rejects deleting a ${status} materialization`, async () => {
|
|
378
|
+
ctx.repository.getMaterializationById.resolves(
|
|
379
|
+
makeMaterialization({ status }),
|
|
380
|
+
);
|
|
381
|
+
await expect(
|
|
382
|
+
ctx.service.deleteMaterialization("my-env", "pkg", "mat-1"),
|
|
383
|
+
).rejects.toThrow(InvalidStateTransitionError);
|
|
384
|
+
expect(ctx.repository.deleteMaterialization.called).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
500
387
|
|
|
501
|
-
it("
|
|
388
|
+
it("does not drop tables by default (record-only delete)", async () => {
|
|
502
389
|
const runSQL = sinon.stub().resolves();
|
|
503
|
-
const
|
|
504
|
-
dialectName: "duckdb",
|
|
505
|
-
runSQL,
|
|
506
|
-
} as unknown as Connection;
|
|
507
|
-
const pkg = {
|
|
508
|
-
getMalloyConnection: async (name: string): Promise<Connection> => {
|
|
509
|
-
if (name === "conn") return connection;
|
|
510
|
-
throw new Error(`unknown connection: ${name}`);
|
|
511
|
-
},
|
|
512
|
-
};
|
|
390
|
+
const getMalloyConnection = sinon.stub().resolves({ runSQL });
|
|
513
391
|
(ctx.environmentStore.getEnvironment as sinon.SinonStub).resolves({
|
|
514
|
-
getPackage: sinon.stub().resolves(
|
|
392
|
+
getPackage: sinon.stub().resolves({ getMalloyConnection }),
|
|
393
|
+
withPackageLock: async (_n: string, fn: () => Promise<unknown>) =>
|
|
394
|
+
fn(),
|
|
515
395
|
});
|
|
516
|
-
ctx.repository.
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
environmentId: "proj-1",
|
|
532
|
-
packageName: "pkg",
|
|
533
|
-
buildId: "1234567890abcdef1234567890abcdef",
|
|
534
|
-
tableName: "table_b",
|
|
535
|
-
sourceName: "src",
|
|
536
|
-
connectionName: "conn",
|
|
537
|
-
createdAt: new Date(),
|
|
538
|
-
updatedAt: new Date(),
|
|
539
|
-
},
|
|
540
|
-
];
|
|
541
|
-
(ctx.manifestService.listEntries as sinon.SinonStub).resolves(entries);
|
|
542
|
-
|
|
543
|
-
const result = await ctx.service.teardownPackage(
|
|
544
|
-
"my-environment",
|
|
545
|
-
"pkg",
|
|
396
|
+
ctx.repository.getMaterializationById.resolves(
|
|
397
|
+
makeMaterialization({
|
|
398
|
+
status: "MANIFEST_FILE_READY",
|
|
399
|
+
manifest: {
|
|
400
|
+
builtAt: new Date().toISOString(),
|
|
401
|
+
strict: false,
|
|
402
|
+
entries: {
|
|
403
|
+
b1: {
|
|
404
|
+
buildId: "b1",
|
|
405
|
+
physicalTableName: "orders_mz",
|
|
406
|
+
connectionName: "duckdb",
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
546
411
|
);
|
|
547
412
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
expect(runSQL.
|
|
413
|
+
await ctx.service.deleteMaterialization("my-env", "pkg", "mat-1");
|
|
414
|
+
|
|
415
|
+
expect(getMalloyConnection.called).toBe(false);
|
|
416
|
+
expect(runSQL.called).toBe(false);
|
|
552
417
|
expect(
|
|
553
|
-
|
|
554
|
-
).toBe(
|
|
418
|
+
ctx.repository.deleteMaterialization.calledOnceWith("mat-1"),
|
|
419
|
+
).toBe(true);
|
|
555
420
|
});
|
|
556
421
|
|
|
557
|
-
it("
|
|
422
|
+
it("drops manifest tables (and staging) when dropTables is set", async () => {
|
|
558
423
|
const runSQL = sinon.stub().resolves();
|
|
559
|
-
const
|
|
560
|
-
dialectName: "duckdb",
|
|
561
|
-
runSQL,
|
|
562
|
-
} as unknown as Connection;
|
|
563
|
-
// Only "live_conn" is registered; the manifest row below points at
|
|
564
|
-
// a vanished "ghost_conn", which used to be impossible to tear down.
|
|
565
|
-
// `teardownPackage` must force-delete the row anyway so teardown
|
|
566
|
-
// can complete.
|
|
567
|
-
const pkg = {
|
|
568
|
-
getMalloyConnection: async (name: string): Promise<Connection> => {
|
|
569
|
-
if (name === "live_conn") return livingConn;
|
|
570
|
-
throw new Error(`unknown connection: ${name}`);
|
|
571
|
-
},
|
|
572
|
-
};
|
|
424
|
+
const getMalloyConnection = sinon.stub().resolves({ runSQL });
|
|
573
425
|
(ctx.environmentStore.getEnvironment as sinon.SinonStub).resolves({
|
|
574
|
-
getPackage: sinon.stub().resolves(
|
|
426
|
+
getPackage: sinon.stub().resolves({ getMalloyConnection }),
|
|
427
|
+
withPackageLock: async (_n: string, fn: () => Promise<unknown>) =>
|
|
428
|
+
fn(),
|
|
575
429
|
});
|
|
576
|
-
ctx.repository.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const result = await ctx.service.teardownPackage(
|
|
593
|
-
"my-environment",
|
|
594
|
-
"pkg",
|
|
430
|
+
ctx.repository.getMaterializationById.resolves(
|
|
431
|
+
makeMaterialization({
|
|
432
|
+
status: "MANIFEST_FILE_READY",
|
|
433
|
+
manifest: {
|
|
434
|
+
builtAt: new Date().toISOString(),
|
|
435
|
+
strict: false,
|
|
436
|
+
entries: {
|
|
437
|
+
b1: {
|
|
438
|
+
buildId: "b1",
|
|
439
|
+
physicalTableName: "orders_mz",
|
|
440
|
+
connectionName: "duckdb",
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
}),
|
|
595
445
|
);
|
|
596
446
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
expect(
|
|
447
|
+
await ctx.service.deleteMaterialization("my-env", "pkg", "mat-1", {
|
|
448
|
+
dropTables: true,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
expect(getMalloyConnection.calledOnceWith("duckdb")).toBe(true);
|
|
452
|
+
const dropped = runSQL.getCalls().map((c) => c.args[0] as string);
|
|
453
|
+
expect(dropped).toContain("DROP TABLE IF EXISTS orders_mz");
|
|
454
|
+
expect(dropped.some((s) => s.includes("orders_mz_"))).toBe(true);
|
|
602
455
|
expect(
|
|
603
|
-
|
|
456
|
+
ctx.repository.deleteMaterialization.calledOnceWith("mat-1"),
|
|
604
457
|
).toBe(true);
|
|
605
458
|
});
|
|
606
459
|
|
|
607
|
-
it("
|
|
608
|
-
const runSQL = sinon.stub().
|
|
609
|
-
const
|
|
610
|
-
dialectName: "duckdb",
|
|
611
|
-
runSQL,
|
|
612
|
-
} as unknown as Connection;
|
|
613
|
-
const pkg = {
|
|
614
|
-
getMalloyConnection: async (name: string): Promise<Connection> => {
|
|
615
|
-
if (name === "conn") return connection;
|
|
616
|
-
throw new Error(`unknown connection: ${name}`);
|
|
617
|
-
},
|
|
618
|
-
};
|
|
460
|
+
it("still deletes the record when a table drop fails", async () => {
|
|
461
|
+
const runSQL = sinon.stub().rejects(new Error("boom"));
|
|
462
|
+
const getMalloyConnection = sinon.stub().resolves({ runSQL });
|
|
619
463
|
(ctx.environmentStore.getEnvironment as sinon.SinonStub).resolves({
|
|
620
|
-
getPackage: sinon.stub().resolves(
|
|
464
|
+
getPackage: sinon.stub().resolves({ getMalloyConnection }),
|
|
465
|
+
withPackageLock: async (_n: string, fn: () => Promise<unknown>) =>
|
|
466
|
+
fn(),
|
|
621
467
|
});
|
|
622
|
-
ctx.repository.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
"my-environment",
|
|
638
|
-
"pkg",
|
|
639
|
-
{
|
|
640
|
-
dryRun: true,
|
|
641
|
-
},
|
|
468
|
+
ctx.repository.getMaterializationById.resolves(
|
|
469
|
+
makeMaterialization({
|
|
470
|
+
status: "FAILED",
|
|
471
|
+
manifest: {
|
|
472
|
+
builtAt: new Date().toISOString(),
|
|
473
|
+
strict: false,
|
|
474
|
+
entries: {
|
|
475
|
+
b1: {
|
|
476
|
+
buildId: "b1",
|
|
477
|
+
physicalTableName: "orders_mz",
|
|
478
|
+
connectionName: "duckdb",
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
}),
|
|
642
483
|
);
|
|
643
484
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
485
|
+
await ctx.service.deleteMaterialization("my-env", "pkg", "mat-1", {
|
|
486
|
+
dropTables: true,
|
|
487
|
+
});
|
|
488
|
+
|
|
647
489
|
expect(
|
|
648
|
-
|
|
649
|
-
).toBe(
|
|
490
|
+
ctx.repository.deleteMaterialization.calledOnceWith("mat-1"),
|
|
491
|
+
).toBe(true);
|
|
650
492
|
});
|
|
651
493
|
});
|
|
652
494
|
});
|
|
653
495
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
it("returns connectionName::tableName", () => {
|
|
658
|
-
expect(manifestTableKey("duckdb", "orders")).toBe("duckdb::orders");
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it("handles schema-qualified table names", () => {
|
|
662
|
-
expect(manifestTableKey("pg", "analytics.summary")).toBe(
|
|
663
|
-
"pg::analytics.summary",
|
|
664
|
-
);
|
|
665
|
-
});
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
describe("tablePhysicallyExists", () => {
|
|
669
|
-
it("returns true when SELECT succeeds", async () => {
|
|
670
|
-
const conn = {
|
|
671
|
-
runSQL: sinon.stub().resolves({ rows: [] }),
|
|
672
|
-
} as unknown as Connection;
|
|
673
|
-
|
|
674
|
-
expect(await tablePhysicallyExists(conn, '"my_table"')).toBe(true);
|
|
675
|
-
expect((conn.runSQL as sinon.SinonStub).calledOnce).toBe(true);
|
|
676
|
-
expect((conn.runSQL as sinon.SinonStub).firstCall.args[0]).toBe(
|
|
677
|
-
'SELECT 1 FROM "my_table" WHERE 1=0',
|
|
678
|
-
);
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
it("returns false when SELECT throws (table does not exist)", async () => {
|
|
682
|
-
const conn = {
|
|
683
|
-
runSQL: sinon.stub().rejects(new Error("table not found")),
|
|
684
|
-
} as unknown as Connection;
|
|
685
|
-
|
|
686
|
-
expect(await tablePhysicallyExists(conn, '"missing"')).toBe(false);
|
|
496
|
+
describe("stagingSuffix", () => {
|
|
497
|
+
it("derives a short, stable suffix from the buildId", () => {
|
|
498
|
+
expect(stagingSuffix("abcdef1234567890")).toBe("_abcdef123456");
|
|
687
499
|
});
|
|
688
500
|
});
|