mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## Context Related to Notebooks/Explorers - this one's just shifting files from the SQLEditor into more generic folders from a file organization POV, such that files under the Explorer folder have no dependency on files within the SQLEditor folder Mainly - UtilityTabResults.utils: `getSqlErrorLines` - Moved into `data/sql/utils.ts` - SQLEditor.utils: `applyAutoLimit`, `getSqlErrorLines`, `trimTrailingSemicolons` - Moved into `data/sql/utils.ts` - SQLEditor/UtilityPanel: `ResultCell`, `Results`, `CellDetailPanel` - Moved into `components/ui/DataGridResults` - Also shifted corresponding tests over here - Also addressed some `any` type casts ## To test - Just need to ensure that the SQL Editor still works as expected <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Standardized query results across the Studio with a shared data grid. * Improved result-table formatting, column sizing, clipboard handling, and large-value display. * Added safer automatic row limits for eligible SQL queries. * Centralized SQL error display and formatting utilities. * **Refactor** * Improved type safety for query rows and cell values. * **Tests** * Added comprehensive coverage for result-grid and SQL utility behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
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/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 }
|
|
}
|