-
Notifications
You must be signed in to change notification settings - Fork 84
fix: refresh stale introspection so editor type stays correct #1567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
prisma-gremlin
wants to merge
4
commits into
main
Choose a base branch
from
fix/refreshable-introspection-editor-type
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8262c43
fix: refresh stale introspection so editor type stays correct
a2290c1
fix: restore 30s staleTime on introspection query
d876468
fix: keep column-type label button keyboard focusable
82f2c88
fix: address CodeRabbit review follow-ups on introspection refetch an…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { AdapterError } from "../adapter"; | ||
| import { | ||
| getPostgresErrorCode, | ||
| isPostgresTypeMismatchError, | ||
| POSTGRES_DATATYPE_MISMATCH_CODE, | ||
| POSTGRES_INVALID_TEXT_REPRESENTATION_CODE, | ||
| } from "./postgres-error"; | ||
|
|
||
| function createAdapterErrorWithCode(code: string): AdapterError { | ||
| const error = new AdapterError(`db error: ${code}`) as AdapterError & { | ||
| code?: string; | ||
| }; | ||
| error.code = code; | ||
| error.adapterSource = "postgresql"; | ||
| return error; | ||
| } | ||
|
|
||
| describe("getPostgresErrorCode", () => { | ||
| it("returns the string code attached to a driver/adapter error", () => { | ||
| expect(getPostgresErrorCode(createAdapterErrorWithCode("42804"))).toBe( | ||
| "42804", | ||
| ); | ||
| }); | ||
|
|
||
| it("returns undefined when the error has no code", () => { | ||
| expect(getPostgresErrorCode(new Error("no code"))).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("returns undefined for non-string code values", () => { | ||
| expect( | ||
| getPostgresErrorCode({ code: 42, message: "numeric code" }), | ||
| ).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("returns undefined for null/undefined input", () => { | ||
| expect(getPostgresErrorCode(null)).toBeUndefined(); | ||
| expect(getPostgresErrorCode(undefined)).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("isPostgresTypeMismatchError", () => { | ||
| it("detects SQLSTATE 42804 (datatype_mismatch)", () => { | ||
| expect( | ||
| isPostgresTypeMismatchError( | ||
| createAdapterErrorWithCode(POSTGRES_DATATYPE_MISMATCH_CODE), | ||
| ), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it("detects SQLSTATE 22P02 (invalid_text_representation)", () => { | ||
| expect( | ||
| isPostgresTypeMismatchError( | ||
| createAdapterErrorWithCode(POSTGRES_INVALID_TEXT_REPRESENTATION_CODE), | ||
| ), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it("does not match unrelated Postgres SQLSTATEs", () => { | ||
| expect( | ||
| isPostgresTypeMismatchError(createAdapterErrorWithCode("42P01")), | ||
| ).toBe(false); | ||
| expect( | ||
| isPostgresTypeMismatchError(createAdapterErrorWithCode("23505")), | ||
| ).toBe(false); | ||
| }); | ||
|
|
||
| it("does not match errors without a SQLSTATE code", () => { | ||
| expect(isPostgresTypeMismatchError(new Error("network failure"))).toBe( | ||
| false, | ||
| ); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import type { AdapterError } from "../adapter"; | ||
|
|
||
| /** | ||
| * PostgreSQL SQLSTATE `datatype_mismatch`. | ||
| * | ||
| * Raised when an INSERT/UPDATE supplies a value whose type does not match | ||
| * the column's current type and Postgres cannot coerce it. | ||
| */ | ||
| export const POSTGRES_DATATYPE_MISMATCH_CODE = "42804"; | ||
|
|
||
| /** | ||
| * PostgreSQL SQLSTATE `invalid_text_representation`. | ||
| * | ||
| * Raised when a text value cannot be parsed into the target type, e.g. the | ||
| * editor sent `"true"`/`true` for a column whose live type is no longer | ||
| * boolean and the value cannot be cast. | ||
| */ | ||
| export const POSTGRES_INVALID_TEXT_REPRESENTATION_CODE = "22P02"; | ||
|
|
||
| const TYPE_MISMATCH_SQLSTATES = new Set<string>([ | ||
| POSTGRES_DATATYPE_MISMATCH_CODE, | ||
| POSTGRES_INVALID_TEXT_REPRESENTATION_CODE, | ||
| ]); | ||
|
|
||
| /** | ||
| * Returns the PostgreSQL SQLSTATE code carried on an error, if any. | ||
| * | ||
| * Postgres drivers (e.g. `postgres`) attach the server's `SqlState` as a | ||
| * string `code` property on the thrown/rejected error. `createAdapterError` | ||
| * mutates that same error object to add `adapterSource`/`query`, so the code | ||
| * survives onto the `AdapterError` that reaches mutation handlers. This | ||
| * mirrors the shape already read by lint diagnostics | ||
| * (`getPostgresErrorCode` in `./sql-lint`). | ||
| */ | ||
| export function getPostgresErrorCode(error: unknown): string | undefined { | ||
| if ( | ||
| error == null || | ||
| (typeof error !== "object" && typeof error !== "function") | ||
| ) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const { code } = error as { code?: unknown }; | ||
| return typeof code === "string" ? code : undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Whether a write error is a PostgreSQL type-mismatch that warrants | ||
| * invalidating cached introspection so the cell editor re-renders with the | ||
| * correct column type. | ||
| * | ||
| * Used by the write-error self-heal path in the row mutation hooks. This only | ||
| * inspects the error; callers MUST still surface the original error to the | ||
| * user (e.g. via `studio_operation_error`). | ||
| */ | ||
| export function isPostgresTypeMismatchError(error: unknown): boolean { | ||
| const code = getPostgresErrorCode(error); | ||
| return code != null && TYPE_MISMATCH_SQLSTATES.has(code); | ||
| } | ||
|
|
||
| /** | ||
| * Type helper for documentation: the error reaching mutation handlers is an | ||
| * `AdapterError` carrying the driver's SQLSTATE `code`. | ||
| */ | ||
| export type PostgresWriteError = AdapterError & { code?: string }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import type { QueryClient } from "@tanstack/react-query"; | ||
|
|
||
| import { isPostgresTypeMismatchError } from "../../data/postgres-core/postgres-error"; | ||
|
|
||
| /** | ||
| * Stable React Query key for the introspection query. | ||
| * | ||
| * The manual "refresh schema" action, the write-error self-heal path, and any | ||
| * future invalidation all go through this key so there is a single place that | ||
| * owns the cache identity of the DB schema. | ||
| */ | ||
| export const INTROSPECTION_QUERY_KEY: ["introspection"] = ["introspection"]; | ||
|
|
||
| /** | ||
| * Invalidate cached introspection and trigger a refetch of any active | ||
| * introspection query. | ||
| * | ||
| * This is the single shared mechanism used by: | ||
| * - the manual "refresh schema" button (via `useIntrospection().refreshSchema`) | ||
| * - the write-error self-heal path (via {@link selfHealOnWriteError}) | ||
| * | ||
| * `invalidateQueries` marks the query stale and refetches active observers, | ||
| * so the editor re-renders with the freshly introspected column types. | ||
| */ | ||
| export async function refreshIntrospection( | ||
| queryClient: QueryClient, | ||
| ): Promise<void> { | ||
| await queryClient.invalidateQueries({ | ||
| queryKey: INTROSPECTION_QUERY_KEY, | ||
| refetchType: "active", | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Inspect a row-write (insert/update) error and, when it indicates the column | ||
| * type Studio cached no longer matches the live database schema, invalidate | ||
| * cached introspection so the cell editor re-renders with the correct type. | ||
| * | ||
| * The original error is NOT swallowed: callers still surface it to the user | ||
| * (e.g. via `studio_operation_error`). This helper only additionally triggers | ||
| * a background introspection refetch on Postgres SQLSTATE `42804` | ||
| * (`datatype_mismatch`) or `22P02` (`invalid_text_representation`). | ||
| */ | ||
| export function selfHealOnWriteError(args: { | ||
| error: unknown; | ||
| queryClient: QueryClient; | ||
| }): void { | ||
| if (isPostgresTypeMismatchError(args.error)) { | ||
| void refreshIntrospection(args.queryClient); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Clarify the shared refresh mechanism contract.
The window-focus path does not call
refreshIntrospection; it uses React Query's built-inrefetchOnWindowFocusbehavior. State that manual refresh and write-error recovery use the shared helper, while window focus uses the query policy directly. This prevents the architecture document from implying that changes torefresh-introspection.tsalso control focus refetching.Proposed wording
📝 Committable suggestion
🤖 Prompt for AI Agents