Files
supabase/packages/pg-meta/test/tables.test.ts
Charis d79a276824 studio: ColumnTypeRef cascade + FK type comparison fixes (2/7) (#45903)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Refactor + bug fixes (part of the SafeSql migration stack — PR 2 of 7,
stacks on top of #45897).

## What is the current behavior?

- `pgMeta.columns.create` and the table-editor SQL builder take column
type as a string with array suffix and schema baked in (e.g.
`'private.test_enum'`, `'int4[]'`).
- The studio table-editor SQL emits the legacy schema-embedded `format`
string for enums in non-public schemas, while the pg-meta columns SQL
already returns the new shape (bare `format` + separate
`format_schema`). The two queries disagree on how to represent the same
column, surfacing as a false-positive type mismatch in the FK selector
when both ends are an enum from a non-public schema.
- The FK selector compares column types by `format` alone — same-named
enums in different schemas appear equal, and arrays vs. scalars of the
same base type pass the family check.
- `displayColumnType` renders arrays as the raw `_typname` pg-meta emits
(e.g. `_int4` instead of `int4[]`).

## What is the new behavior?

**pg-meta**

- Introduce `ColumnTypeRef` (`{ schema?, name, isArray? }`) for column
type input, replacing the legacy string-with-array-suffix format.
`pgMeta.columns.create` and the table-editor SQL builder consume the new
shape.
- Add `format_schema` to the column zod schema; pg-meta SQL emits the
type's schema for the table editor's ColumnType dropdown.
- `pgMeta.columns.create` returns a `SafeSqlFragment`.
- Studio table-editor SQL now emits bare `format` + `format_schema`,
matching pg-meta's columns SQL.

**Studio**

- `SafePostgresColumn`/`SafePostgresTable` extend the new `PG*` types
(master dropped postgres-meta).
- Pipe `ColumnTypeRef` through `SidePanelEditor` → `ColumnEditor` →
`TableEditor`, along with the column-create mutation, table
retrieve/list queries, and the `TableList`/`ColumnList` surfaces.
- `displayColumnType` helper renders arrays as `type[]` (or
`schema.type[]`) and handles non-implicit schemas.
- FK selector now carries `sourceIsArray`/`targetIsArray` and compares
the full `(format, format_schema, isArray)` triple. Family checks for
numeric/text/uuid skip when either side is an array (FKs across array
boundaries are never compatible).
- Type-mismatch and type-notice alerts pass `isArray` to the display
helper.
- Bundle `Policies.utils` + `Policies.types` + `sql-policy-mutation`,
`PolicyEditorModal`, and `SchemaGraph` here because `SidePanelEditor`
consumes `acceptGeneratedPolicy`/`AcceptedGeneratedPolicy` — splitting
requires temporary overloads with no architectural payoff.

## Additional context

Part of the SafeSql migration stack. Stacks on top of #45897.

### Manual test checklist

Surfaces touched by this PR — please exercise each:

**Table editor**
- [x] Create a new table with a mix of column types (scalar, array,
enum, foreign key)
- [x] Add a column to an existing table; verify the type dropdown lists
scalars + arrays separately and shows schema-qualified names for
non-public enums
- [x] Edit an existing column's type (scalar ↔ array, switch between
enums in different schemas) and save
- [x] Verify enum types from a non-public schema (e.g.
`private.my_enum`) display as `private.my_enum` in the column list

**Foreign key selector**
- [x] Open the FK selector for a column and pick a target column with a
matching type — no mismatch warning
- [x] Pick a target column whose type differs only by schema (two
same-named enums in different schemas) — should show a type-mismatch
alert
- [x] Pick a target column where one side is an array and the other is a
scalar of the same base type — should show a type-mismatch alert (no
auto-cast across array boundary)
- [x] When FK target sets the column type, verify `format_schema` and
`isArray` are preserved on the source column
- [x] Type-mismatch and type-notice alert messages render array types as
`type[]` (not `_type`)

**Column list / table list**
- [x] Schema-qualified type names display correctly for columns whose
type lives in a non-public schema
- [x] Array columns display as `type[]` (or `schema.type[]`)

**Policies (bundled due to import dependency)**
- [x] Open the Policies page; create/edit/delete a row-level policy via
the modal
- [x] Generate a policy via the AI assistant and accept it through
`SidePanelEditor` — verify the accepted policy lands in the editor
correctly

**Schema visualizer**
- [x] Open the Schemas → Schema Visualizer page; verify it renders
without type errors and shows tables/relationships

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

* **Improvements**
* Support for column types in non-public schemas and richer column type
presentation (includes schema and array info).
* Stronger SQL safety around policies and constraints; draft policy SQL
is now promoted explicitly on save.
* Improved foreign-key type validation and compatibility checks using
enhanced type metadata.

* **Tests**
* Updated snapshots and tests to reflect new column metadata and SQL
fragment handling.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45903)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-14 15:12:08 -04:00

1494 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",
"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)
})
}