mirror of
https://github.com/supabase/supabase.git
synced 2026-05-18 11:01:18 +08:00
## 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 --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45903) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1494 lines
42 KiB
TypeScript
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)
|
|
})
|
|
}
|