Files
supabase/packages/pg-meta/test/query/advanced-query.test.ts
T
Charis 0433eeb5f5 feat(studio): mark sql provenance for safety (#45336)
Mark provenance of SQL via the branded types SafeSqlFragment and
UntrustedSqlFragment. Only SafeSqlFragment should be executed;
UntrustedSqlFragments require some kind of implicit user approval (show
on screen + user has to click something) before they are promoted to
SafeSqlFragment.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Editor and RLS tester show loading states for inferred/generated SQL
and include a dedicated user SQL editor for safer edits.

* **Refactor**
* Platform-wide SQL handling tightened: snippets and AI-generated SQL
are treated as untrusted/display-only until promoted, improving safety
and consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 13:08:06 -04:00

601 lines
21 KiB
TypeScript

import { afterAll, describe, expect, test } from 'vitest'
import { ident, joinSqlFragments, safeSql } from '../../src/pg-format'
import { Query } from '../../src/query/Query'
import { cleanupRoot, createTestDatabase } from '../db/utils'
type TestDb = Awaited<ReturnType<typeof createTestDatabase>>
async function validateSql(db: TestDb, sql: string): Promise<any> {
try {
const result = await db.executeQuery(sql)
return result
} catch (error) {
throw new Error(`Invalid SQL generated: ${sql}\nError: ${error}`)
}
}
const withTestDatabase = (name: string, fn: (db: TestDb) => Promise<void>) => {
test(name, async () => {
const db = await createTestDatabase()
try {
// Setup test tables with special characters, spaces, and quotes
await db.executeQuery(`
CREATE TABLE "public"."normal_table" (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE "public"."table with spaces" (
id SERIAL PRIMARY KEY,
"column with spaces" TEXT,
"quoted""column" TEXT,
"quoted'column" TEXT,
"camelCaseColumn" TEXT,
"special#$%^&Column" TEXT
);
CREATE TABLE "public"."quoted""table" (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE "public"."quoted'table" (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE "public"."camelCaseTable" (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE "public"."special#$%^&Table" (
id SERIAL PRIMARY KEY,
name TEXT
);
`)
// Insert test data into each table
await db.executeQuery(`
-- Add data to normal_table
INSERT INTO "public"."normal_table" (name)
VALUES
('John Doe'),
('Jane Smith'),
('O''Reilly Books'),
(NULL);
-- Add data to table with spaces
INSERT INTO "public"."table with spaces" (
"column with spaces",
"quoted""column",
"quoted'column",
"camelCaseColumn",
"special#$%^&Column"
)
VALUES
('value with spaces', 'value with "quotes"', 'value with ''quotes''', 'camelCaseValue', 'special#$%^&Value'),
('another value', 'another "quoted" value', 'another ''quoted'' value', 'anotherCamelCase', 'another#$%^&');
-- Add data to quoted"table
INSERT INTO "public"."quoted""table" (name)
VALUES
('quoted table row 1'),
('quoted table row 2');
-- Add data to quoted'table
INSERT INTO "public"."quoted'table" (name)
VALUES
('single quoted table row 1'),
('single quoted table row 2');
-- Add data to camelCaseTable
INSERT INTO "public"."camelCaseTable" (name)
VALUES
('camel case table row 1'),
('camel case table row 2');
-- Add data to special#$%^&Table
INSERT INTO "public"."special#$%^&Table" (name)
VALUES
('special char table row 1'),
('special char table row 2');
`)
await fn(db)
} finally {
await db.cleanup()
}
})
}
describe('Advanced Query Tests', () => {
afterAll(async () => {
await cleanupRoot()
})
describe('Special Table and Column Names', () => {
withTestDatabase('should handle tables with spaces', async (db) => {
const query = new Query()
const sql = query.from('table with spaces', 'public').select().toSql()
expect(sql).toMatchInlineSnapshot(`"select * from public."table with spaces";"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0]['column with spaces']).toBe('value with spaces')
expect(result[1]['column with spaces']).toBe('another value')
})
withTestDatabase('should handle tables with double quotes', async (db) => {
const query = new Query()
const sql = query.from('quoted"table', 'public').select().toSql()
expect(sql).toMatchInlineSnapshot(`"select * from public."quoted""table";"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0].name).toBe('quoted table row 1')
expect(result[1].name).toBe('quoted table row 2')
})
withTestDatabase('should handle tables with single quotes', async (db) => {
const query = new Query()
const sql = query.from("quoted'table", 'public').select().toSql()
expect(sql).toMatchInlineSnapshot(`"select * from public."quoted'table";"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0].name).toBe('single quoted table row 1')
expect(result[1].name).toBe('single quoted table row 2')
})
withTestDatabase('should handle camelCase table names', async (db) => {
const query = new Query()
const sql = query.from('camelCaseTable', 'public').select().toSql()
expect(sql).toMatchInlineSnapshot(`"select * from public."camelCaseTable";"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0].name).toBe('camel case table row 1')
expect(result[1].name).toBe('camel case table row 2')
})
withTestDatabase('should handle tables with special characters', async (db) => {
const query = new Query()
const sql = query.from('special#$%^&Table', 'public').select().toSql()
expect(sql).toMatchInlineSnapshot(`"select * from public."special#$%^&Table";"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0].name).toBe('special char table row 1')
expect(result[1].name).toBe('special char table row 2')
})
withTestDatabase('should handle columns with spaces', async (db) => {
const query = new Query()
const sql = query
.from('table with spaces', 'public')
.select(safeSql`"column with spaces"`)
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select "column with spaces" from public."table with spaces";"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0]['column with spaces']).toBe('value with spaces')
expect(result[1]['column with spaces']).toBe('another value')
})
withTestDatabase('should handle columns with double quotes', async (db) => {
const query = new Query()
const sql = query
.from('table with spaces', 'public')
.select(safeSql`"quoted""column"`)
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select "quoted""column" from public."table with spaces";"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0]['quoted"column']).toBe('value with "quotes"')
expect(result[1]['quoted"column']).toBe('another "quoted" value')
})
withTestDatabase('should handle columns with single quotes', async (db) => {
const query = new Query()
const sql = query
.from('table with spaces', 'public')
.select(safeSql`"quoted'column"`)
.toSql()
expect(sql).toMatchInlineSnapshot(`"select "quoted'column" from public."table with spaces";"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0]["quoted'column"]).toBe("value with 'quotes'")
expect(result[1]["quoted'column"]).toBe("another 'quoted' value")
})
withTestDatabase('should handle camelCase column names', async (db) => {
const query = new Query()
const sql = query
.from('table with spaces', 'public')
.select(safeSql`"camelCaseColumn"`)
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select "camelCaseColumn" from public."table with spaces";"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0].camelCaseColumn).toBe('camelCaseValue')
expect(result[1].camelCaseColumn).toBe('anotherCamelCase')
})
withTestDatabase('should handle columns with special characters', async (db) => {
const query = new Query()
const sql = query
.from('table with spaces', 'public')
.select(safeSql`"special#$%^&Column"`)
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select "special#$%^&Column" from public."table with spaces";"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0]['special#$%^&Column']).toBe('special#$%^&Value')
expect(result[1]['special#$%^&Column']).toBe('another#$%^&')
})
})
describe('Complex Queries with Special Names', () => {
withTestDatabase('should handle filtering on columns with spaces', async (db) => {
// First ensure the table exists with the right column
await db.executeQuery(`
DROP TABLE IF EXISTS "public"."table with spaces";
CREATE TABLE "public"."table with spaces" (
id SERIAL PRIMARY KEY,
"column with spaces" TEXT
);
-- Insert test data
INSERT INTO "public"."table with spaces" ("column with spaces")
VALUES ('test value'), ('other value');
`)
const query = new Query()
// Specify the column name without extra quotes in the filter
// The Query class handles the proper quoting
const sql = query
.from('table with spaces', 'public')
.select()
.filter('column with spaces', '=', 'test value')
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select * from public."table with spaces" where "column with spaces" = 'test value';"`
)
// Validate the generated SQL directly against the database
const result = await validateSql(db, sql)
expect(result.length).toBe(1)
expect(result[0]['column with spaces']).toBe('test value')
})
withTestDatabase('should handle filtering with values containing quotes', async (db) => {
await db.executeQuery(`
INSERT INTO "public"."normal_table" (name)
VALUES ('O''Reilly');
`)
const query = new Query()
const sql = query
.from('normal_table', 'public')
.select()
.filter('name', '=', "O'Reilly")
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select * from public.normal_table where name = 'O''Reilly';"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(1)
expect(result[0].name).toBe("O'Reilly")
})
withTestDatabase('should handle updating with values containing quotes', async (db) => {
const query = new Query()
const sql = query
.from('normal_table', 'public')
.update({ name: "John O'Reilly" }, { returning: true })
.filter('id', '=', 1)
.toSql()
expect(sql).toMatchInlineSnapshot(
`"update public.normal_table set (name) = (select name from json_populate_record(null::public.normal_table, '{"name":"John O''Reilly"}')) where id = 1 returning *;"`
)
await validateSql(db, sql)
})
withTestDatabase('should handle inserting with values containing quotes', async (db) => {
const query = new Query()
const sql = query
.from('normal_table', 'public')
.insert([{ name: "John O'Reilly" }], { returning: true })
.toSql()
expect(sql).toMatchInlineSnapshot(
`"insert into public.normal_table (name) select name from jsonb_populate_recordset(null::public.normal_table, '[{"name":"John O''Reilly"}]') returning *;"`
)
await validateSql(db, sql)
})
})
describe('Advanced SQL Generation and Validation', () => {
withTestDatabase(
'should generate valid select with multiple filters and sorting',
async (db) => {
await db.executeQuery(`
DELETE FROM "public"."normal_table";
INSERT INTO "public"."normal_table" (id, name)
VALUES
(11, 'John Smith'),
(12, 'John Doe'),
(13, 'Jane Smith'),
(14, 'Someone Else');
`)
const query = new Query()
const sql = query
.from('normal_table', 'public')
.select(safeSql`id, name`)
.filter('id', '>', 10)
.filter('name', '~~', '%John%')
.order('normal_table', 'name', true, false)
.range(0, 9)
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select id, name from public.normal_table where id > 10 and name::text ~~ '%John%' order by normal_table.name asc nulls last limit 10 offset 0;"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(2)
expect(result[0].name).toBe('John Doe') // Alphabetically first
expect(result[1].name).toBe('John Smith')
expect(result.every((row: any) => row.id > 10)).toBe(true)
}
)
withTestDatabase('should generate valid insert with returning clause', async (db) => {
const query = new Query()
const sql = query
.from('normal_table', 'public')
.insert([{ name: 'John Doe' }], { returning: true })
.toSql()
expect(sql).toMatchInlineSnapshot(
`"insert into public.normal_table (name) select name from jsonb_populate_recordset(null::public.normal_table, '[{"name":"John Doe"}]') returning *;"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(1)
expect(result[0].name).toBe('John Doe')
})
withTestDatabase('should generate valid update with filtering', async (db) => {
await db.executeQuery(`
-- Clear and insert test data
DELETE FROM "public"."normal_table";
INSERT INTO "public"."normal_table" (id, name)
VALUES (1, 'Original Name') ON CONFLICT (id) DO UPDATE SET name = 'Original Name';
`)
const query = new Query()
const sql = query
.from('normal_table', 'public')
.update({ name: 'Updated Name' }, { returning: true })
.filter('id', '=', 1)
.toSql()
expect(sql).toMatchInlineSnapshot(
`"update public.normal_table set (name) = (select name from json_populate_record(null::public.normal_table, '{"name":"Updated Name"}')) where id = 1 returning *;"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(1)
expect(result[0].id).toBe(1)
expect(result[0].name).toBe('Updated Name')
// Verify the update was actually persisted
const verifyResult = await db.executeQuery('SELECT * FROM public.normal_table WHERE id = 1')
expect(verifyResult[0].name).toBe('Updated Name')
})
withTestDatabase('should generate valid delete with filtering', async (db) => {
await db.executeQuery(`
-- Clear and insert test data
DELETE FROM "public"."normal_table";
INSERT INTO "public"."normal_table" (id, name)
VALUES (1, 'To Be Deleted') ON CONFLICT (id) DO UPDATE SET name = 'To Be Deleted';
`)
const query = new Query()
const sql = query
.from('normal_table', 'public')
.delete({ returning: true })
.filter('id', '=', 1)
.toSql()
expect(sql).toMatchInlineSnapshot(
'"delete from public.normal_table where id = 1 returning *;"'
)
const result = await validateSql(db, sql)
expect(result.length).toBe(1)
expect(result[0].id).toBe(1)
expect(result[0].name).toBe('To Be Deleted')
// Verify the row was actually deleted
const verifyResult = await db.executeQuery('SELECT * FROM public.normal_table WHERE id = 1')
expect(verifyResult.length).toBe(0)
})
withTestDatabase('should generate valid count with filtering', async (db) => {
await db.executeQuery(`
-- Clear and insert test data
DELETE FROM "public"."normal_table";
INSERT INTO "public"."normal_table" (name)
VALUES ('John Smith'), ('John Doe'), ('Jane Doe');
`)
const query = new Query()
const sql = query
.from('normal_table', 'public')
.count()
.filter('name', '~~', '%John%')
.toSql()
expect(sql).toMatchInlineSnapshot(
'"select count(*) from public.normal_table where name::text ~~ \'%John%\';"'
)
const result = await validateSql(db, sql)
expect(result[0].count).toBe(2) // PostgreSQL returns count as string
})
withTestDatabase('should generate valid truncate query', async (db) => {
await db.executeQuery(`
INSERT INTO "public"."normal_table" (name)
VALUES ('Test Row 1'), ('Test Row 2');
`)
// Verify data exists
const beforeCount = await db.executeQuery(`SELECT COUNT(*) FROM "public"."normal_table"`)
expect(parseInt(beforeCount[0].count)).toBeGreaterThan(0)
const query = new Query()
const sql = query.from('normal_table', 'public').truncate().toSql()
expect(sql).toMatchInlineSnapshot('"truncate public.normal_table;"')
await validateSql(db, sql)
// Verify truncate worked
const afterCount = await db.executeQuery(`SELECT COUNT(*) FROM "public"."normal_table"`)
expect(parseInt(afterCount[0].count)).toBe(0)
})
})
describe('Corner Cases and Error Handling', () => {
withTestDatabase('should throw error for delete without filters', async () => {
const query = new Query()
const action = query.from('normal_table', 'public').delete({ returning: true })
expect(() => action.toSql()).toThrow(/no filters/)
})
withTestDatabase('should throw error for update without filters', async () => {
const query = new Query()
const action = query.from('normal_table', 'public').update({ name: 'Updated Name' })
expect(() => action.toSql()).toThrow(/no filters/)
})
withTestDatabase('should throw error for insert without values', async () => {
const query = new Query()
// We're passing an empty array to test the runtime error
const action = query.from('normal_table', 'public').insert([] as any, { returning: true })
expect(() => action.toSql()).toThrow(/no value to insert/)
})
withTestDatabase('should handle special characters in values', async (db) => {
const query = new Query()
const sql = query
.from('normal_table', 'public')
.select()
.filter('name', '=', 'Special $ ^ & * ( ) _ + { } | : < > ? characters')
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select * from public.normal_table where name = 'Special $ ^ & * ( ) _ + { } | : < > ? characters';"`
)
await validateSql(db, sql)
})
})
describe('Advanced Filtering', () => {
withTestDatabase('should handle "in" operator with array values', async (db) => {
await db.executeQuery(`
DELETE FROM "public"."normal_table";
INSERT INTO "public"."normal_table" (id, name)
VALUES
(1, 'Row 1'),
(2, 'Row 2'),
(3, 'Row 3'),
(4, 'Row 4');
`)
const query = new Query()
const sql = query
.from('normal_table', 'public')
.select()
.filter('id', 'in', [1, 2, 3])
.toSql()
expect(sql).toMatchInlineSnapshot(`"select * from public.normal_table where id in (1,2,3);"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(3)
expect(result.map((row: any) => row.id).sort()).toEqual([1, 2, 3])
})
withTestDatabase('should handle "is" operator with null value', async (db) => {
await db.executeQuery(`
DELETE FROM "public"."normal_table";
INSERT INTO "public"."normal_table" (id, name)
VALUES
(1, 'Not Null'),
(2, NULL);
`)
const query = new Query()
const sql = query.from('normal_table', 'public').select().filter('name', 'is', 'null').toSql()
expect(sql).toMatchInlineSnapshot(`"select * from public.normal_table where name is null;"`)
const result = await validateSql(db, sql)
expect(result.length).toBe(1)
expect(result[0].id).toBe(2)
expect(result[0].name).toBeNull()
})
withTestDatabase('should handle "is" operator with not null value', async (db) => {
await db.executeQuery(`
DELETE FROM "public"."normal_table";
INSERT INTO "public"."normal_table" (id, name)
VALUES
(1, 'Not Null'),
(2, NULL);
`)
const query = new Query()
const sql = query
.from('normal_table', 'public')
.select()
.filter('name', 'is', 'not null')
.toSql()
expect(sql).toMatchInlineSnapshot(
`"select * from public.normal_table where name is not null;"`
)
const result = await validateSql(db, sql)
expect(result.length).toBe(1)
expect(result[0].id).toBe(1)
expect(result[0].name).toBe('Not Null')
})
})
})