| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600 |
- 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')
- })
- })
- })
|