mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 06:39:09 +03:00
## Problem attempt to [fix this ticket](https://supabase.frontapp.com/inboxes/teams/folders/27650590/unassigned/116845868830): when the studio ai assistant runs a query with previously set database opt-in, `execute_sql` returns every row while the query cell's footer says "Limit 100 rows" causing in case of large amount of returned rows browser to freeze + the chat fails with "Sorry, I'm having trouble responding right now" until the user clears it ## Solution - `execute_sql` now keeps the first 100 rows using the cell's existing `DEFAULT_CELL_ROW_LIMIT` - uses in history sanitizer the same `toModelRows` helper - chats saved before this change have no `totalRows` so the helper falls back to `rows.length` and cuts them to 100 too <!-- ## Preview links If relevant, include links to changed pages for easy review access. Copy the preview base URL from the Vercel bot comment on this PR. Use the following table as an example template. | Site | Live | Preview | Search for | | -------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------- | | WWW | [/blog/your-post](https://supabase.com/blog/your-post) | [/blog/your-post](https://zone-www-dot-com-git-branch-name-supabase.vercel.app/blog/your-post) | unique phrase from the change | | Docs | [/docs/guides/your-page](https://supabase.com/docs/guides/your-page) | [/docs/guides/your-page](https://docs-git-branch-name-supabase.vercel.app/docs/guides/your-page) | unique phrase from the change | | Studio | [/dashboard](https://supabase.com/dashboard) | [/dashboard](https://studio-git-branch-name-supabase.vercel.app/dashboard) | unique phrase from the change | | Design system | [/design-system](https://supabase.com/design-system) | [/design-system](https://design-system-git-branch-name-supabase.vercel.app/design-system) | unique phrase from the change | | UI library | [/library](https://supabase.com/library) | [/library](https://ui-library-git-branch-name-supabase.vercel.app/library) | unique phrase from the change | | Knowledge base | [/kb/guides/your-page](https://supabase.com/kb/guides/your-page) | [/kb/guides/your-page](https://kb-git-branch-name-supabase.vercel.app/kb/guides/your-page) | unique phrase from the change | --> <!-- ## Additional context Optionally add any other context or screenshots. --> ## Review instructions 1. In a test project, run this in the SQL editor: ```sql create table "emailSubscriber" (id bigint generated always as identity primary key, email text); create table "stripeCustomer" (id bigint generated always as identity primary key, email text); insert into "emailSubscriber" (email) select md5(g::text) || '@example.com' from generate_series(1, 50000) g; insert into "stripeCustomer" (email) select md5(g::text) || '@example.com' from generate_series(1, 1000) g; 2. on the live dashboard, open the assistant in a new chat and send "i want all the emails in emailSubscriber where the email is not in the stripeCustomer table" 3. in the opt in requested settings choose "Schema, Logs & Database Data" 4. then click run query, the cell should show 100 rows (vs 50000 rows in prod) ## Checklist Check all before review: - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) - [x] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs [style guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide) --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
81 lines
3.8 KiB
TypeScript
81 lines
3.8 KiB
TypeScript
import { literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
|
|
|
|
/**
|
|
* Pick which lines to render for a SQL editor error.
|
|
*
|
|
* pg-meta returns `formattedError` with multi-line ERROR/HINT/LINE output from Postgres.
|
|
* Historically only `message` was reliably populated end-to-end, which is why the UI also
|
|
* falls back to splitting `message` on newlines — e.g. the enhanced permission-denied HINT
|
|
* added by supabase/postgres#2084 arrives in the message body on some paths.
|
|
*
|
|
* Returns an empty array when the error is single-line (message only) — callers fall back to
|
|
* a plain "Error: {message}" rendering in that case.
|
|
*/
|
|
export function getSqlErrorLines(error: { message?: string; formattedError?: string }): string[] {
|
|
const formattedLines = (error.formattedError?.split('\n') ?? []).filter((x) => x.length > 0)
|
|
if (formattedLines.length > 0) return formattedLines
|
|
|
|
const messageLines = (error.message?.split('\n') ?? []).filter((x) => x.length > 0)
|
|
return messageLines.length > 1 ? messageLines : []
|
|
}
|
|
|
|
/**
|
|
* Removes trailing `;` characters from a safe SQL fragment. Only ever removes
|
|
* existing terminators — never adds text — so the result is exactly as safe
|
|
* as the input; the brand carries over intentionally. This is the one place
|
|
* in the file allowed to reassert `SafeSqlFragment` on a derived string —
|
|
* every other function composes new fragments through `safeSql`/`literal`.
|
|
*/
|
|
export function trimTrailingSemicolons(sql: SafeSqlFragment): SafeSqlFragment {
|
|
return sql.replace(/;+\s*$/, '') as SafeSqlFragment
|
|
}
|
|
|
|
// [Joshen] Just FYI as well the checks here on whether to append limit is quite restricted
|
|
// This is to prevent dashboard from accidentally appending limit to the end of a query
|
|
// thats not supposed to have any, since there's too many cases to cover.
|
|
// We can however look into making this logic better in the future
|
|
// i.e It's harder to append the limit param, than just leaving the query as it is
|
|
// Otherwise we'd need a full on parser to do this properly
|
|
//
|
|
// Only accepts `SafeSqlFragment`: this decides whether to build (and builds)
|
|
// a new SQL fragment that gets executed, so every caller — including ones
|
|
// that only want the `appendAutoLimit` flag for a display hint — must already
|
|
// hold safe SQL. Composes the ` limit N;` suffix through `safeSql`/`literal`
|
|
// rather than gluing raw template-literal text onto the fragment and casting
|
|
// the result, so the only new content this function ever stamps safe is an
|
|
// internally-generated integer literal, never arbitrary concatenated text.
|
|
export function applyAutoLimit(
|
|
sql: SafeSqlFragment,
|
|
limit: number = 0
|
|
): { sql: SafeSqlFragment; appendAutoLimit: boolean } {
|
|
// Remove lines and whitespaces to use for checking
|
|
const cleanedSql = sql.trim().replaceAll('\n', ' ').replaceAll(/\s+/g, ' ')
|
|
|
|
// Check how many queries
|
|
const regMatch = cleanedSql.matchAll(/[a-zA-Z]*[0-9]*[;]+/g)
|
|
const queries = new Array(...regMatch)
|
|
const indexSemiColon = cleanedSql.lastIndexOf(';')
|
|
const hasComments = cleanedSql.includes('--')
|
|
const hasMultipleQueries =
|
|
queries.length > 1 || (indexSemiColon > 0 && indexSemiColon !== cleanedSql.length - 1)
|
|
|
|
// Check if need to auto limit rows
|
|
const appendAutoLimit =
|
|
limit > 0 &&
|
|
!hasComments &&
|
|
!hasMultipleQueries &&
|
|
cleanedSql.toLowerCase().startsWith('select') &&
|
|
!cleanedSql.toLowerCase().match(/fetch\s+(first|next)/i) &&
|
|
!cleanedSql.match(/limit$/i) &&
|
|
!cleanedSql.match(/limit;$/i) &&
|
|
!cleanedSql.match(/limit [0-9]* offset [0-9]*\s*[;]?$/i) &&
|
|
!cleanedSql.match(/limit [0-9]*\s*[;]?$/i)
|
|
|
|
if (!appendAutoLimit) return { sql, appendAutoLimit: false }
|
|
|
|
const core = cleanedSql.endsWith(';') ? trimTrailingSemicolons(sql) : sql
|
|
const suffixed = safeSql`${core} limit ${literal(limit)};`
|
|
|
|
return { sql: suffixed, appendAutoLimit: true }
|
|
}
|