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 -->
242 lines
9.8 KiB
TypeScript
242 lines
9.8 KiB
TypeScript
import { safeSql } from '@supabase/pg-meta'
|
|
import { describe, expect, it, test } from 'vitest'
|
|
|
|
import { applyAutoLimit, getSqlErrorLines, trimTrailingSemicolons } from '../utils'
|
|
|
|
describe('getSqlErrorLines', () => {
|
|
it('returns formattedError lines when present', () => {
|
|
const lines = getSqlErrorLines({
|
|
message: 'permission denied for table users',
|
|
formattedError:
|
|
'ERROR: 42501: permission denied for table users\n' +
|
|
'HINT: To grant access to anon on a specific table:\n' +
|
|
' GRANT SELECT ON TABLE public.users TO anon;',
|
|
})
|
|
|
|
expect(lines).toEqual([
|
|
'ERROR: 42501: permission denied for table users',
|
|
'HINT: To grant access to anon on a specific table:',
|
|
' GRANT SELECT ON TABLE public.users TO anon;',
|
|
])
|
|
})
|
|
|
|
it('strips empty lines from formattedError', () => {
|
|
const lines = getSqlErrorLines({
|
|
formattedError: 'ERROR: boom\n\nHINT: retry\n',
|
|
})
|
|
|
|
expect(lines).toEqual(['ERROR: boom', 'HINT: retry'])
|
|
})
|
|
|
|
it('falls back to message lines when formattedError is missing and message is multi-line', () => {
|
|
const lines = getSqlErrorLines({
|
|
message:
|
|
'ERROR: 42501: permission denied for table users\n' +
|
|
'HINT: To grant access to anon on a specific table:\n' +
|
|
' GRANT SELECT ON TABLE public.users TO anon;',
|
|
})
|
|
|
|
expect(lines).toEqual([
|
|
'ERROR: 42501: permission denied for table users',
|
|
'HINT: To grant access to anon on a specific table:',
|
|
' GRANT SELECT ON TABLE public.users TO anon;',
|
|
])
|
|
})
|
|
|
|
it('returns empty array for a single-line message so callers render the fallback', () => {
|
|
const lines = getSqlErrorLines({ message: 'permission denied for table users' })
|
|
expect(lines).toEqual([])
|
|
})
|
|
|
|
it('returns empty array when both fields are missing', () => {
|
|
expect(getSqlErrorLines({})).toEqual([])
|
|
})
|
|
|
|
it('returns empty array when message is an empty string', () => {
|
|
expect(getSqlErrorLines({ message: '' })).toEqual([])
|
|
})
|
|
|
|
it('returns empty array when message only contains whitespace newlines', () => {
|
|
// Only empty segments after filtering — treated as single-line
|
|
expect(getSqlErrorLines({ message: '\n\n' })).toEqual([])
|
|
})
|
|
|
|
it('prefers formattedError even when message is also multi-line', () => {
|
|
const lines = getSqlErrorLines({
|
|
message: 'message line 1\nmessage line 2',
|
|
formattedError: 'formatted line 1\nformatted line 2',
|
|
})
|
|
|
|
expect(lines).toEqual(['formatted line 1', 'formatted line 2'])
|
|
})
|
|
|
|
it('falls through to message when formattedError is empty string', () => {
|
|
const lines = getSqlErrorLines({
|
|
message: 'ERROR: line 1\nHINT: line 2',
|
|
formattedError: '',
|
|
})
|
|
|
|
expect(lines).toEqual(['ERROR: line 1', 'HINT: line 2'])
|
|
})
|
|
})
|
|
|
|
describe('trimTrailingSemicolons', () => {
|
|
test('removes a single trailing semicolon', () => {
|
|
const sql = safeSql`select * from countries;`
|
|
expect(trimTrailingSemicolons(sql)).toBe('select * from countries')
|
|
})
|
|
test('removes multiple trailing semicolons', () => {
|
|
const sql = safeSql`select * from countries;;;;;;;`
|
|
expect(trimTrailingSemicolons(sql)).toBe('select * from countries')
|
|
})
|
|
test('leaves a fragment with no trailing semicolon unchanged', () => {
|
|
const sql = safeSql`select * from countries`
|
|
expect(trimTrailingSemicolons(sql)).toBe('select * from countries')
|
|
})
|
|
test('does not touch semicolons that are not trailing', () => {
|
|
const sql = safeSql`select 1; select 2`
|
|
expect(trimTrailingSemicolons(sql)).toBe('select 1; select 2')
|
|
})
|
|
})
|
|
|
|
describe('applyAutoLimit', () => {
|
|
test('Should return false if limit passed is <= 0', () => {
|
|
const sql = safeSql`select * from countries;`
|
|
const limit = -1
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return true if limit passed is > 0', () => {
|
|
const sql = safeSql`select * from countries;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(true)
|
|
})
|
|
test('Should return false if query already has a limit', () => {
|
|
const sql = safeSql`select * from countries limit 10;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit (check for case-insensitiveness)', () => {
|
|
const sql = safeSql`SELECT * FROM countries LIMIT 10;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit with whitespace before the semi colon', () => {
|
|
const sql = safeSql`select * from countries limit 10 ;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit and offset', () => {
|
|
const sql = safeSql`select * from countries limit 10 offset 0;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit and offset with whitespace before the semi colon', () => {
|
|
const sql = safeSql`select * from countries limit 10 offset 0 ;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit and offset (flip order of limit and offset)', () => {
|
|
const sql = safeSql`select * from countries offset 0 limit 1;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit, even if no value provided for limit', () => {
|
|
const sql = safeSql`select * from countries limit`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query uses `FETCH FIRST` instead of limit ', () => {
|
|
const sql = safeSql`select * from countries FETCH FIRST 5 rows only`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query uses `fetch first` instead of limit ', () => {
|
|
const sql = safeSql`select * from countries fetch first 5 rows only`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query uses `fetch first` (with random spaces) instead of limit ', () => {
|
|
const sql = safeSql`select * from countries FETCH FIRST 5 rows only`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query is not a select statement', () => {
|
|
const sql = safeSql`create table test (id int8 primary key, name varchar);`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if there are multiple queries I', () => {
|
|
const sql1 = safeSql`select * from countries;
|
|
select * from cities;`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql1, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if there are multiple queries II', () => {
|
|
const sql1 = safeSql`select * from countries;
|
|
select * from cities`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql1, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
// [Joshen] Opting to just avoid appending in this case to prevent making the logic overly complex atm
|
|
test('Should return false if query has with a comment I', () => {
|
|
const sql = safeSql`-- This is a comment
|
|
select * from cities`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query has with a comment II', () => {
|
|
const sql = safeSql`select * from cities
|
|
-- This is a comment`
|
|
const limit = 100
|
|
const { appendAutoLimit } = applyAutoLimit(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
|
|
// [Joshen] These will just need to test the cases when appendAutoLimit returns true then
|
|
test('Should add the limit param properly if query ends without a semi colon', () => {
|
|
const sql = safeSql`select * from countries`
|
|
const limit = 100
|
|
const { sql: formattedSql } = applyAutoLimit(sql, limit)
|
|
expect(formattedSql).toBe('select * from countries limit 100;')
|
|
})
|
|
test('Should add the limit param properly if query ends with a semi colon', () => {
|
|
const sql = safeSql`select * from countries;`
|
|
const limit = 100
|
|
const { sql: formattedSql } = applyAutoLimit(sql, limit)
|
|
expect(formattedSql).toBe('select * from countries limit 100;')
|
|
})
|
|
test('Should add the limit param properly if query ends with multiple semi colon', () => {
|
|
const sql = safeSql`select * from countries;;;;;;;`
|
|
const limit = 100
|
|
const { sql: formattedSql } = applyAutoLimit(sql, limit)
|
|
expect(formattedSql).toBe('select * from countries limit 100;')
|
|
})
|
|
test('Should not append a limit if query already has one with whitespace before the semi colon', () => {
|
|
const sql = safeSql`select * from countries limit 10 ;`
|
|
const limit = 100
|
|
const { sql: formattedSql } = applyAutoLimit(sql, limit)
|
|
expect(formattedSql).toBe('select * from countries limit 10 ;')
|
|
})
|
|
test('returns the SafeSqlFragment result unchanged when no limit is appended', () => {
|
|
const sql = safeSql`select * from countries limit 10;`
|
|
const { sql: formattedSql } = applyAutoLimit(sql, 100)
|
|
expect(formattedSql).toBe(sql)
|
|
})
|
|
})
|