mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 20:35:07 +03:00
## Context Adds a warning if running an `ALTER DATABASE` command that prevents connections to the database. This would lock the dashboard out of the database, and re-configuring the setting will require a direct connection to the DB <img width="623" height="535" alt="image" src="https://github.com/user-attachments/assets/4c388f4a-753b-4fd6-89c5-89dfaa52c859" /> ## To test - Could try running the following command in the SQL editor to check the warning, this will show all the warnings ``` ALTER TABLE colors2 drop column sss; update colors set name = 'test'; alter database postgres connection limit 0; ```
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
import { stripIndent } from 'common-tags'
|
|
import {
|
|
checkAlterDatabaseConnection,
|
|
checkDestructiveQuery,
|
|
checkIfAppendLimitRequired,
|
|
isUpdateWithoutWhere,
|
|
suffixWithLimit,
|
|
} from './SQLEditor.utils'
|
|
import { describe, test, expect, it } from 'vitest'
|
|
|
|
describe('SQLEditor.utils.ts:checkIfAppendLimitRequired', () => {
|
|
test('Should return false if limit passed is <= 0', () => {
|
|
const sql = 'select * from countries;'
|
|
const limit = -1
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return true if limit passed is > 0', () => {
|
|
const sql = 'select * from countries;'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(true)
|
|
})
|
|
test('Should return false if query already has a limit', () => {
|
|
const sql = 'select * from countries limit 10;'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit (check for case-insensitiveness)', () => {
|
|
const sql = 'SELECT * FROM countries LIMIT 10;'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query already has a limit and offset', () => {
|
|
const sql = 'select * from countries limit 10 offset 0;'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(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 = 'select * from countries offset 0 limit 1;'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(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 = 'select * from countries limit'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query uses `FETCH FIRST` instead of limit ', () => {
|
|
const sql = 'select * from countries FETCH FIRST 5 rows only'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query uses `fetch first` instead of limit ', () => {
|
|
const sql = 'select * from countries fetch first 5 rows only'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query uses `fetch first` (with random spaces) instead of limit ', () => {
|
|
const sql = 'select * from countries FETCH FIRST 5 rows only'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query is not a select statement', () => {
|
|
const sql = 'create table test (id int8 primary key, name varchar);'
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if there are multiple queries I', () => {
|
|
const sql1 = `
|
|
select * from countries;
|
|
select * from cities;
|
|
`.trim()
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql1, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if there are multiple queries II', () => {
|
|
const sql1 = `
|
|
select * from countries;
|
|
select * from cities
|
|
`.trim()
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(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 = `
|
|
-- This is a comment
|
|
select * from cities
|
|
`.trim()
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
test('Should return false if query has with a comment II', () => {
|
|
const sql = `
|
|
select * from cities
|
|
-- This is a comment
|
|
`.trim()
|
|
const limit = 100
|
|
const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
|
|
expect(appendAutoLimit).toBe(false)
|
|
})
|
|
})
|
|
|
|
// [Joshen] These will just need to test the cases when appendAutoLimit returns true then
|
|
describe('SQLEditor.utils.ts:suffixWithLimit', () => {
|
|
test('Should add the limit param properly if query ends without a semi colon', () => {
|
|
const sql = 'select * from countries'
|
|
const limit = 100
|
|
const formattedSql = suffixWithLimit(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 = 'select * from countries;'
|
|
const limit = 100
|
|
const formattedSql = suffixWithLimit(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 = 'select * from countries;;;;;;;'
|
|
const limit = 100
|
|
const formattedSql = suffixWithLimit(sql, limit)
|
|
expect(formattedSql).toBe('select * from countries limit 100;')
|
|
})
|
|
})
|
|
|
|
describe(`SQLEditor.utils.ts:checkDestructiveQuery`, () => {
|
|
it('drop statement matches', () => {
|
|
const match = checkDestructiveQuery('drop table films, distributors;')
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('truncate statement matches', () => {
|
|
const match = checkDestructiveQuery('truncate films;')
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('delete statement matches', () => {
|
|
const match = checkDestructiveQuery("delete from films where kind <> 'Musical';")
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('delete statement after another statement matches', () => {
|
|
const match = checkDestructiveQuery(stripIndent`
|
|
select * from films;
|
|
|
|
delete from films where kind <> 'Musical';
|
|
`)
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it("rls policy containing delete doesn't match", () => {
|
|
const match = checkDestructiveQuery(stripIndent`
|
|
create policy "Users can delete their own files"
|
|
on storage.objects for delete to authenticated using (
|
|
bucket id = 'files' and (select auth.uid()) = owner
|
|
);
|
|
`)
|
|
|
|
expect(match).toBe(false)
|
|
})
|
|
|
|
it('capitalized statement matches', () => {
|
|
const match = checkDestructiveQuery("DELETE FROM films WHERE kind <> 'Musical';")
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it("comment containing keyword doesn't match", () => {
|
|
const match = checkDestructiveQuery(stripIndent`
|
|
-- Going to drop this in here, might delete later
|
|
select * from films;
|
|
`)
|
|
|
|
expect(match).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('SQLEditor.utils:updateWithoutWhere', () => {
|
|
it('contains an update query with a where clause', () => {
|
|
const match = isUpdateWithoutWhere(stripIndent`
|
|
UPDATE public.countries SET name = 'New Name' WHERE id = 1;
|
|
`)
|
|
|
|
expect(match).toBe(false)
|
|
})
|
|
|
|
it('contains an update query without a where clause', () => {
|
|
const match = isUpdateWithoutWhere(stripIndent`
|
|
UPDATE public.countries SET name = 'New Name';
|
|
`)
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('contains an update query, with quoted identifiers with a where clause', () => {
|
|
const match = isUpdateWithoutWhere(stripIndent`
|
|
UPDATE "public"."countries" SET name = 'New Name' WHERE id = 1;
|
|
`)
|
|
|
|
expect(match).toBe(false)
|
|
})
|
|
|
|
it('contains an update query, with quoted identifiers without a where clause', () => {
|
|
const match = isUpdateWithoutWhere(stripIndent`
|
|
UPDATE "public"."countries" SET name = 'New Name';
|
|
`)
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('contains both an update query and a delete query, triggers destructive', () => {
|
|
const match = checkDestructiveQuery(stripIndent`
|
|
delete from countries; update countries set name = 'hello';
|
|
`)
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('contains both an update query and a delete query, triggers no where', () => {
|
|
const match = isUpdateWithoutWhere(stripIndent`
|
|
delete from countries; update countries set name = 'hello';
|
|
`)
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
it('contains both an update query and a delete query, triggers no where', () => {
|
|
const match = isUpdateWithoutWhere(stripIndent`
|
|
delete from countries; update countries set name = 'hello';
|
|
`)
|
|
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('should catch potential destructive queries', () => {
|
|
const DESTRUCTIVE_QUERIES = [
|
|
`ALTER TABLE test DROP COLUMN test;`,
|
|
`DELETE FROM test;`,
|
|
`DROP TABLE test;`,
|
|
`TRUNCATE TABLE test;`,
|
|
]
|
|
|
|
DESTRUCTIVE_QUERIES.forEach((query) => {
|
|
expect(checkDestructiveQuery(query), `Query ${query} should be destructive`).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('SQLEditor.utils:checkAlterDatabaseConnection', () => {
|
|
it('detects connection limit 0', () => {
|
|
const match = checkAlterDatabaseConnection('alter database postgres connection limit 0;')
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('detects allow_connections false', () => {
|
|
const match = checkAlterDatabaseConnection('alter database postgres allow_connections false;')
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('detects case-insensitive match', () => {
|
|
const match = checkAlterDatabaseConnection('ALTER DATABASE postgres CONNECTION LIMIT 0;')
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('detects statement among multiple statements', () => {
|
|
const match = checkAlterDatabaseConnection(stripIndent`
|
|
select * from countries;
|
|
alter database postgres connection limit 0;
|
|
`)
|
|
expect(match).toBe(true)
|
|
})
|
|
|
|
it('does not flag unrelated alter database statement', () => {
|
|
const match = checkAlterDatabaseConnection(
|
|
'alter database postgres set statement_timeout = 60000;'
|
|
)
|
|
expect(match).toBe(false)
|
|
})
|
|
|
|
it('does not flag non-alter statements', () => {
|
|
const match = checkAlterDatabaseConnection('select * from countries;')
|
|
expect(match).toBe(false)
|
|
})
|
|
|
|
it('ignores statements inside comments', () => {
|
|
const match = checkAlterDatabaseConnection(stripIndent`
|
|
-- alter database postgres connection limit 0;
|
|
select 1;
|
|
`)
|
|
expect(match).toBe(false)
|
|
})
|
|
|
|
it('detects both dangerous statements in same query', () => {
|
|
const match = checkAlterDatabaseConnection(stripIndent`
|
|
alter database postgres connection limit 0;
|
|
alter database postgres allow_connections false;
|
|
`)
|
|
expect(match).toBe(true)
|
|
})
|
|
})
|