Skip to content

Commit c341072

Browse files
authored
Fix duplicate startup introspection requests (#1539)
* fix initial introspection reset * address review feedback
1 parent e6cc33f commit c341072

5 files changed

Lines changed: 78 additions & 5 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@prisma/studio-core": minor
3+
---
4+
5+
# Fix duplicate startup introspection requests
6+
7+
Avoid cancelling and repeating introspection requests when Studio initially mounts.

Architecture/introspection.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Failure diagnostics MUST include:
8787

8888
Changes to this subsystem MUST include tests for:
8989

90+
- a single initial introspection without mount-time cancellation or refetch
9091
- failed initial introspection without automatic retry
9192
- stale-data preservation after a failed refetch
9293
- startup recovery UI rendering

FEATURES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Each adapter handles introspection, querying, inserts, updates, and deletes whil
99

1010
Studio introspects connected databases to build schemas, tables, columns, relationships, filter operators, and timezone metadata.
1111
This gives users an accurate live model of the database and keeps table navigation grounded in current structure.
12+
A fresh Studio mount performs this discovery once, while actual adapter or database-availability changes invalidate cached metadata and load it again.
1213

1314
## Deployable Prisma Postgres Demo
1415

ui/studio/context.test.tsx

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { QueryClient } from "@tanstack/react-query";
12
import type { ReactNode } from "react";
23
import { act } from "react";
34
import { createRoot } from "react-dom/client";
@@ -50,10 +51,20 @@ function createAdapter(): Adapter {
5051
} as unknown as Adapter;
5152
}
5253

53-
function renderHarness(props?: { streamsUrl?: string }) {
54+
type RenderHarnessProps = {
55+
adapter?: Adapter;
56+
hasDatabase?: boolean;
57+
streamsUrl?: string;
58+
};
59+
60+
function renderHarness(props?: RenderHarnessProps) {
5461
const container = document.createElement("div");
5562
document.body.appendChild(container);
5663
const root = createRoot(container);
64+
let currentProps = {
65+
...props,
66+
adapter: props?.adapter ?? createAdapter(),
67+
};
5768

5869
let latestStudio: ReturnType<typeof useStudio> | undefined;
5970

@@ -62,15 +73,20 @@ function renderHarness(props?: { streamsUrl?: string }) {
6273
return null;
6374
}
6475

65-
act(() => {
76+
function render() {
6677
root.render(
6778
<StudioContextProvider
68-
adapter={createAdapter()}
69-
streamsUrl={props?.streamsUrl}
79+
adapter={currentProps.adapter}
80+
hasDatabase={currentProps.hasDatabase}
81+
streamsUrl={currentProps.streamsUrl}
7082
>
7183
<Harness />
7284
</StudioContextProvider>,
7385
);
86+
}
87+
88+
act(() => {
89+
render();
7490
});
7591

7692
return {
@@ -83,6 +99,16 @@ function renderHarness(props?: { streamsUrl?: string }) {
8399
getLatestStudio() {
84100
return latestStudio;
85101
},
102+
rerender(nextProps: RenderHarnessProps) {
103+
currentProps = {
104+
...currentProps,
105+
...nextProps,
106+
};
107+
108+
act(() => {
109+
render();
110+
});
111+
},
86112
};
87113
}
88114

@@ -170,6 +196,33 @@ afterEach(() => {
170196
.VERSION_INJECTED_AT_BUILD_TIME;
171197
});
172198

199+
describe("StudioContextProvider database cache lifecycle", () => {
200+
it("resets cached queries only after the database configuration changes", () => {
201+
const resetQueriesSpy = vi
202+
.spyOn(QueryClient.prototype, "resetQueries")
203+
.mockResolvedValue();
204+
const initialAdapter = createAdapter();
205+
const harness = renderHarness({ adapter: initialAdapter });
206+
207+
try {
208+
expect(resetQueriesSpy).not.toHaveBeenCalled();
209+
210+
const nextAdapter = createAdapter();
211+
harness.rerender({ adapter: nextAdapter });
212+
expect(resetQueriesSpy).toHaveBeenCalledTimes(1);
213+
214+
harness.rerender({ adapter: nextAdapter });
215+
expect(resetQueriesSpy).toHaveBeenCalledTimes(1);
216+
217+
harness.rerender({ adapter: nextAdapter, hasDatabase: false });
218+
expect(resetQueriesSpy).toHaveBeenCalledTimes(2);
219+
} finally {
220+
harness.cleanup();
221+
resetQueriesSpy.mockRestore();
222+
}
223+
});
224+
});
225+
173226
describe("StudioContextProvider pagination preferences", () => {
174227
it("persists shared page-size and infinite-scroll preferences across remounts", () => {
175228
const firstHarness = renderHarness();

ui/studio/context.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ export function StudioContextProvider(props: StudioContextProviderProps) {
317317
} = props;
318318

319319
const queryClientRef = useRef(new QueryClient());
320+
const previousDatabaseConfigRef = useRef({ adapter, hasDatabase });
320321
const signatureRef = useRef(shortUUID.generate());
321322
const rowsCollectionCacheRef = useRef(new Map<string, unknown>());
322323
const tableQueryExecutionStateCacheRef = useRef(
@@ -526,7 +527,17 @@ export function StudioContextProvider(props: StudioContextProviderProps) {
526527
}, [studioUiCollection]);
527528

528529
useEffect(() => {
529-
// if the adapter has been changed, then we need to reload
530+
const previousDatabaseConfig = previousDatabaseConfigRef.current;
531+
previousDatabaseConfigRef.current = { adapter, hasDatabase };
532+
533+
if (
534+
previousDatabaseConfig.adapter === adapter &&
535+
previousDatabaseConfig.hasDatabase === hasDatabase
536+
) {
537+
return;
538+
}
539+
540+
// If the database configuration changed, then we need to reload.
530541
for (const state of tableQueryExecutionStateCacheRef.current.values()) {
531542
state.activeController?.abort();
532543
}

0 commit comments

Comments
 (0)