<!-- ccr-slack-attribution --> _Requested by **Ali Waseem** · [Slack thread](https://supabase.slack.com/archives/C063LNYJJKS/p1787336596649619)_ **Before:** editing a cell in the table editor could throw, and the edit was silently lost — the typed value vanished and nothing was saved. Separately, opening the SQL editor could throw before the editor rendered, and the global error boundary replaced the entire page, so there was no editor at all until a reload. **After:** a row change with no matching previous row is a no-op instead of a throw, and the SQL editor shows its normal loading state instead of taking down the page. Two independent undefined guards for two confirmed Sentry crashes, one per commit so either can be dropped on its own. **How:** the first commit moves the existing previousRow guard in `useOnRowsChange` above the `changedColumn` computation that dereferences it, and drops the non-null assertion that hid the problem from TypeScript. The second reads the snippet content in `deriveSnippetIdentity` through optional chaining, so a missing `snippets` map, or an entry without a `snippet`, resolves to still-loading — the same answer the old code gave for an id that is not in the map. Behaviour is unchanged in every case that did not crash. ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix. ## What is the current behavior? **[K7M, Cannot read properties of undefined (reading 'idx')](https://supabase.sentry.io/issues/7681899596/)** — 4 events / 1 user — in `apps/studio/components/grid/components/grid/Grid.utils.tsx`. Inside `useOnRowsChange`, the callback passed to `Object.keys(rowData).find(...)` reads the candidate column off `previousRow` through a non-null assertion, three lines above the `if (!previousRow || !changedColumn) return` that was meant to protect it. `rows.find(...)` returns undefined whenever no row matches, and the assertion is why TypeScript never flagged the dereference. The four events came from one user inside about two minutes, so it is deterministic rather than a one-off, and every throw is an edit the user loses. **[K7J, Cannot use 'in' operator to search for a snippet uuid in undefined](https://supabase.sentry.io/issues/7680905437/)** — 1 event / 1 user, full-page crash — in `apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts`, line 331. `deriveSnippetIdentity` applies the `in` operator to its `snippets` argument and then reads `snippets[id].snippet.content`. The parameter is declared required and non-optional, but `snippets` arrived undefined at runtime, so `in` threw and the error reached the global error boundary, which unmounted the whole SQL editor page. The `snippets[id].snippet` read on the same line is a second unguarded dereference: an entry without a `snippet` crashes identically. ## What is the new behavior? Both crashes become no-ops. - Grid: return early when `previousRow` is missing, then compute `changedColumn`, with the assertion removed. When a previous row is found, the code takes exactly the path it took before. - SQL editor: `snippets?.[id]?.snippet?.content === undefined` replaces the `in` check. A missing map, a missing entry, and an entry with no `snippet` all read as still loading, which is what the surrounding code already does while a snippet is being fetched. The parameter type is left required, since the only caller (`useSnippetIdentity.ts`) passes the store's `snippets` record, which is typed as always present — the type is not the thing that was wrong. Two test cases are added to the existing `deriveSnippetIdentity` block, which previously only passed fully populated maps: one for an undefined `snippets` map, and one for an entry missing its `snippet`. ## Additional context **Why `snippets` was undefined is unexplained.** The store initialises it to an empty object (`apps/studio/state/sql-editor/sql-editor-state.ts:34`) and its only reassignment writes an object, so there is no code path in studio that sets it to undefined. This commit is a defensive guard against a crash, not a root-cause fix, and nothing here should be read as an explanation. **Verification:** no local checks were possible in the authoring environment — the checkout has no `node_modules` and `pnpm install` cannot complete there, so typecheck, lint and the studio unit tests could not be run. CI on this PR is the only verification. --------- Co-authored-by: Claude <noreply@anthropic.com>
SQL Editor Overview
Quick run-through of the different building blocks that make up the SQL Editor, to help navigating the codebase a little easier 🙂🙏
UI structure
-
Folders and snippets (in the product menu) are rendered via
SQLEditorMenuandSQLEditorNav, in which the data are all loaded from the API via React Query directly. (Refer to point 3 under "Data Structure" below)SQLEditorMenu: Wraps aroundSQLEditorNav+ renders search input + View running queries buttonSQLEditorNav: Renders the 3 collapsible snippet sections
-
The Tabs interface is powered by a separate state
tabs.ts. (Also used by the Table Editor) -
Route validation to check for snippet validity + last visited snippet lies in the page level on
[id].tsx -
When searching for snippets, we're deliberately opting to render the results as a flat list in
SearchListfor ease of finding (rather than keeping the 3 separate sections / having folders)
Data structure
-
SQL Editor is mainly powered by a Valtio store in
sql-editor-v2.ts, in which most of the data is being managed on the client side for optimistic rendering to keep the editor feeling snappy. (unlike other parts of the dashboard where the data is always invalidated whenever a mutation happens). -
The Valtio store here stores snippets across multiple projects as we aren't using a context provider, though this was a legacy decision (refer to
ProjectContextfor more context on using providers with stores) -
While
SQLEditorNavrenders the folders + snippets directly from the API endpoints via React Query, we still store them in the Valtio store to store some properties used on the client side likesplitSizesandprojectReffor snippets, andstatusfor folders (although it's possible that we can simplify the Valtio store).
Data fetching
-
The endpoint to fetch snippets and folders are via
useSQLSnippetFoldersQueryanduseSqlSnippetsQuery, both of which are paginated (limit set at 100)useSQLSnippetFoldersQuery: Specifically for fetching private snippets and foldersuseSqlSnippetsQuery: For fetching shared and favorite snippets
-
Page fetching is done on demand for the snippets via a "Load more" button due to the complexity of a tree view (we've deliberate avoided an infinite loading UX which we commonly do across other parts of the dashboard)
Data flow
Landing on the SQL Editor
-
Snippets and folders are all initially loaded via React Query in
SQLEditorNav, which are then initialized into the Valtio store viasnapV2.addSnippetcalls in theuseEffects -
On
/editor/sql- We'll redirect users to the last visited snippet if there's one (
/editor/sql/[id]), otherwise will redirect to/editor/sql/new(within[id].tsx)
- We'll redirect users to the last visited snippet if there's one (
-
On
/editor/sql/[id]- We'll load the content of the snippet via
useContentQueryand update the Valtio store viasnapV2.setSnippet
- We'll load the content of the snippet via
Writing a snippet
-
On
/editor/sql/new:- The first character input will update the Valtio store with a snippet skeleton via
snapV2.addSnippetand user will be redirected to/editor/sql/[id], using theidfrom the skeleton - Note that
snapV2.addSnippetonly handles adding snippets to the store and does not queue the snippet for saving - Subsequent character inputs will follow below as per
/editor/sql/[id]
- The first character input will update the Valtio store with a snippet skeleton via
-
On
/editor/sql/[id]:snapV2.setSqlwill be called based on the debounced value of the code editor, in which we'll then queue the snippet for saving viaupsertSnippet.- Note that we do invalidate some React Queries (snippet count, snippets, and folders) after saving via
upsertSnippet, but the invalidation is only triggered if it's a new snippet that's not saved in the DB yet
Running a snippet
-
There's several safeguards in place before we run the query
- Check for destructive operations: Will open a confirmation modal before running the query
- Check for update statements without where clause: Will open a confirmation modal before running the query
- Append a preset limit to the query if it's a select: To prevent accidentally running an expensive query on the database. Users can explicitly opt out of this to set "No limit"
-
If the snippet's name is "Untitled query", we'll also trigger a non-blocking request to rename the snippet via AI. Snippet name will be updated whenever the request completes.
Renaming, Moving, Deleting, Sharing snippets
- These functionalities all call
upsertContentvia React Query directly without going through the Valtio store - We thereafter update the Valtio stores for the SQL Editor and Tabs as required if the upsert is successful
Possible areas to simplify, refactor, or improve
-
updateSnippetandsetSqlcould be consolidated insql-editor-v2.ts -
Refactor renaming a query to have optimistic rendering for consistency on how we update snippets in the
sql-editor-v2.ts -
Implement drag and drop functionality for snippets into folders
-
RenameQueryModalandMoveQueryModal, could callupdateSnippetinstead ofremoveSnippet+addSnippet?