Files
supabase/packages/pg-meta/test/tables.test.ts
Charis df63ce3658 fix(mssql foreign tables): disallow sort on columns filtered for equality (#40137)
There is an edge case interaction between the Postgres query parser and
MSSQL foreign tables, where the query parser may drop sort clauses that
are redundant with applied filters. This leads to invalid MSSQL syntax,
because the resulting query has a `limit` but no `sort`, and the user
sees a confusing error message.

This PR detects this edge case on MSSQL foreign tables. There are three
cases:
1. The user filters by a column, but there are still other columns
available for sorting. The default search for a sorting column will
leave out the filtered column.
2. The user filters by a column/several columns, and there are no more
columns that can be used for sorting. We stop the query and show an
admonition.
3. The user filters by a column, then tries to sort by the same column.
We stop the query and show an admonition.
2025-11-06 13:14:30 -05:00

1470 lines
42 KiB
TypeScript

import { afterAll, beforeAll, expect, test } from 'vitest'
import pgMeta from '../src/index'
import { cleanupRoot, createTestDatabase } from './db/utils'
beforeAll(async () => {
// Any global setup if needed
})
afterAll(async () => {
await cleanupRoot()
})
const cleanNondet = (x: any) => {
const { columns, primary_keys, relationships, ...rest2 } = x
return {
columns: columns.map(({ id, table_id, ...rest }: any) => rest),
primary_keys: primary_keys.map(({ table_id, ...rest }: any) => rest),
relationships: relationships.map(({ id, ...rest }: any) => rest),
...rest2,
}
}
type TestDb = Awaited<ReturnType<typeof createTestDatabase>>
const withTestDatabase = (name: string, fn: (db: TestDb) => Promise<void>) => {
test(name, async () => {
const db = await createTestDatabase()
try {
await fn(db)
} finally {
await db.cleanup()
}
})
}
/** Original tests ported from postgres-meta */
withTestDatabase('list tables', async ({ executeQuery }) => {
const { sql, zod } = await pgMeta.tables.list()
const res = zod.parse(await executeQuery(sql))
const { columns, primary_keys, relationships, ...rest } = res.find(
({ name }) => name === 'users'
)!
expect({
columns: columns!.map(({ id, table_id, ...rest }) => rest),
primary_keys: primary_keys.map(({ table_id, ...rest }) => rest),
relationships: relationships.map(({ id, ...rest }) => rest),
...rest,
}).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [
{
"check": null,
"comment": null,
"data_type": "bigint",
"default_value": null,
"enums": [],
"format": "int8",
"identity_generation": "BY DEFAULT",
"is_generated": false,
"is_identity": true,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "id",
"ordinal_position": 1,
"schema": "public",
"table": "users",
},
{
"check": null,
"comment": null,
"data_type": "text",
"default_value": null,
"enums": [],
"format": "text",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "name",
"ordinal_position": 2,
"schema": "public",
"table": "users",
},
{
"check": null,
"comment": null,
"data_type": "USER-DEFINED",
"default_value": "'ACTIVE'::user_status",
"enums": [
"ACTIVE",
"INACTIVE",
],
"format": "user_status",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "status",
"ordinal_position": 3,
"schema": "public",
"table": "users",
},
],
"comment": null,
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "users",
"primary_keys": [
{
"name": "id",
"schema": "public",
"table_name": "users",
},
],
"relationships": [
{
"constraint_name": "todos_user-id_fkey",
"source_column_name": "user-id",
"source_schema": "public",
"source_table_name": "todos",
"target_column_name": "id",
"target_table_name": "users",
"target_table_schema": "public",
},
{
"constraint_name": "user_details_user_id_fkey",
"source_column_name": "user_id",
"source_schema": "public",
"source_table_name": "user_details",
"target_column_name": "id",
"target_table_name": "users",
"target_table_schema": "public",
},
],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
})
withTestDatabase('list tables without columns', async ({ executeQuery }) => {
const { sql, zod } = await pgMeta.tables.list({ includeColumns: false })
const res = zod.parse(await executeQuery(sql))
//@ts-expect-error columns doesn't exist at type level if includeColumns is false
const { columns, primary_keys, relationships, ...rest } = res.find(
({ name }) => name === 'users'
)!
expect({
primary_keys: primary_keys.map(({ table_id, ...rest }) => rest),
relationships: relationships.map(({ id, ...rest }) => rest),
...rest,
}).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"comment": null,
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "users",
"primary_keys": [
{
"name": "id",
"schema": "public",
"table_name": "users",
},
],
"relationships": [
{
"constraint_name": "todos_user-id_fkey",
"source_column_name": "user-id",
"source_schema": "public",
"source_table_name": "todos",
"target_column_name": "id",
"target_table_name": "users",
"target_table_schema": "public",
},
{
"constraint_name": "user_details_user_id_fkey",
"source_column_name": "user_id",
"source_schema": "public",
"source_table_name": "user_details",
"target_column_name": "id",
"target_table_name": "users",
"target_table_schema": "public",
},
],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
})
withTestDatabase('list tables with included schemas', async ({ executeQuery }) => {
const { sql, zod } = await pgMeta.tables.list({
includedSchemas: ['public'],
})
const res = zod.parse(await executeQuery(sql))
expect(res.length).toBeGreaterThan(0)
res.forEach((table) => {
expect(table.schema).toBe('public')
})
})
withTestDatabase('list tables with excluded schemas', async ({ executeQuery }) => {
const { sql, zod } = await pgMeta.tables.list({
excludedSchemas: ['public'],
})
const res = zod.parse(await executeQuery(sql))
res.forEach((table) => {
expect(table.schema).not.toBe('public')
})
})
withTestDatabase(
'list tables with excluded schemas and include System Schemas',
async ({ executeQuery }) => {
const { sql, zod } = await pgMeta.tables.list({
excludedSchemas: ['public'],
includeSystemSchemas: true,
})
const res = zod.parse(await executeQuery(sql))
expect(res.length).toBeGreaterThan(0)
res.forEach((table) => {
expect(table.schema).not.toBe('public')
})
}
)
withTestDatabase('create, retrieve, update, and delete table', async ({ executeQuery }) => {
// Create table
const { sql: createSql } = await pgMeta.tables.create({
name: 'test',
comment: 'foo',
})
await executeQuery(createSql)
// Retrieve the created table
const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
name: 'test',
schema: 'public',
})
const retrieveRes = retrieveZod.parse((await executeQuery(retrieveSql))[0])
expect(retrieveRes).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [],
"comment": "foo",
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "test",
"primary_keys": [],
"relationships": [],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
// Update table
const { sql: updateSql } = await pgMeta.tables.update(retrieveRes!, {
name: 'test a',
rls_enabled: true,
rls_forced: true,
replica_identity: 'NOTHING',
comment: 'foo',
})
await executeQuery(updateSql)
// Retrieve the updated table
const { sql: retrieveUpdatedSql, zod: retrieveUpdatedZod } = await pgMeta.tables.retrieve({
name: 'test a',
schema: 'public',
})
const updateRes = retrieveUpdatedZod.parse((await executeQuery(retrieveUpdatedSql))[0])
expect(updateRes).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [],
"comment": "foo",
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "test a",
"primary_keys": [],
"relationships": [],
"replica_identity": "NOTHING",
"rls_enabled": true,
"rls_forced": true,
"schema": "public",
"size": Any<String>,
}
`
)
// Remove table
const { sql: removeSql } = await pgMeta.tables.remove(updateRes!)
await executeQuery(removeSql)
// Verify table is deleted
const { sql: verifyDeleteSql } = await pgMeta.tables.retrieve(updateRes!)
const verifyDeleteRes = await executeQuery(verifyDeleteSql)
expect(verifyDeleteRes).toHaveLength(0)
})
withTestDatabase('update with name unchanged', async ({ executeQuery }) => {
// Create table
const { sql: createSql } = await pgMeta.tables.create({ name: 't' })
await executeQuery(createSql)
// Get the created table
const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
name: 't',
schema: 'public',
})
const table = retrieveZod.parse((await executeQuery(retrieveSql))[0])
// Update table with same name
const { sql: updateSql } = await pgMeta.tables.update(table!, { name: 't' })
await executeQuery(updateSql)
// Verify update
const { sql: verifySQL, zod: verifyZod } = await pgMeta.tables.retrieve({
name: 't',
schema: 'public',
})
const res = verifyZod.parse((await executeQuery(verifySQL))[0])
expect(res).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [],
"comment": null,
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "t",
"primary_keys": [],
"relationships": [],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
})
withTestDatabase("allow ' in comments", async ({ executeQuery }) => {
// Create table with single quote in comment
const { sql: createSql } = await pgMeta.tables.create({
name: 't',
comment: "'",
})
await executeQuery(createSql)
// Verify creation
const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
name: 't',
schema: 'public',
})
const res = retrieveZod.parse((await executeQuery(retrieveSql))[0])
expect(res).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [],
"comment": "'",
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "t",
"primary_keys": [],
"relationships": [],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
})
withTestDatabase('primary keys', async ({ executeQuery }) => {
// Create table with columns
const { sql: createSql } = await pgMeta.tables.create({ name: 't' })
await executeQuery(createSql)
await executeQuery(`
ALTER TABLE t
ADD COLUMN c bigint,
ADD COLUMN cc text
`)
// Get the created table
const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
name: 't',
schema: 'public',
})
const table = retrieveZod.parse((await executeQuery(retrieveSql))[0])
// Update table with primary keys
const { sql: updateSql } = await pgMeta.tables.update(table!, {
primary_keys: [{ name: 'c' }, { name: 'cc' }],
})
await executeQuery(updateSql)
// Verify update
const { sql: verifySQL, zod: verifyZod } = await pgMeta.tables.retrieve({
name: 't',
schema: 'public',
})
const res = verifyZod.parse((await executeQuery(verifySQL))[0])
expect(cleanNondet(res)).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [
{
"check": null,
"comment": null,
"data_type": "bigint",
"default_value": null,
"enums": [],
"format": "int8",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "c",
"ordinal_position": 1,
"schema": "public",
"table": "t",
},
{
"check": null,
"comment": null,
"data_type": "text",
"default_value": null,
"enums": [],
"format": "text",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "cc",
"ordinal_position": 2,
"schema": "public",
"table": "t",
},
],
"comment": null,
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "t",
"primary_keys": [
{
"name": "c",
"schema": "public",
"table_name": "t",
},
{
"name": "cc",
"schema": "public",
"table_name": "t",
},
],
"relationships": [],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
})
// /** Additional tests */
withTestDatabase('retrieve table by id', async ({ executeQuery }) => {
const { sql, zod } = await pgMeta.tables.retrieve({ id: 16510 })
const res = zod.parse((await executeQuery(sql))[0])
expect(res).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [
{
"check": null,
"comment": null,
"data_type": "text",
"default_value": null,
"enums": [],
"format": "text",
"id": "16510.2",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "name",
"ordinal_position": 2,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "integer",
"default_value": null,
"enums": [],
"format": "int4",
"id": "16510.3",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "category",
"ordinal_position": 3,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "jsonb",
"default_value": null,
"enums": [],
"format": "jsonb",
"id": "16510.4",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "metadata",
"ordinal_position": 4,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "timestamp without time zone",
"default_value": null,
"enums": [],
"format": "timestamp",
"id": "16510.5",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "created_at",
"ordinal_position": 5,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "integer",
"default_value": "nextval('memes_id_seq'::regclass)",
"enums": [],
"format": "int4",
"id": "16510.1",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "id",
"ordinal_position": 1,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "USER-DEFINED",
"default_value": "'old'::meme_status",
"enums": [
"new",
"old",
"retired",
],
"format": "meme_status",
"id": "16510.6",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "status",
"ordinal_position": 6,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
],
"comment": null,
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "memes",
"primary_keys": [
{
"name": "id",
"schema": "public",
"table_id": 16510,
"table_name": "memes",
},
],
"relationships": [
{
"constraint_name": "memes_category_fkey",
"id": 16519,
"source_column_name": "category",
"source_schema": "public",
"source_table_name": "memes",
"target_column_name": "id",
"target_table_name": "category",
"target_table_schema": "public",
},
],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
})
withTestDatabase('retrieve table by name and schema', async ({ executeQuery }) => {
const { sql, zod } = await pgMeta.tables.retrieve({ name: 'memes', schema: 'public' })
const res = zod.parse((await executeQuery(sql))[0])
expect(res).toMatchInlineSnapshot(
{
bytes: expect.any(Number),
dead_rows_estimate: expect.any(Number),
id: expect.any(Number),
live_rows_estimate: expect.any(Number),
size: expect.any(String),
},
`
{
"bytes": Any<Number>,
"columns": [
{
"check": null,
"comment": null,
"data_type": "text",
"default_value": null,
"enums": [],
"format": "text",
"id": "16510.2",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "name",
"ordinal_position": 2,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "integer",
"default_value": null,
"enums": [],
"format": "int4",
"id": "16510.3",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "category",
"ordinal_position": 3,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "jsonb",
"default_value": null,
"enums": [],
"format": "jsonb",
"id": "16510.4",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "metadata",
"ordinal_position": 4,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "timestamp without time zone",
"default_value": null,
"enums": [],
"format": "timestamp",
"id": "16510.5",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "created_at",
"ordinal_position": 5,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "integer",
"default_value": "nextval('memes_id_seq'::regclass)",
"enums": [],
"format": "int4",
"id": "16510.1",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": false,
"is_unique": false,
"is_updatable": true,
"name": "id",
"ordinal_position": 1,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
{
"check": null,
"comment": null,
"data_type": "USER-DEFINED",
"default_value": "'old'::meme_status",
"enums": [
"new",
"old",
"retired",
],
"format": "meme_status",
"id": "16510.6",
"identity_generation": null,
"is_generated": false,
"is_identity": false,
"is_nullable": true,
"is_unique": false,
"is_updatable": true,
"name": "status",
"ordinal_position": 6,
"schema": "public",
"table": "memes",
"table_id": 16510,
},
],
"comment": null,
"dead_rows_estimate": Any<Number>,
"id": Any<Number>,
"live_rows_estimate": Any<Number>,
"name": "memes",
"primary_keys": [
{
"name": "id",
"schema": "public",
"table_id": 16510,
"table_name": "memes",
},
],
"relationships": [
{
"constraint_name": "memes_category_fkey",
"id": 16519,
"source_column_name": "category",
"source_schema": "public",
"source_table_name": "memes",
"target_column_name": "id",
"target_table_name": "category",
"target_table_schema": "public",
},
],
"replica_identity": "DEFAULT",
"rls_enabled": false,
"rls_forced": false,
"schema": "public",
"size": Any<String>,
}
`
)
})
withTestDatabase('retrieve error if missing identifiers', async ({}) => {
await expect(async () => {
//@ts-expect-error use with missing params
await pgMeta.tables.retrieve({ name: 'memes' })
}).rejects.toThrow('Must provide either id or name and schema')
})
withTestDatabase('remove table by id', async ({ executeQuery }) => {
// First create a test table
await executeQuery(`
CREATE TABLE test_remove_table (
id SERIAL PRIMARY KEY,
name TEXT
);
`)
// Get the table's id
const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
const tables = listZod.parse(await executeQuery(listSql))
const tableId = tables.find((t) => t.name === 'test_remove_table')!
// Remove the table
const { sql } = await pgMeta.tables.remove(tableId)
await executeQuery(sql)
// Verify the table is gone
const tablesAfter = listZod.parse(await executeQuery(listSql))
expect(tablesAfter.find((t) => t.name === 'test_remove_table')).toBeUndefined()
})
withTestDatabase('remove table by name and schema', async ({ executeQuery }) => {
// First create a test table
await executeQuery(`
CREATE TABLE test_remove_table_2 (
id SERIAL PRIMARY KEY,
name TEXT
);
`)
// Remove the table
const { sql } = await pgMeta.tables.remove({
name: 'test_remove_table_2',
schema: 'public',
})
await executeQuery(sql)
// Verify the table is gone
const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
const tables = listZod.parse(await executeQuery(listSql))
expect(tables.find((t) => t.name === 'test_remove_table_2')).toBeUndefined()
})
withTestDatabase('remove throws error for non-existent table', async ({ executeQuery }) => {
const { sql } = await pgMeta.tables.remove({
name: 'non_existent_table',
schema: 'public',
})
// With schema and name
await expect(executeQuery(sql)).rejects.toThrow(
`Failed to execute query: table "non_existent_table" does not exist`
)
})
withTestDatabase('remove throws error with missing identifiers', async ({}) => {
await expect(async () => {
//@ts-expect-error use with missing params
await pgMeta.tables.remove({ name: 'some_table' })
}).rejects.toThrow('SQL identifier cannot be null or undefined')
})
withTestDatabase('update table - rename', async ({ executeQuery }) => {
// Create test table
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_rename' })
await executeQuery(createSql)
// Update table name
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_rename', schema: 'public' },
{ name: 'test_renamed' }
)
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_renamed',
schema: 'public',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.name).toBe('test_renamed')
})
withTestDatabase('update table - change schema', async ({ executeQuery }) => {
// Create test schema and table
await executeQuery('CREATE SCHEMA test_schema')
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_schema_move' })
await executeQuery(createSql)
// Move table to new schema
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_schema_move', schema: 'public' },
{ schema: 'test_schema' }
)
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_schema_move',
schema: 'test_schema',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.schema).toBe('test_schema')
})
withTestDatabase('update table - row level security', async ({ executeQuery }) => {
// Create test table
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_rls' })
await executeQuery(createSql)
// Enable RLS
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_rls', schema: 'public' },
{ rls_enabled: true, rls_forced: true }
)
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_rls',
schema: 'public',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.rls_enabled).toBe(true)
expect(res!.rls_forced).toBe(true)
})
withTestDatabase('update table - replica identity', async ({ executeQuery }) => {
// Create test table
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_replica' })
await executeQuery(createSql)
// Change replica identity
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_replica', schema: 'public' },
{ replica_identity: 'NOTHING' }
)
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_replica',
schema: 'public',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.replica_identity).toBe('NOTHING')
})
withTestDatabase('update table - primary keys', async ({ executeQuery }) => {
// Create test table with a column
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_pk' })
await executeQuery(createSql)
await executeQuery('ALTER TABLE test_pk ADD COLUMN id INT')
// Add primary key
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_pk', schema: 'public' },
{ primary_keys: [{ name: 'id' }] }
)
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_pk',
schema: 'public',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.primary_keys).toHaveLength(1)
expect(res!.primary_keys[0].name).toBe('id')
})
withTestDatabase('update table - remove primary keys', async ({ executeQuery }) => {
// Create test table with primary key
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_pk_remove' })
await executeQuery(createSql)
await executeQuery('ALTER TABLE test_pk_remove ADD COLUMN id INT PRIMARY KEY')
const { sql: tableSql, zod: tableZod } = await pgMeta.tables.retrieve({
name: 'test_pk_remove',
schema: 'public',
})
const table = tableZod.parse((await executeQuery(tableSql))[0])
// Remove primary key
const { sql: updateSql } = await pgMeta.tables.update(table!, { primary_keys: [] })
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_pk_remove',
schema: 'public',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.primary_keys).toHaveLength(0)
})
withTestDatabase('update table - comment', async ({ executeQuery }) => {
// Create test table
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_comment' })
await executeQuery(createSql)
// Add comment
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_comment', schema: 'public' },
{ comment: 'Test comment' }
)
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_comment',
schema: 'public',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.comment).toBe('Test comment')
})
withTestDatabase('update table - multiple changes', async ({ executeQuery }) => {
// Create test table
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_multiple' })
await executeQuery(createSql)
await executeQuery('ALTER TABLE test_multiple ADD COLUMN id INT')
// Make multiple changes
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_multiple', schema: 'public' },
{
name: 'test_multiple_updated',
comment: 'Updated table',
rls_enabled: true,
primary_keys: [{ name: 'id' }],
replica_identity: 'FULL',
}
)
await executeQuery(updateSql)
// Verify all updates
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_multiple_updated',
schema: 'public',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res).toMatchObject({
name: 'test_multiple_updated',
comment: 'Updated table',
rls_enabled: true,
replica_identity: 'FULL',
primary_keys: [expect.objectContaining({ name: 'id' })],
})
})
withTestDatabase('update table - by id', async ({ executeQuery }) => {
// Create test table
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_by_id' })
await executeQuery(createSql)
// Get table id
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_by_id',
schema: 'public',
})
const table = zod.parse((await executeQuery(retrieveSql))[0])
// Update by id
const { sql: updateSql } = await pgMeta.tables.update(table!, { name: 'test_by_id_updated' })
await executeQuery(updateSql)
// Verify update
const { sql: verifySQL, zod: verifyZod } = await pgMeta.tables.retrieve({
name: 'test_by_id_updated',
schema: 'public',
})
const res = verifyZod.parse((await executeQuery(verifySQL))[0])
expect(res!.name).toBe('test_by_id_updated')
})
withTestDatabase('update table - error on non-existent table', async ({ executeQuery }) => {
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'non_existent', schema: 'public' },
{ name: 'new_name' }
)
await expect(executeQuery(updateSql)).rejects.toThrow()
})
withTestDatabase('update table - rename with schema change', async ({ executeQuery }) => {
// Create test schema and table
await executeQuery('CREATE SCHEMA test_schema')
const { sql: createSql } = await pgMeta.tables.create({ name: 'test_rename_schema' })
await executeQuery(createSql)
// Update both name and schema
const { sql: updateSql } = await pgMeta.tables.update(
{ id: 0, name: 'test_rename_schema', schema: 'public' },
{
name: 'test_renamed_schema',
schema: 'test_schema',
}
)
await executeQuery(updateSql)
// Verify update
const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
name: 'test_renamed_schema',
schema: 'test_schema',
})
const res = zod.parse((await executeQuery(retrieveSql))[0])
expect(res!.name).toBe('test_renamed_schema')
expect(res!.schema).toBe('test_schema')
})
// Table creation test cases
const tableCreationTests = [
{
name: 'create table with default schema',
input: {
name: 'test_table_1',
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with explicit public schema',
input: {
name: 'test_table_2',
schema: 'public',
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with custom schema',
input: {
name: 'test_table_3',
schema: 'custom_schema',
},
expectedSchema: 'custom_schema',
expectedComment: null,
beforeTest: async (
executeQuery: Awaited<ReturnType<typeof createTestDatabase>>['executeQuery']
) => {
await executeQuery('CREATE SCHEMA IF NOT EXISTS custom_schema')
},
},
{
name: 'create table with comment',
input: {
name: 'test_table_4',
comment: 'Test comment',
},
expectedSchema: 'public',
expectedComment: 'Test comment',
},
{
name: 'create table with empty string comment',
input: {
name: 'test_table_5',
comment: '',
},
expectedSchema: 'public',
expectedComment: null, // PostgreSQL treats empty string comments as NULL
},
{
name: 'create table with null comment',
input: {
name: 'test_table_6',
comment: null,
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with uppercase name',
input: {
name: 'UPPERCASE_TABLE',
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with mixed case name',
input: {
name: 'MixedCase_Table_Name',
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with quoted name',
input: {
name: 'table "with" quotes',
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with special characters',
input: {
name: 'table$with#special@chars',
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with name at maximum length',
input: {
name: 'a'.repeat(63), // PostgreSQL has a 63-byte limit for identifiers
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with schema containing special characters',
input: {
name: 'normal_table',
schema: 'Special.Schema$Name',
},
expectedSchema: 'Special.Schema$Name',
expectedComment: null,
beforeTest: async (executeQuery: TestDb['executeQuery']) => {
await executeQuery('CREATE SCHEMA IF NOT EXISTS "Special.Schema$Name"')
},
},
{
name: 'create table with very long name',
input: {
name: 'a'.repeat(63),
},
expectedSchema: 'public',
expectedComment: null,
},
{
name: 'create table with special characters in name',
input: {
name: 'table,name',
},
expectedSchema: 'public',
expectedComment: null,
},
]
// SQL injection test cases
const sqlInjectionTests = [
{
name: 'prevent SQL injection in table name',
input: {
name: "table_name'; DROP TABLE users; --",
},
},
{
name: 'prevent SQL injection in comment',
input: {
name: 'safe_table',
comment: "normal comment'; DROP TABLE users; --",
},
},
]
// Error test cases
const errorTests = [
{
name: 'fail on duplicate table name',
input: { name: 'duplicate_table' },
setup: async (executeQuery: TestDb['executeQuery']) => {
await executeQuery(pgMeta.tables.create({ name: 'duplicate_table' }).sql)
},
expectedError: /relation.*already exists/,
},
{
name: 'fail on invalid schema',
input: { name: 'test_table', schema: 'nonexistent_schema' },
expectedError: /schema.*does not exist/,
},
{
name: 'fail on empty table name',
input: {
name: '',
},
expectedError: /zero-length delimited identifier/,
},
{
name: 'fail on schema name exceeding maximum length',
input: {
name: 'table',
schema: 'a'.repeat(64),
},
expectedError: /schema.*does not exist/,
},
]
// Generate individual test cases for successful table creation
for (const testCase of tableCreationTests) {
withTestDatabase(testCase.name, async ({ executeQuery }) => {
if (testCase.beforeTest) {
await testCase.beforeTest(executeQuery)
}
const { sql } = pgMeta.tables.create(testCase.input)
await executeQuery(sql)
const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
const tables = listZod.parse(await executeQuery(listSql))
const createdTable = tables.find((t) => t.name === testCase.input.name)
expect(createdTable).toBeDefined()
expect(createdTable?.schema).toBe(testCase.expectedSchema)
expect(createdTable?.comment).toBe(testCase.expectedComment)
})
}
// Generate individual test cases for SQL injection prevention
for (const testCase of sqlInjectionTests) {
withTestDatabase(`create - ${testCase.name}`, async ({ executeQuery }) => {
const { sql } = pgMeta.tables.create(testCase.input)
await executeQuery(sql)
// Verify table was created with correct name
const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
const tables = listZod.parse(await executeQuery(listSql))
expect(tables.find((t) => t.name === testCase.input.name)).toBeDefined()
// Verify users table still exists (wasn't dropped)
expect(tables.find((t) => t.name === 'users')).toBeDefined()
})
}
// Generate individual test cases for error conditions
for (const testCase of errorTests) {
withTestDatabase(`create - ${testCase.name}`, async ({ executeQuery }) => {
if (testCase.setup) {
await testCase.setup(executeQuery)
}
const { sql } = pgMeta.tables.create(testCase.input)
await expect(executeQuery(sql)).rejects.toThrow(testCase.expectedError)
})
}