Files
supabase/packages/pg-meta/test/tables.test.ts
VaibhavandAndrew Valleteau c427615234 fix(pg-meta): pair composite foreign key columns correctly (#41080)
## TL;DR

Correctly pairs composite foreign key columns when loading table
metadata.

## What's hurting?

The tables introspection query matched every source column in a
composite foreign key with every target column. For `(user_id,
tenant_id) → (id, tenant_id)`, it returned four relationships instead of
the correct two, causing incorrect relationship metadata in the Table
Editor...

## Now fixed

Source and target columns are paired by ordinal position using a lateral
multi-array `unnest`.
Regression coverage now verifies adversarial column ordering, and the
existing performance guard exercises thousands of composite foreign
keys...

PS: local stress test found no performance regression or unexpected
sequential scans.

## Ref

- Closes #41068

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

* **Bug Fixes**
* Corrected foreign-key relationship detection for composite keys,
ensuring source and target columns are paired accurately.
* Improved catalog relationship queries to remain within performance
limits for composite-key tables.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Andrew Valleteau <avallete@users.noreply.github.com>
2026-09-10 19:35:52 +02:00

1654 lines
49 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()
}
})
}
// Legacy relationships come out in plan-dependent order (frozen TABLES_SQL has
// no ORDER BY; proven by the adversarial-FK test below), so canonicalize ONLY
// the legacy side to the scoped ORDER BY: constraint_name, then the column names
// (a composite FK expands to one entry per ordinal column pair).
const sortRels = (rels: any[]) =>
[...rels].sort(
(a, b) =>
a.constraint_name.localeCompare(b.constraint_name) ||
a.source_column_name.localeCompare(b.source_column_name) ||
a.target_column_name.localeCompare(b.target_column_name)
)
withTestDatabase(
'scoped tables.retrieve matches legacy (FKs both directions, PK, comment, enums)',
async ({ executeQuery }) => {
await executeQuery(`
create type mood as enum ('sad', 'ok', 'happy');
create table public.parent (id int primary key, label text);
create table public.child (
id int primary key,
parent_id int references public.parent(id),
feeling mood,
self_ref int references public.child(id)
);
comment on table public.child is 'a child table';
`)
const [{ parent_id }] = await executeQuery<{ parent_id: number }[]>(
`select 'public.parent'::regclass::oid::int8 as parent_id;`
)
const [{ child_id }] = await executeQuery<{ child_id: number }[]>(
`select 'public.child'::regclass::oid::int8 as child_id;`
)
// parent: incoming FK (child.parent_id -> parent.id). child: outgoing FK to
// parent + a self-referential FK. Cover the id and name+schema branches.
const cases = [
{
label: 'parent by id',
legacy: { id: Number(parent_id) },
scoped: { id: Number(parent_id), scoped: true },
},
{
label: 'child by name+schema',
legacy: { name: 'child', schema: 'public' },
scoped: { name: 'child', schema: 'public', scoped: true },
},
{
label: 'child by id',
legacy: { id: Number(child_id) },
scoped: { id: Number(child_id), scoped: true },
},
] as const
for (const c of cases) {
const legacy = pgMeta.tables.retrieve(c.legacy as any)
const scoped = pgMeta.tables.retrieve(c.scoped as any)
const legacyRow: any = legacy.zod.parse((await executeQuery(legacy.sql))[0])
const scopedRow: any = scoped.zod.parse((await executeQuery(scoped.sql))[0])
// Everything raw except the legacy relationships array, canonicalized to
// the scoped ORDER BY (legacy order is plan-dependent, see above).
legacyRow.relationships = sortRels(legacyRow.relationships)
expect(scopedRow, c.label).toEqual(legacyRow)
// Scoped's array is already in that order raw (no scoped-side sort).
expect(scopedRow.relationships, c.label).toEqual(sortRels(scopedRow.relationships))
}
// Sanity: the scoped parent retrieve surfaces the INCOMING FK from child.
const scopedParent = pgMeta.tables.retrieve({ id: Number(parent_id), scoped: true })
const parentRow: any = scopedParent.zod.parse((await executeQuery(scopedParent.sql))[0])
expect(
parentRow.relationships.some(
(r: any) => r.source_table_name === 'child' && r.target_table_name === 'parent'
)
).toBe(true)
}
)
// Regression proof for the relationships exception: FK creation order (zzz, aaa,
// mmm) differs from constraint_name order, so legacy's plan order cannot match
// scoped's without the legacy-side sort, while scoped is deterministic by name.
withTestDatabase(
'scoped tables.retrieve orders relationships by constraint_name (adversarial)',
async ({ executeQuery }) => {
await executeQuery(`
create table public.ref (id int primary key);
create table public.multi (id int primary key, a int, b int, c int);
alter table public.multi add constraint zzz_fk foreign key (a) references public.ref (id);
alter table public.multi add constraint aaa_fk foreign key (b) references public.ref (id);
alter table public.multi add constraint mmm_fk foreign key (c) references public.ref (id);
`)
const legacy = pgMeta.tables.retrieve({ name: 'multi', schema: 'public' })
const scoped = pgMeta.tables.retrieve({ name: 'multi', schema: 'public', scoped: true })
const legacyRow: any = legacy.zod.parse((await executeQuery(legacy.sql))[0])
const scopedRow: any = scoped.zod.parse((await executeQuery(scoped.sql))[0])
// Scoped is deterministically constraint_name-ordered, raw.
expect(scopedRow.relationships.map((r: any) => r.constraint_name)).toEqual([
'aaa_fk',
'mmm_fk',
'zzz_fk',
])
// Equal only after canonicalizing the (plan-dependent) legacy side.
legacyRow.relationships = sortRels(legacyRow.relationships)
expect(scopedRow).toEqual(legacyRow)
}
)
// Composite (multi-column) FK: the relationships subquery expands it to one
// entry per ordinal column pair, all sharing constraint_name, so the
// scoped ORDER BY tie-breaks on the column names to stay deterministic.
withTestDatabase(
'scoped tables.retrieve orders composite-FK relationship entries deterministically',
async ({ executeQuery }) => {
await executeQuery(`
create table public.ctgt (x int, y int, primary key (x, y));
create table public.csrc (a int, b int, foreign key (b, a) references public.ctgt (x, y));
`)
const scoped = pgMeta.tables.retrieve({ name: 'csrc', schema: 'public', scoped: true })
const legacy = pgMeta.tables.retrieve({ name: 'csrc', schema: 'public' })
const scopedRow: any = scoped.zod.parse((await executeQuery(scoped.sql))[0])
const legacyRow: any = legacy.zod.parse((await executeQuery(legacy.sql))[0])
// Two ordinal pairs, both sharing one constraint_name.
expect(scopedRow.relationships).toHaveLength(2)
expect(new Set(scopedRow.relationships.map((r: any) => r.constraint_name)).size).toBe(1)
// Scoped is already ordered by (name, source col, target col), raw.
expect(
scopedRow.relationships.map((r: any) => [r.source_column_name, r.target_column_name])
).toEqual([
['a', 'y'],
['b', 'x'],
])
legacyRow.relationships = sortRels(legacyRow.relationships)
expect(scopedRow).toEqual(legacyRow)
}
)
withTestDatabase(
'scoped tables.retrieve preserves composite primary-key column order (indkey, not sorted)',
async ({ executeQuery }) => {
// PK declared (b, a) -- index column order is the reverse of alphabetical, so
// an accidental name-sort would be caught here.
await executeQuery(
`create table public.composite_pk (a int not null, b int not null, c int, primary key (b, a));`
)
const scoped = pgMeta.tables.retrieve({ name: 'composite_pk', schema: 'public', scoped: true })
const scopedRow: any = scoped.zod.parse((await executeQuery(scoped.sql))[0])
// Emitted in index (indkey) order (b, a), the semantically meaningful order.
expect(scopedRow.primary_keys.map((pk: any) => pk.name)).toEqual(['b', 'a'])
// Legacy emits the same PK order (the PK aggregate is ordered identically in
// both renderings), so primary_keys match without any normalization.
const legacy = pgMeta.tables.retrieve({ name: 'composite_pk', schema: 'public' })
const legacyRow: any = legacy.zod.parse((await executeQuery(legacy.sql))[0])
expect(scopedRow.primary_keys).toEqual(legacyRow.primary_keys)
}
)
/** 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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "public",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "public",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "pg_catalog",
"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",
"format_schema": "public",
"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 - replica identity INDEX requires index name', async () => {
expect(() =>
pgMeta.tables.update({ id: 1, name: 'test', schema: 'public' }, { replica_identity: 'INDEX' })
).toThrow('replica_identity_index is required')
})
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)
})
}