Skip to content

Commit 7489dfb

Browse files
committed
validate kind, arch, branch region on adopt too
1 parent 8fba3a2 commit 7489dfb

File tree

3 files changed

+168
-1
lines changed

3 files changed

+168
-1
lines changed

alchemy/src/planetscale/branch.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ export interface BranchProps extends PlanetScaleProps {
8383
* Enable or disable safe migrations on this branch
8484
*/
8585
safeMigrations?: boolean;
86+
87+
/**
88+
* The region to create the branch in.
89+
* If not provided, the branch will be created in the default region for its database.
90+
* On adopt/update, if specified, the actual branch region is validated against this value.
91+
*/
92+
region?: {
93+
/**
94+
* The slug identifier of the region (e.g. "us-east", "eu-west")
95+
*/
96+
slug: string;
97+
};
8698
}
8799

88100
/**
@@ -113,6 +125,20 @@ export interface Branch extends BranchProps {
113125
* HTML URL to access the branch
114126
*/
115127
htmlUrl: string;
128+
129+
/**
130+
* The actual region of the branch as reported by PlanetScale
131+
*/
132+
actualRegion: {
133+
/**
134+
* The slug identifier of the region (e.g. "us-east", "eu-west")
135+
*/
136+
slug: string;
137+
/**
138+
* Display name of the region (e.g. "US East", "EU West")
139+
*/
140+
displayName: string;
141+
};
116142
}
117143

118144
/**
@@ -259,6 +285,18 @@ export const Branch = Resource(
259285
const data = getResponse.data;
260286
const currentParentBranch = data.parent_branch || "main";
261287

288+
// Validate region matches if specified
289+
if (props.region) {
290+
const actualSlug = data.region.slug;
291+
if (actualSlug !== props.region.slug) {
292+
throw new Error(
293+
`Branch "${branchName}" is in region "${actualSlug}" but expected "${props.region.slug}". ` +
294+
`PlanetScale branch regions cannot be changed after creation. ` +
295+
`Either update the region in your configuration to match, or create a new branch in the correct region.`,
296+
);
297+
}
298+
}
299+
262300
// Check immutable properties
263301
if (props.parentBranch && parentBranchName !== currentParentBranch) {
264302
throw new Error(
@@ -323,6 +361,10 @@ export const Branch = Resource(
323361
createdAt: data.created_at,
324362
updatedAt: data.updated_at,
325363
htmlUrl: data.html_url,
364+
actualRegion: {
365+
slug: data.region.slug,
366+
displayName: data.region.display_name,
367+
},
326368
};
327369
}
328370
let clusterSize: string | undefined;
@@ -352,6 +394,7 @@ export const Branch = Resource(
352394
parent_branch: parentBranchName,
353395
backup_id: props.backupId,
354396
seed_data: props.seedData,
397+
region: props.region?.slug,
355398
// This is ignored unless props.backupId is provided
356399
cluster_size: clusterSize,
357400
},
@@ -391,6 +434,10 @@ export const Branch = Resource(
391434
createdAt: data.created_at,
392435
updatedAt: data.updated_at,
393436
htmlUrl: data.html_url,
437+
actualRegion: {
438+
slug: data.region.slug,
439+
displayName: data.region.display_name,
440+
},
394441
};
395442
},
396443
);

alchemy/src/planetscale/database.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,15 @@ export const Database = Resource(
346346
});
347347
}
348348

349-
// Validate region matches if specified
349+
// Validate immutable properties match if specified
350+
const actualKind = getResponse.data.kind;
351+
if (props.kind && actualKind !== props.kind) {
352+
throw new Error(
353+
`Database "${databaseName}" has kind "${actualKind}" but expected "${props.kind}". ` +
354+
`Database kind cannot be changed after creation.`,
355+
);
356+
}
357+
350358
if (props.region) {
351359
const actualSlug = getResponse.data.region.slug;
352360
if (actualSlug !== props.region.slug) {
@@ -358,6 +366,28 @@ export const Database = Resource(
358366
}
359367
}
360368

369+
if (props.kind === "postgresql" && props.arch) {
370+
const defaultBranch = props.defaultBranch || "main";
371+
const branchInfo = await api.getBranch({
372+
path: {
373+
organization,
374+
database: databaseName,
375+
branch: defaultBranch,
376+
},
377+
throwOnError: false,
378+
});
379+
if (branchInfo.data?.cluster_architecture) {
380+
const actualArch =
381+
branchInfo.data.cluster_architecture === "aarch64" ? "arm" : "x86";
382+
if (actualArch !== props.arch) {
383+
throw new Error(
384+
`Database "${databaseName}" has architecture "${actualArch}" but expected "${props.arch}". ` +
385+
`Database architecture cannot be changed after creation.`,
386+
);
387+
}
388+
}
389+
}
390+
361391
// Update database settings
362392
// If updating to a non-'main' default branch, create it first
363393
if (props.defaultBranch && props.defaultBranch !== "main") {

alchemy/test/planetscale/database.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,96 @@ describe.skipIf(!process.env.PLANETSCALE_TEST).concurrent.each(kinds)(
348348
}
349349
}, 5_000_000);
350350

351+
test(`adopt with wrong kind should throw (${kind})`, async (scope) => {
352+
const name = `${BRANCH_PREFIX}-${kind}-kind`;
353+
const wrongKind = kind === "mysql" ? "postgresql" : "mysql";
354+
355+
try {
356+
await Database("kind-check", {
357+
name,
358+
clusterSize: "PS_10",
359+
kind,
360+
delete: true,
361+
});
362+
363+
await waitForDatabaseReady(api, organization, name);
364+
365+
// Try to adopt with the wrong kind — should throw
366+
await expect(
367+
Database("kind-check", {
368+
name,
369+
adopt: true,
370+
clusterSize: "PS_10",
371+
kind: wrongKind,
372+
delete: true,
373+
}),
374+
).rejects.toThrow(
375+
new RegExp(`has kind "${kind}" but expected "${wrongKind}"`),
376+
);
377+
378+
// Adopting with the correct kind should succeed
379+
const adopted = await Database("kind-check", {
380+
name,
381+
adopt: true,
382+
clusterSize: "PS_10",
383+
kind,
384+
delete: true,
385+
});
386+
387+
expect(adopted.name).toBe(name);
388+
} finally {
389+
await destroy(scope);
390+
await assertDatabaseDeleted(api, organization, name);
391+
}
392+
}, 5_000_000);
393+
394+
test.skipIf(kind !== "postgresql")(
395+
`adopt with wrong arch should throw (${kind})`,
396+
async (scope) => {
397+
const name = `${BRANCH_PREFIX}-${kind}-arch-check`;
398+
399+
try {
400+
await Database("arch-check", {
401+
name,
402+
clusterSize: "PS_10",
403+
kind: "postgresql",
404+
arch: "x86",
405+
delete: true,
406+
});
407+
408+
await waitForDatabaseReady(api, organization, name);
409+
410+
// Try to adopt with the wrong arch — should throw
411+
await expect(
412+
Database("arch-check", {
413+
name,
414+
adopt: true,
415+
clusterSize: "PS_10",
416+
kind: "postgresql",
417+
arch: "arm",
418+
delete: true,
419+
}),
420+
).rejects.toThrow(/has architecture "x86" but expected "arm"/);
421+
422+
// Adopting with the correct arch should succeed
423+
const adopted = await Database("arch-check", {
424+
name,
425+
adopt: true,
426+
clusterSize: "PS_10",
427+
kind: "postgresql",
428+
arch: "x86",
429+
delete: true,
430+
});
431+
432+
expect(adopted.name).toBe(name);
433+
} finally {
434+
await destroy(scope);
435+
await assertDatabaseDeleted(api, organization, name);
436+
}
437+
},
438+
5_000_000,
439+
);
440+
351441
test(`database with delete=false should not be deleted via API (${kind})`, async (scope) => {
352442
const name = `${BRANCH_PREFIX}-${kind}-nodelete`;
353443

0 commit comments

Comments
 (0)