getTableDefinitionSql to escape SQL identifiers (#51258)

## Context

Similar to domain to https://github.com/supabase/supabase/pull/51256 -
`getTableDefinitionSql` doesn't escape SQL identifiers, which generates
invalid SQL on the dashboard's table editor for the "Copy table schema"
CTA, or the table definition tab.

Also fixes the "Copy table schema" CTA which was missing the `scoped`
parameter when calling `getTableDefinition`

Changes here addresses this issue, can test with a table named like
`test"table`
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-10-06 21:34:36 +08:00
1 parent 1da25a7e72
commit eb20a4674d
12 files changed
+110 -40

No files matched your search

@@ -72,7 +72,7 @@ export const EditHookPanel = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const [isLoadingTable, setIsLoadingTable] = useState(false)
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const { data: hooks = [], isSuccess } = useDatabaseHooksQuery({
projectRef: project?.ref,
@@ -193,7 +193,7 @@ export const SidePanelEditor = ({
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const { updateRow, addRow, isEditPending } = useTableRowOperations()
const getImpersonatedRoleState = useGetImpersonatedRoleState()
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const [isEdited, setIsEdited] = useState<boolean>(false)
const csvImportKey = useVisibleKey(snap.sidePanel?.type === 'csv-import')
@@ -97,7 +97,7 @@ export const UnifiedLogs = () => {
const [search, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
const showMultigresLogs = useShowMultigresLogs()
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const computeEnabled = !!useFlag('compute')
const computeEnabled = useFlag('compute')
const computeAvailability = getComputeLogsAvailability({
isPlatform: IS_PLATFORM,
flagsLoaded,
@@ -1,4 +1,4 @@
import { useParams } from 'common'
import { useFlag, useParams } from 'common'
import { Copy, Download, Edit, Globe, Lock, MoreVertical, Trash } from 'lucide-react'
import Link from 'next/link'
import { type CSSProperties } from 'react'
@@ -38,6 +38,7 @@ import type {
TableApiAccessData,
TableApiAccessMap,
} from '@/data/privileges/table-api-access-query'
import { PG_META_SCOPED_INTROSPECTION_FLAG } from '@/data/table-editor/table-editor-query'
import { useTableRowsCountQuery } from '@/data/table-rows/table-rows-count-query'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -78,6 +79,7 @@ export const EntityListItem = ({
const { data: project } = useSelectedProjectQuery()
const snap = useTableEditorStateSnapshot()
const { selectedSchema } = useQuerySchemaState()
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const tabId = createTabId(entity.type, { id: entity.id })
const tabs = useTabsStateSnapshot()
@@ -276,6 +278,7 @@ export const EntityListItem = ({
id: entity.id,
projectRef: project?.ref,
connectionString: project?.connectionString,
scoped,
}).then((tableDefinition) => {
if (!tableDefinition) {
throw new Error('Failed to get table schema')
@@ -47,7 +47,7 @@ export const useTableDefinitionQuery = <TData = TableDefinitionData>(
...options
}: UseCustomQueryOptions<TableDefinitionData, TableDefinitionError, TData> = {}
) => {
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
return useQuery<TableDefinitionData, TableDefinitionError, TData>({
queryKey: [...databaseKeys.tableDefinition(projectRef, id), { scoped }],
@@ -76,7 +76,7 @@ export function usePrefetchEditorTablePage() {
const { data: project } = useSelectedProjectQuery()
const { connectionString, identifier: readReplicaIdentifier } = useConnectionStringForReadOps()
const roleImpersonationState = useRoleImpersonationStateSnapshot()
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
return useCallback(
({ id: _id, filters, sorts }: { id?: string; filters?: Filter[]; sorts?: Sort[] }) => {
+1 -1
View File
@@ -58,7 +58,7 @@ export const scopedIntrospectionReady = (): Promise<void> => {
*/
export const useSyncScopedIntrospection = () => {
const { hasLoaded } = useFeatureFlags()
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
useEffect(() => {
setScopedIntrospection(scoped)
@@ -51,7 +51,7 @@ export const useTableEditorQuery = <TData = TableEditorData>(
...options
}: UseCustomQueryOptions<TableEditorData, TableEditorError, TData> = {}
) => {
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
return useQuery<TableEditorData, TableEditorError, TData>({
...tableEditorQueryOptions({ projectRef, connectionString, id, scoped }),
@@ -116,7 +116,7 @@ export const useTableRowsCountQuery = <TData = TableRowsCountData>(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
)
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
return useQuery<TableRowsCountData, TableRowsCountError, TData>({
queryKey: tableRowKeys.tableRowsCount(projectRef, {
@@ -403,7 +403,7 @@ export const useTableRowsQuery = <TData = TableRowsData>(
) => {
const queryClient = useQueryClient()
const { connectionString, identifier: readReplicaIdentifier } = useConnectionStringForReadOps()
const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
// [Ali] Exclude preflightCheck from query key — it controls how the query
// executes (whether an EXPLAIN guard runs first), not what data is returned.
@@ -941,16 +941,16 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
bInheritance := False;
IF v_pgversion < 100000 THEN
-- Issue#11: handle parent schema
SELECT c2.relname parent, c2.relnamespace::regnamespace INTO v_parent, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
SELECT c2.relname parent, n2.nspname INTO v_parent, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2, pg_namespace n2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r' and n2.oid = c2.relnamespace;
IF (v_parent IS NOT NULL) THEN
bPartition := True;
bInheritance := True;
END IF;
ELSE
-- Issue#11: handle parent schema
SELECT c2.relname parent, c1.relispartition, pg_get_expr(c1.relpartbound, c1.oid, true), c2.relnamespace::regnamespace INTO v_parent, bRelispartition, v_partbound, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
SELECT c2.relname parent, c1.relispartition, pg_get_expr(c1.relpartbound, c1.oid, true), n2.nspname INTO v_parent, bRelispartition, v_partbound, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2, pg_namespace n2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r' and n2.oid = c2.relnamespace;
IF (v_parent IS NOT NULL) THEN
bPartition := True;
IF bRelispartition THEN
@@ -977,9 +977,9 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
IF bInheritance THEN
-- inheritance-based
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '"( '|| E'\\n';
v_table_ddl := 'CREATE TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || '( '|| E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || '( '|| E'\\n';
v_table_ddl := 'CREATE TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || '( '|| E'\\n';
END IF;
-- Jump to constraints section to add the check constraints
@@ -987,15 +987,15 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
-- declarative-based
IF v_relopts <> '' THEN
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '" PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
v_table_ddl := 'CREATE TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || ' PARTITION OF ' || quote_ident(in_schema) || '.' || quote_ident(v_parent) || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || ' PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
v_table_ddl := 'CREATE TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || ' PARTITION OF ' || quote_ident(in_schema) || '.' || quote_ident(v_parent) || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
END IF;
ELSE
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '" PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
v_table_ddl := 'CREATE TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || ' PARTITION OF ' || quote_ident(in_schema) || '.' || quote_ident(v_parent) || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || ' PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
v_table_ddl := 'CREATE TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || ' PARTITION OF ' || quote_ident(in_schema) || '.' || quote_ident(v_parent) || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
END IF;
END IF;
-- Jump to constraints and index section to add the check constraints and indexes and perhaps FKeys
@@ -1024,9 +1024,9 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
v_cnt1 := CASE WHEN in_table ~ '[A-Z]' THEN 1 ELSE 0 END;
IF v_cnt1 > 0 THEN
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '."' || in_table || '" (' || E'\\n';
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || ' (' || E'\\n';
ELSE
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '.' || in_table || ' (' || E'\\n';
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || quote_ident(in_schema) || '.' || quote_ident(in_table) || ' (' || E'\\n';
END IF;
END IF;
-- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl;
@@ -1066,9 +1066,9 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := v_table_ddl || ' "' || v_colrec.column_name || '" ';
v_table_ddl := v_table_ddl || ' ' || quote_ident(v_colrec.column_name) || ' ';
ELSE
v_table_ddl := v_table_ddl || ' ' || v_colrec.column_name || ' ';
v_table_ddl := v_table_ddl || ' ' || quote_ident(v_colrec.column_name) || ' ';
END IF;
-- Issue#23: Handle autogenerated columns and rewrite as a simpler IF THEN ELSE branch instead of a much more complex embedded CASE STATEMENT
@@ -1078,7 +1078,7 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
ELSEIF v_colrec.udt_name in ('geometry', 'box2d', 'box2df', 'box3d', 'geography', 'geometry_dump', 'gidx', 'spheroid', 'valid_detail') THEN
v_temp = v_colrec.udt_name;
ELSEIF v_colrec.data_type = 'USER-DEFINED' THEN
v_temp = v_colrec.udt_schema || '.' || v_colrec.udt_name;
v_temp = quote_ident(v_colrec.udt_schema) || '.' || quote_ident(v_colrec.udt_name);
ELSEIF v_colrec.data_type = 'ARRAY' THEN
-- Issue#6 fix: handle arrays
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
@@ -1152,12 +1152,12 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
v_constraint_def := v_constraintrec.constraint_definition;
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| quote_ident(v_constraint_name) || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
SELECT 'ALTER TABLE ONLY ' || quote_ident(in_schema) || '.' || quote_ident(c.relname) || ' ADD CONSTRAINT ' || quote_ident(r.conname) || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name;
END IF;
IF bPartition THEN
@@ -1173,12 +1173,12 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
-- internal def
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| quote_ident(v_constraint_name) || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
SELECT 'ALTER TABLE ONLY ' || quote_ident(n.nspname) || '.' || quote_ident(c2.relname) || ' ADD CONSTRAINT ' || quote_ident(r.conname) || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
END IF;
@@ -1186,7 +1186,7 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
-- handle all other constraints besides PKEY and FKEYS as internal defs by default
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| quote_ident(v_constraint_name) || ' '
|| v_constraint_def
|| ',' || E'\\n';
END IF;
@@ -1222,12 +1222,12 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
v_constraint_def := v_constraintrec.constraint_definition;
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| quote_ident(v_constraint_name) || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
SELECT 'ALTER TABLE ONLY ' || quote_ident(in_schema) || '.' || quote_ident(c.relname) || ' ADD CONSTRAINT ' || quote_ident(r.conname) || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table;
END IF;
IF bPartition THEN
@@ -1243,12 +1243,12 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
-- internal def
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| quote_ident(v_constraint_name) || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
SELECT 'ALTER TABLE ONLY ' || quote_ident(n.nspname) || '.' || quote_ident(c2.relname) || ' ADD CONSTRAINT ' || quote_ident(r.conname) || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and
r.conname = v_constraint_name and r.conparentid = 0;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
@@ -1257,7 +1257,7 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
-- handle all other constraints besides PKEY and FKEYS as internal defs by default
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| quote_ident(v_constraint_name) || ' '
|| v_constraint_def
|| ',' || E'\\n';
END IF;
@@ -1285,7 +1285,7 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
-- Issue#11: handle parent schema
-- v_table_ddl := v_table_ddl || ') INHERITS (' || in_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
IF v_parent_schema = '' OR v_parent_schema IS NULL THEN v_parent_schema = in_schema; END IF;
v_table_ddl := v_table_ddl || ') INHERITS (' || v_parent_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
v_table_ddl := v_table_ddl || ') INHERITS (' || quote_ident(v_parent_schema) || '.' || quote_ident(v_parent) || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
END IF;
IF v_pgversion >= 100000 AND NOT bPartition and NOT bInheritance THEN
@@ -1370,9 +1370,9 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
v_pos = POSITION(' WHERE ' IN v_temp);
v_temp2 = SUBSTRING(v_temp, v_pos);
v_temp = SUBSTRING(v_temp, 1, v_pos);
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n';
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || quote_ident(v_indexrec.tablespace) || v_temp2 || ';' || E'\\n';
ELSE
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || v_indexrec.tablespace || ';' || E'\\n';
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || quote_ident(v_indexrec.tablespace) || ';' || E'\\n';
END IF;
END IF;
@@ -1383,8 +1383,8 @@ const SCOPED_PG_GET_TABLEDEF_SQL: SafeSqlFragment = safeSql`
IF cmtcnt > 0 THEN
FOR v_rec IN
SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE '
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' ||
CASE WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN quote_ident(c.relname) || '.' || a.attname ELSE quote_ident(c.relname) END || ' IS ' || quote_literal(d.description) || ';' as ddl
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || quote_ident(n.nspname) || '.' ||
CASE WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN quote_ident(c.relname) || '.' || quote_ident(a.attname) ELSE quote_ident(c.relname) END || ' IS ' || quote_literal(d.description) || ';' as ddl
FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace) LEFT JOIN pg_description d ON (c.oid = d.objoid) LEFT JOIN pg_attribute a ON (c.oid = a.attrelid AND a.attnum > 0 and a.attnum = d.objsubid)
WHERE d.description IS NOT NULL AND n.nspname = in_schema AND c.relname = in_table ORDER BY 2 desc, ddl
LOOP
@@ -0,0 +1,67 @@
import { afterAll, expect, test } from 'vitest'
import { getTableDefinitionSql } from '../../../src'
import { cleanupRoot, createTestDatabase } from '../../db/utils'
afterAll(async () => {
await cleanupRoot()
})
test('getTableDefinitionSql (scoped): escapes double quotes in schema, table, column and constraint names', async () => {
const db = await createTestDatabase()
try {
await db.executeQuery(`
create schema "pub""lic2";
create table "pub""lic2"."te""st" (
id bigint generated by default as identity,
"we""ird" text not null,
constraint "pk""test" primary key (id)
);
`)
const [{ id }] = await db.executeQuery<{ id: number }[]>(
`select '"pub""lic2"."te""st"'::regclass::oid::int8 as id;`
)
const sql = getTableDefinitionSql({ id, scoped: true })
const [{ definition }] = await db.executeQuery<{ definition: string }[]>(sql)
expect(definition).toContain('"pub""lic2"')
expect(definition).toContain('"te""st"')
expect(definition).toContain('"we""ird"')
expect(definition).toContain('"pk""test"')
// the generated DDL must itself be valid, executable SQL
await db.executeQuery(`drop table "pub""lic2"."te""st";`)
await db.executeQuery(definition)
} finally {
await db.cleanup()
}
})
test('getTableDefinitionSql (scoped): quotes the parent schema for an inherited table', async () => {
const db = await createTestDatabase()
try {
await db.executeQuery(`
create schema "par""ent1";
create table "par""ent1"."par""ent_tbl" (id bigint primary key);
create schema "chi""ld1";
create table "chi""ld1"."chi""ld_tbl" () inherits ("par""ent1"."par""ent_tbl");
`)
const [{ id }] = await db.executeQuery<{ id: number }[]>(
`select '"chi""ld1"."chi""ld_tbl"'::regclass::oid::int8 as id;`
)
const sql = getTableDefinitionSql({ id, scoped: true })
const [{ definition }] = await db.executeQuery<{ definition: string }[]>(sql)
expect(definition).toContain('INHERITS ("par""ent1"."par""ent_tbl")')
// the generated DDL must itself be valid, executable SQL
await db.executeQuery(`drop table "chi""ld1"."chi""ld_tbl";`)
await db.executeQuery(definition)
} finally {
await db.cleanup()
}
})