mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
0433eeb5f5
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 -->
601 lines
21 KiB
TypeScript
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')
|
|
})
|
|
})
|
|
})
|