Skip to content

Commit b3c0291

Browse files
committed
launch scratch: add retry for connection string (#2659)
Synced from monorepo@5d3685eae93d62ef913d8c0d52f003adf66b36d0
1 parent b8f9fbc commit b3c0291

5 files changed

Lines changed: 65 additions & 11 deletions

File tree

.sync-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5162fbd21b5849c51c0f806b7fe2276aefc6c014
1+
5d3685eae93d62ef913d8c0d52f003adf66b36d0

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "xata-cli",
33
"description": "Xata CLI",
4-
"version": "1.4.4",
4+
"version": "1.4.5",
55
"private": true,
66
"module": "src/app.ts",
77
"type": "module",
@@ -37,15 +37,15 @@
3737
"zod": "4.4.3",
3838
"zod-config": "1.4.0",
3939
"@xata.io/ai": "0.1.0",
40+
"@xata.io/api": "0.1.1",
41+
"@xata.io/config": "0.0.1",
4042
"@xata.io/lang": "0.0.1",
4143
"@xata.io/pgroll": "0.9.0",
4244
"@xata.io/pgstream": "0.2.0",
4345
"@xata.io/sql": "0.1.4",
4446
"@xata.io/test-utils": "0.0.1",
4547
"@xata.io/tsconfig": "0.0.1",
46-
"@xata.io/config": "0.0.1",
47-
"@xata.io/utils": "0.1.0",
48-
"@xata.io/api": "0.1.1"
48+
"@xata.io/utils": "0.1.0"
4949
},
5050
"scripts": {
5151
"dev": "bun src/bin/cli.ts",

src/app.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ const routes = buildRouteMap({
5151
hideRoute: {
5252
onboard: true,
5353
ai: true,
54-
stream: true,
55-
scratch: true
54+
stream: true
5655
}
5756
}
5857
});

src/commands/scratch.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ type Flags = {
1818
};
1919

2020
const SIGNAL_CLEANUP_TIMEOUT_MS = 10 * 1000;
21+
const CONNECTION_STRING_RETRY_COUNT = 2;
22+
const CONNECTION_STRING_RETRY_DELAY_MS = 100;
2123
const SCRATCH_SCALE_TO_ZERO = {
2224
enabled: true,
2325
inactivityPeriodMinutes: 10
@@ -57,6 +59,10 @@ async function withTimeout<T>(promise: Promise<T>, ms: number) {
5759
}
5860
}
5961

62+
async function sleep(ms: number) {
63+
await new Promise((resolve) => setTimeout(resolve, ms));
64+
}
65+
6066
function getErrorMessage(error: unknown) {
6167
if (error instanceof Error) return error.message;
6268
if (typeof error === 'object' && error !== null && 'message' in error) return String(error.message);
@@ -202,6 +208,28 @@ async function deleteScratchBranchByName(
202208
}
203209
}
204210

211+
async function resolveScratchBranchConnectionString(
212+
context: LocalContext,
213+
organizationId: string,
214+
projectId: string,
215+
branch: ScratchBranch
216+
) {
217+
if (branch.connectionString) return branch;
218+
219+
let resolvedBranch = branch;
220+
for (let retry = 0; retry < CONNECTION_STRING_RETRY_COUNT; retry++) {
221+
await sleep(CONNECTION_STRING_RETRY_DELAY_MS);
222+
223+
resolvedBranch = await context.api.branches.describeBranch({
224+
pathParams: { organizationID: organizationId, projectID: projectId, branchID: branch.id }
225+
});
226+
227+
if (resolvedBranch.connectionString) return resolvedBranch;
228+
}
229+
230+
return resolvedBranch;
231+
}
232+
205233
export async function implementation(this: LocalContext, flags: Flags, ...command: string[]) {
206234
const hasQuery = Boolean(flags.execute);
207235
const hasBinary = command.length > 0;
@@ -337,9 +365,11 @@ export async function implementation(this: LocalContext, flags: Flags, ...comman
337365

338366
this.process.stderr.write(chalk.green(`Created scratch branch ${createdScratchBranch.name}\n`));
339367

340-
invariant(createdScratchBranch.connectionString, 'Scratch branch should have a connection string.');
368+
scratchBranch = await resolveScratchBranchConnectionString(this, organizationId, projectId, createdScratchBranch);
369+
370+
invariant(scratchBranch.connectionString, 'Scratch branch should have a connection string.');
341371

342-
const connectionString = buildConnectionString(createdScratchBranch.connectionString, {
372+
const connectionString = buildConnectionString(scratchBranch.connectionString, {
343373
database,
344374
endpointType: 'rw'
345375
});

src/commands/scratch.unit.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ function buildContext() {
2727
name: body.name,
2828
createdAt: '2026-06-09T00:00:00.000Z',
2929
parentID: 'source-branch-id',
30-
connectionString: 'postgresql://user:pass@localhost:5432/postgres'
30+
connectionString: 'postgresql://user:pass@localhost:5432/postgres' as string | null
3131
}));
3232
const deleteBranch = mock(async () => ({}));
3333
const listBranches = mock(async (): Promise<{ branches: { id: string; name: string }[] }> => ({ branches: [] }));
3434
const describeBranch = mock(async () => ({
3535
id: 'scratch-branch-id',
3636
name: 'scratch-branch',
37-
connectionString: 'postgresql://user:pass@localhost:5432/postgres',
37+
connectionString: 'postgresql://user:pass@localhost:5432/postgres' as string | null,
3838
status: { statusType: 'STATUS_TYPE_HEALTHY', status: 'healthy' }
3939
}));
4040

@@ -95,6 +95,31 @@ describe('scratch command', () => {
9595
expect(stderr.join('')).toContain('Deleted scratch branch scratch-');
9696
});
9797

98+
test('retries describing the scratch branch when create does not return a connection string', async () => {
99+
const { context, createBranch, describeBranch, unsafe } = buildContext();
100+
createBranch.mockImplementationOnce(async ({ body }: { body: { name: string } }) => ({
101+
id: 'scratch-branch-id',
102+
name: body.name,
103+
createdAt: '2026-06-09T00:00:00.000Z',
104+
parentID: 'source-branch-id',
105+
connectionString: null
106+
}));
107+
describeBranch.mockImplementationOnce(async () => ({
108+
id: 'scratch-branch-id',
109+
name: 'scratch-branch',
110+
connectionString: null,
111+
status: { statusType: 'STATUS_TYPE_HEALTHY', status: 'healthy' }
112+
}));
113+
114+
await implementation.call(context, { execute: 'select 1', json: false });
115+
116+
expect(describeBranch).toHaveBeenCalledTimes(2);
117+
expect(describeBranch).toHaveBeenCalledWith({
118+
pathParams: { organizationID: 'org-id', projectID: 'project-id', branchID: 'scratch-branch-id' }
119+
});
120+
expect(unsafe).toHaveBeenCalledWith('select 1');
121+
});
122+
98123
test('prints SQL results as JSON when --json is passed', async () => {
99124
const { context, stdout } = buildContext();
100125

0 commit comments

Comments
 (0)