From eb20a4674d2a55ac0648bade69f8f9722e331d4a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 6 Oct 2026 21:34:36 +0800 Subject: [PATCH] `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` --- .../Database/Hooks/EditHookPanel.tsx | 2 +- .../SidePanelEditor/SidePanelEditor.tsx | 2 +- .../interfaces/UnifiedLogs/UnifiedLogs.tsx | 2 +- .../TableEditorLayout/EntityListItem.tsx | 5 +- .../data/database/table-definition-query.ts | 2 +- .../prefetchers/project.$ref.editor.$id.tsx | 2 +- apps/studio/data/scoped-introspection.ts | 2 +- .../data/table-editor/table-editor-query.ts | 2 +- .../data/table-rows/table-rows-count-query.ts | 2 +- .../data/table-rows/table-rows-query.ts | 2 +- .../sql/studio/database/table-definition.ts | 60 ++++++++--------- .../test/sql/studio/table-definition.test.ts | 67 +++++++++++++++++++ 12 files changed, 110 insertions(+), 40 deletions(-) create mode 100644 packages/pg-meta/test/sql/studio/table-definition.test.ts diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index 8a2f9e9d688..64895bcc61f 100644 --- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -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, diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index 9e995ed9a4e..a1f101cba96 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -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(false) const csvImportKey = useVisibleKey(snap.sidePanel?.type === 'csv-import') diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx index e61aaefc6a9..79ffa5db357 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx @@ -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, diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx index 96a5db87d82..07fe3ca7637 100644 --- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx @@ -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') diff --git a/apps/studio/data/database/table-definition-query.ts b/apps/studio/data/database/table-definition-query.ts index 219fa1e2fb5..35ec50537a5 100644 --- a/apps/studio/data/database/table-definition-query.ts +++ b/apps/studio/data/database/table-definition-query.ts @@ -47,7 +47,7 @@ export const useTableDefinitionQuery = ( ...options }: UseCustomQueryOptions = {} ) => { - const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG) + const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG) return useQuery({ queryKey: [...databaseKeys.tableDefinition(projectRef, id), { scoped }], diff --git a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx index fdde7f38002..59fbeb5edc2 100644 --- a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx +++ b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx @@ -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[] }) => { diff --git a/apps/studio/data/scoped-introspection.ts b/apps/studio/data/scoped-introspection.ts index 00d801a089f..d1b69e518b1 100644 --- a/apps/studio/data/scoped-introspection.ts +++ b/apps/studio/data/scoped-introspection.ts @@ -58,7 +58,7 @@ export const scopedIntrospectionReady = (): Promise => { */ 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) diff --git a/apps/studio/data/table-editor/table-editor-query.ts b/apps/studio/data/table-editor/table-editor-query.ts index e964103c8f5..6cd8cf37aa5 100644 --- a/apps/studio/data/table-editor/table-editor-query.ts +++ b/apps/studio/data/table-editor/table-editor-query.ts @@ -51,7 +51,7 @@ export const useTableEditorQuery = ( ...options }: UseCustomQueryOptions = {} ) => { - const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG) + const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG) return useQuery({ ...tableEditorQueryOptions({ projectRef, connectionString, id, scoped }), diff --git a/apps/studio/data/table-rows/table-rows-count-query.ts b/apps/studio/data/table-rows/table-rows-count-query.ts index d002da4aef5..e1ba0ec16d1 100644 --- a/apps/studio/data/table-rows/table-rows-count-query.ts +++ b/apps/studio/data/table-rows/table-rows-count-query.ts @@ -116,7 +116,7 @@ export const useTableRowsCountQuery = ( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables' ) - const scoped = !!useFlag(PG_META_SCOPED_INTROSPECTION_FLAG) + const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG) return useQuery({ queryKey: tableRowKeys.tableRowsCount(projectRef, { diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index f2a874d6aaa..be7c8be4d06 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -403,7 +403,7 @@ export const useTableRowsQuery = ( ) => { 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. diff --git a/packages/pg-meta/src/sql/studio/database/table-definition.ts b/packages/pg-meta/src/sql/studio/database/table-definition.ts index a7f234deb1d..4fbda5ea0c2 100644 --- a/packages/pg-meta/src/sql/studio/database/table-definition.ts +++ b/packages/pg-meta/src/sql/studio/database/table-definition.ts @@ -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 diff --git a/packages/pg-meta/test/sql/studio/table-definition.test.ts b/packages/pg-meta/test/sql/studio/table-definition.test.ts new file mode 100644 index 00000000000..7a82944d5ec --- /dev/null +++ b/packages/pg-meta/test/sql/studio/table-definition.test.ts @@ -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() + } +})