From f7f1388b3295c953252c68d5b6259ad8931e14c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Tue, 14 Jul 2026 09:42:24 +0700 Subject: [PATCH 1/2] fix initial introspection reset --- .changeset/bright-dragons-introspect-once.md | 5 ++ Architecture/introspection.md | 1 + FEATURES.md | 1 + ui/studio/context.test.tsx | 58 ++++++++++++++++++-- ui/studio/context.tsx | 13 ++++- 5 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 .changeset/bright-dragons-introspect-once.md diff --git a/.changeset/bright-dragons-introspect-once.md b/.changeset/bright-dragons-introspect-once.md new file mode 100644 index 00000000..701ff9f4 --- /dev/null +++ b/.changeset/bright-dragons-introspect-once.md @@ -0,0 +1,5 @@ +--- +"@prisma/studio-core": minor +--- + +Avoid cancelling and repeating introspection requests when Studio initially mounts. diff --git a/Architecture/introspection.md b/Architecture/introspection.md index 36decc02..e7504294 100644 --- a/Architecture/introspection.md +++ b/Architecture/introspection.md @@ -87,6 +87,7 @@ Failure diagnostics MUST include: Changes to this subsystem MUST include tests for: +- a single initial introspection without mount-time cancellation or refetch - failed initial introspection without automatic retry - stale-data preservation after a failed refetch - startup recovery UI rendering diff --git a/FEATURES.md b/FEATURES.md index b617da84..2acaa70e 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -9,6 +9,7 @@ Each adapter handles introspection, querying, inserts, updates, and deletes whil Studio introspects connected databases to build schemas, tables, columns, relationships, filter operators, and timezone metadata. This gives users an accurate live model of the database and keeps table navigation grounded in current structure. +A fresh Studio mount performs this discovery once, while actual adapter or database-availability changes invalidate cached metadata and load it again. ## Deployable Prisma Postgres Demo diff --git a/ui/studio/context.test.tsx b/ui/studio/context.test.tsx index 90049f48..87b79dde 100644 --- a/ui/studio/context.test.tsx +++ b/ui/studio/context.test.tsx @@ -1,3 +1,4 @@ +import { QueryClient } from "@tanstack/react-query"; import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; @@ -50,10 +51,20 @@ function createAdapter(): Adapter { } as unknown as Adapter; } -function renderHarness(props?: { streamsUrl?: string }) { +type RenderHarnessProps = { + adapter?: Adapter; + hasDatabase?: boolean; + streamsUrl?: string; +}; + +function renderHarness(props?: RenderHarnessProps) { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); + let currentProps = { + ...props, + adapter: props?.adapter ?? createAdapter(), + }; let latestStudio: ReturnType | undefined; @@ -62,15 +73,20 @@ function renderHarness(props?: { streamsUrl?: string }) { return null; } - act(() => { + function render() { root.render( , ); + } + + act(() => { + render(); }); return { @@ -83,6 +99,16 @@ function renderHarness(props?: { streamsUrl?: string }) { getLatestStudio() { return latestStudio; }, + rerender(nextProps: RenderHarnessProps) { + currentProps = { + ...currentProps, + ...nextProps, + }; + + act(() => { + render(); + }); + }, }; } @@ -170,6 +196,30 @@ afterEach(() => { .VERSION_INJECTED_AT_BUILD_TIME; }); +describe("StudioContextProvider database cache lifecycle", () => { + it("resets cached queries only after the database configuration changes", () => { + const resetQueriesSpy = vi + .spyOn(QueryClient.prototype, "resetQueries") + .mockResolvedValue(); + const initialAdapter = createAdapter(); + const harness = renderHarness({ adapter: initialAdapter }); + + try { + expect(resetQueriesSpy).not.toHaveBeenCalled(); + + const nextAdapter = createAdapter(); + harness.rerender({ adapter: nextAdapter }); + expect(resetQueriesSpy).toHaveBeenCalledTimes(1); + + harness.rerender({ adapter: nextAdapter, hasDatabase: false }); + expect(resetQueriesSpy).toHaveBeenCalledTimes(2); + } finally { + harness.cleanup(); + resetQueriesSpy.mockRestore(); + } + }); +}); + describe("StudioContextProvider pagination preferences", () => { it("persists shared page-size and infinite-scroll preferences across remounts", () => { const firstHarness = renderHarness(); diff --git a/ui/studio/context.tsx b/ui/studio/context.tsx index a688e24d..f074dd0b 100644 --- a/ui/studio/context.tsx +++ b/ui/studio/context.tsx @@ -317,6 +317,7 @@ export function StudioContextProvider(props: StudioContextProviderProps) { } = props; const queryClientRef = useRef(new QueryClient()); + const previousDatabaseConfigRef = useRef({ adapter, hasDatabase }); const signatureRef = useRef(shortUUID.generate()); const rowsCollectionCacheRef = useRef(new Map()); const tableQueryExecutionStateCacheRef = useRef( @@ -526,7 +527,17 @@ export function StudioContextProvider(props: StudioContextProviderProps) { }, [studioUiCollection]); useEffect(() => { - // if the adapter has been changed, then we need to reload + const previousDatabaseConfig = previousDatabaseConfigRef.current; + previousDatabaseConfigRef.current = { adapter, hasDatabase }; + + if ( + previousDatabaseConfig.adapter === adapter && + previousDatabaseConfig.hasDatabase === hasDatabase + ) { + return; + } + + // If the database configuration changed, then we need to reload. for (const state of tableQueryExecutionStateCacheRef.current.values()) { state.activeController?.abort(); } From c860f864f30b77e28021a22b7e42a74e2cf653cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Tue, 14 Jul 2026 09:48:44 +0700 Subject: [PATCH 2/2] address review feedback --- .changeset/bright-dragons-introspect-once.md | 2 ++ ui/studio/context.test.tsx | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.changeset/bright-dragons-introspect-once.md b/.changeset/bright-dragons-introspect-once.md index 701ff9f4..efa97620 100644 --- a/.changeset/bright-dragons-introspect-once.md +++ b/.changeset/bright-dragons-introspect-once.md @@ -2,4 +2,6 @@ "@prisma/studio-core": minor --- +# Fix duplicate startup introspection requests + Avoid cancelling and repeating introspection requests when Studio initially mounts. diff --git a/ui/studio/context.test.tsx b/ui/studio/context.test.tsx index 87b79dde..8f4ec91c 100644 --- a/ui/studio/context.test.tsx +++ b/ui/studio/context.test.tsx @@ -211,6 +211,9 @@ describe("StudioContextProvider database cache lifecycle", () => { harness.rerender({ adapter: nextAdapter }); expect(resetQueriesSpy).toHaveBeenCalledTimes(1); + harness.rerender({ adapter: nextAdapter }); + expect(resetQueriesSpy).toHaveBeenCalledTimes(1); + harness.rerender({ adapter: nextAdapter, hasDatabase: false }); expect(resetQueriesSpy).toHaveBeenCalledTimes(2); } finally {