diff --git a/apps/studio/components/grid/components/grid/ColumnHeader.tsx b/apps/studio/components/grid/components/grid/ColumnHeader.tsx index aa4492882b5..ffaf0a179a5 100644 --- a/apps/studio/components/grid/components/grid/ColumnHeader.tsx +++ b/apps/studio/components/grid/components/grid/ColumnHeader.tsx @@ -1,12 +1,12 @@ +import pgMeta from '@supabase/pg-meta' +import { getForeignKeyCascadeAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils' import type { XYCoord } from 'dnd-core' import { ArrowRight, Key, Lightbulb, Link, Lock } from 'lucide-react' import { useEffect, useRef } from 'react' import { useDrag, useDrop } from 'react-dnd' - -import { getForeignKeyCascadeAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' + import { useColumnHasIndexSuggestion, useTableIndexAdvisor, @@ -206,12 +206,14 @@ function renderColumnIcon( {foreignKey?.targetColumnName}
- {foreignKey?.updateAction !== FOREIGN_KEY_CASCADE_ACTION.NO_ACTION && ( + {foreignKey?.updateAction !== + pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION && (On update: {getForeignKeyCascadeAction(foreignKey?.updateAction)}
)} - {foreignKey?.deletionAction !== FOREIGN_KEY_CASCADE_ACTION.NO_ACTION && ( + {foreignKey?.deletionAction !== + pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION && (On delete: {getForeignKeyCascadeAction(foreignKey?.deletionAction)}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts index ed358d6cf2c..42809399b3f 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts @@ -1,12 +1,12 @@ +import pgMeta from '@supabase/pg-meta' import type { PostgresColumn } from '@supabase/postgres-meta' -import { isNull } from 'lodash' -import type { Dictionary } from 'types' - -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' import type { RetrievedTableColumn, RetrieveTableResult } from 'data/tables/table-retrieve-query' import { uuidv4 } from 'lib/helpers' +import { isNull } from 'lodash' import { toast } from 'sonner' +import type { Dictionary } from 'types' + import { ColumnField, CreateColumnPayload, @@ -229,8 +229,10 @@ export const getColumnForeignKey = ( const foreignKeyMeta = foreignKeys.find((fk) => fk.id === foreignKey.id) return { ...foreignKey, - deletion_action: foreignKeyMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, - update_action: foreignKeyMeta?.update_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, + deletion_action: + foreignKeyMeta?.deletion_action ?? pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, + update_action: + foreignKeyMeta?.update_action ?? pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, } } } @@ -243,13 +245,13 @@ const formatArrayToPostgresArray = (arrayString: string) => { export const getForeignKeyCascadeAction = (action?: string) => { switch (action) { - case FOREIGN_KEY_CASCADE_ACTION.CASCADE: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.CASCADE: return 'Cascade' - case FOREIGN_KEY_CASCADE_ACTION.RESTRICT: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.RESTRICT: return 'Restrict' - case FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT: return 'Set default' - case FOREIGN_KEY_CASCADE_ACTION.SET_NULL: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.SET_NULL: return 'Set NULL' default: return undefined diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.constants.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.constants.ts index 17f218bce48..14f9842e7f4 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.constants.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.constants.ts @@ -1,9 +1,29 @@ -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import pgMeta from '@supabase/pg-meta' export const FOREIGN_KEY_CASCADE_OPTIONS = [ - { key: 'no-action', label: 'No action', value: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION }, - { key: 'cascade', label: 'Cascade', value: FOREIGN_KEY_CASCADE_ACTION.CASCADE }, - { key: 'restrict', label: 'Restrict', value: FOREIGN_KEY_CASCADE_ACTION.RESTRICT }, - { key: 'set-default', label: 'Set default', value: FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT }, - { key: 'set-null', label: 'Set NULL', value: FOREIGN_KEY_CASCADE_ACTION.SET_NULL }, + { + key: 'no-action', + label: 'No action', + value: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, + }, + { + key: 'cascade', + label: 'Cascade', + value: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.CASCADE, + }, + { + key: 'restrict', + label: 'Restrict', + value: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.RESTRICT, + }, + { + key: 'set-default', + label: 'Set default', + value: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT, + }, + { + key: 'set-null', + label: 'Set NULL', + value: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.SET_NULL, + }, ] diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx index 11c008d3839..547b1d07108 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx @@ -1,4 +1,5 @@ import type { PostgresTable } from '@supabase/postgres-meta' +import pgMeta from '@supabase/pg-meta' import { sortBy } from 'lodash' import { ArrowRight, Database, HelpCircle, Loader2, Table, X } from 'lucide-react' import { Fragment, useEffect, useState } from 'react' @@ -13,7 +14,6 @@ import { import { DocsButton } from 'components/ui/DocsButton' import InformationBox from 'components/ui/InformationBox' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import { useSchemasQuery } from 'data/database/schemas-query' import { useTableQuery } from 'data/tables/table-retrieve-query' import { useTablesQuery } from 'data/tables/tables-query' @@ -33,8 +33,8 @@ const EMPTY_STATE: ForeignKey = { schema: 'public', table: '', columns: [] as { source: string; target: string }[], - deletionAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, - updateAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, + deletionAction: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, + updateAction: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, } interface ForeignKeySelectorProps { diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx index fcb10df4f84..4917504e692 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx @@ -1,4 +1,4 @@ -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import pgMeta from '@supabase/pg-meta' import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' import { HelpCircle } from 'lucide-react' import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' @@ -29,7 +29,7 @@ export const generateCascadeActionDescription = ( const actionName = getForeignKeyCascadeAction(cascadeAction) ?? 'No action' switch (cascadeAction) { - case FOREIGN_KEY_CASCADE_ACTION.NO_ACTION: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION: return ( <> {actionName}: {actionVerb} a record from{' '} @@ -38,7 +38,7 @@ export const generateCascadeActionDescription = ( existing in this table that reference it > ) - case FOREIGN_KEY_CASCADE_ACTION.CASCADE: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.CASCADE: return ( <> {actionName}: {actionVerb} a record from{' '} @@ -47,7 +47,7 @@ export const generateCascadeActionDescription = ( reference it in this table > ) - case FOREIGN_KEY_CASCADE_ACTION.RESTRICT: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.RESTRICT: return ( <> {actionName} @@ -65,7 +65,7 @@ export const generateCascadeActionDescription = ( existing referencing rows from this table. > ) - case FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT: return ( <> {actionName}: {actionVerb} a record from{' '} @@ -74,7 +74,7 @@ export const generateCascadeActionDescription = ( default value > ) - case FOREIGN_KEY_CASCADE_ACTION.SET_NULL: + case pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.SET_NULL: return ( <> {actionName}: {actionVerb} a record from{' '} diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts index 1c09396321b..195ad07fac4 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import pgMeta from '@supabase/pg-meta' import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types' import type { ColumnField } from './SidePanelEditor.types' @@ -288,8 +288,8 @@ describe('createTable', () => { schema: 'public', table: 'users', columns: [{ source: 'user_id', target: 'id' }], - deletionAction: FOREIGN_KEY_CASCADE_ACTION.CASCADE, - updateAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, + deletionAction: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.CASCADE, + updateAction: pgMeta.tableEditor.FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, }, ] @@ -306,7 +306,7 @@ describe('createTable', () => { const sqlCall = mockExecuteSql.mock.calls[0][0] expect(sqlCall.sql).toContain('ADD FOREIGN KEY') expect(sqlCall.sql).toContain('REFERENCES') - expect(sqlCall.sql).toContain('"users"') + expect(sqlCall.sql).toContain('users') expect(sqlCall.sql).toContain('ON DELETE CASCADE') }) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index 8fd48e910d2..2a88e59e327 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/nextjs' -import pgMeta from '@supabase/pg-meta' +import pgMeta, { type ForeignKey } from '@supabase/pg-meta' import { Query } from '@supabase/pg-meta/src/query' import type { PostgresPrimaryKey } from '@supabase/postgres-meta' import type { SupaRow } from 'components/grid/types' @@ -10,7 +10,6 @@ import { deleteDatabaseColumn } from 'data/database-columns/database-column-dele import { updateDatabaseColumn } from 'data/database-columns/database-column-update-mutation' import { createDatabasePolicy } from 'data/database-policies/database-policy-create-mutation' import type { Constraint } from 'data/database/constraints-query' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' import { databaseKeys } from 'data/database/keys' import { entityTypeKeys } from 'data/entity-types/keys' @@ -24,10 +23,10 @@ import { tableRowKeys } from 'data/table-rows/keys' import { executeWithRetry } from 'data/table-rows/table-rows-query' import { tableKeys } from 'data/tables/keys' import { - RetrieveTableResult, - RetrievedTableColumn, getTable, getTableQuery, + RetrievedTableColumn, + RetrieveTableResult, } from 'data/tables/table-retrieve-query' import { UpdateTableBody, @@ -45,7 +44,6 @@ import { generateCreateColumnPayload, generateUpdateColumnPayload, } from './ColumnEditor/ColumnEditor.utils' -import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types' import type { ColumnField, CreateColumnPayload, UpdateColumnPayload } from './SidePanelEditor.types' import { checkIfRelationChanged } from './TableEditor/ForeignKeysManagement/ForeignKeysManagement.utils' import type { ImportContent } from './TableEditor/TableEditor.types' @@ -81,23 +79,6 @@ export function getRowFromSidePanel( } } -/** - * The functions below are basically just queries but may be supported directly - * from the pg-meta library in the future - */ -const getAddPrimaryKeySQL = ({ - schema, - table, - columns, -}: { - schema: string - table: string - columns: string[] -}) => { - const primaryKeyColumns = columns.map((col) => `"${col}"`).join(', ') - return `ALTER TABLE "${schema}"."${table}" ADD PRIMARY KEY (${primaryKeyColumns})` -} - const addPrimaryKey = async ( projectRef: string, connectionString: string | undefined | null, @@ -105,7 +86,7 @@ const addPrimaryKey = async ( table: string, columns: string[] ) => { - const query = getAddPrimaryKeySQL({ schema, table, columns }) + const query = pgMeta.tableEditor.getAddPrimaryKeySQL({ schema, table, columns }) return await executeSql({ projectRef: projectRef, connectionString: connectionString, @@ -121,7 +102,7 @@ const dropConstraint = async ( table: string, name: string ) => { - const query = `ALTER TABLE "${schema}"."${table}" DROP CONSTRAINT "${name}"` + const query = pgMeta.tableEditor.getDropConstraintSQL({ schema, table, name }) return await executeSql({ projectRef: projectRef, connectionString: connectionString, @@ -130,49 +111,6 @@ const dropConstraint = async ( }) } -const getAddForeignKeySQL = ({ - table, - foreignKeys, -}: { - table: { schema: string; name: string } - foreignKeys: ForeignKey[] -}) => { - const getOnDeleteSql = (action: string) => - action === FOREIGN_KEY_CASCADE_ACTION.CASCADE - ? 'ON DELETE CASCADE' - : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT - ? 'ON DELETE RESTRICT' - : action === FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT - ? 'ON DELETE SET DEFAULT' - : action === FOREIGN_KEY_CASCADE_ACTION.SET_NULL - ? 'ON DELETE SET NULL' - : '' - const getOnUpdateSql = (action: string) => - action === FOREIGN_KEY_CASCADE_ACTION.CASCADE - ? 'ON UPDATE CASCADE' - : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT - ? 'ON UPDATE RESTRICT' - : '' - return ( - foreignKeys - .map((relation) => { - const { deletionAction, updateAction } = relation - const onDeleteSql = getOnDeleteSql(deletionAction) - const onUpdateSql = getOnUpdateSql(updateAction) - return ` - ALTER TABLE "${table.schema}"."${table.name}" - ADD FOREIGN KEY (${relation.columns.map((column) => `"${column.source}"`).join(',')}) - REFERENCES "${relation.schema}"."${relation.table}" (${relation.columns.map((column) => `"${column.target}"`).join(',')}) - ${onUpdateSql} - ${onDeleteSql} - ` - .replace(/\s+/g, ' ') - .trim() - }) - .join(';') + ';' - ) -} - const addForeignKey = async ({ projectRef, connectionString, @@ -184,7 +122,7 @@ const addForeignKey = async ({ table: { schema: string; name: string } foreignKeys: ForeignKey[] }) => { - const query = getAddForeignKeySQL({ table, foreignKeys }) + const query = pgMeta.tableEditor.getAddForeignKeySQL({ table, foreignKeys }) return await executeSql({ projectRef: projectRef, connectionString: connectionString, @@ -193,27 +131,6 @@ const addForeignKey = async ({ }) } -const getRemoveForeignKeySQL = ({ - table, - foreignKeys, -}: { - table: { schema: string; name: string } - foreignKeys: ForeignKey[] -}) => { - return ( - foreignKeys - .map((relation) => - ` -ALTER TABLE IF EXISTS "${table.schema}"."${table.name}" -DROP CONSTRAINT IF EXISTS "${relation.name}" -` - .replace(/\s+/g, ' ') - .trim() - ) - .join(';') + ';' - ) -} - const removeForeignKey = async ({ projectRef, connectionString, @@ -225,7 +142,7 @@ const removeForeignKey = async ({ table: { schema: string; name: string } foreignKeys: ForeignKey[] }) => { - const query = getRemoveForeignKeySQL({ table, foreignKeys }) + const query = pgMeta.tableEditor.getRemoveForeignKeySQL({ table, foreignKeys }) return await executeSql({ projectRef: projectRef, connectionString: connectionString, @@ -246,8 +163,8 @@ const updateForeignKey = async ({ foreignKeys: ForeignKey[] }) => { const query = ` - ${getRemoveForeignKeySQL({ table, foreignKeys })} - ${getAddForeignKeySQL({ table, foreignKeys })} + ${pgMeta.tableEditor.getRemoveForeignKeySQL({ table, foreignKeys })} + ${pgMeta.tableEditor.getAddForeignKeySQL({ table, foreignKeys })} ` .replace(/\s+/g, ' ') .trim() @@ -259,22 +176,6 @@ const updateForeignKey = async ({ }) } -const getUpdateIdentitySequenceSQL = ({ - schema, - table, - column, -}: { - schema: string - table: string - column: string -}) => { - return `SELECT setval('"${schema}"."${table}_${column}_seq"', (SELECT COALESCE(MAX("${column}"), 1) FROM "${schema}"."${table}"))` -} - -const getEnableRLSSQL = ({ schema, table }: { schema: string; table: string }) => { - return `ALTER TABLE "${schema}"."${table}" ENABLE ROW LEVEL SECURITY` -} - /** * The methods below involve several contexts due to the UI flow of the * dashboard and hence do not sit within their own stores @@ -458,12 +359,12 @@ export const duplicateTable = async ( await executeSql({ projectRef, connectionString, - sql: [ - `CREATE TABLE "${sourceTableSchema}"."${duplicatedTableName}" (LIKE "${sourceTableSchema}"."${sourceTableName}" INCLUDING ALL);`, - payload.comment != undefined - ? `comment on table "${sourceTableSchema}"."${duplicatedTableName}" is '${payload.comment}';` - : '', - ].join('\n'), + sql: pgMeta.tableEditor.getDuplicateTableSQL({ + sourceTableName, + sourceTableSchema, + duplicatedTableName, + comment: payload.comment, + }), }) await queryClient.invalidateQueries({ queryKey: tableKeys.list(projectRef, sourceTableSchema) }) @@ -482,7 +383,11 @@ export const duplicateTable = async ( await executeSql({ projectRef, connectionString, - sql: `INSERT INTO "${sourceTableSchema}"."${duplicatedTableName}" SELECT * FROM "${sourceTableSchema}"."${sourceTableName}";`, + sql: pgMeta.tableEditor.getDuplicateRowsSQL({ + sourceTableName, + sourceTableSchema, + duplicatedTableName, + }), }) // Insert into does not copy over auto increment sequences, so we manually do it next if any @@ -492,7 +397,12 @@ export const duplicateTable = async ( await executeSql({ projectRef, connectionString, - sql: `SELECT setval('"${sourceTableSchema}"."${duplicatedTableName}_${column.name}_seq"', (SELECT MAX("${column.name}") FROM "${sourceTableSchema}"."${sourceTableName}"));`, + sql: pgMeta.tableEditor.getDuplicateIdentitySequenceSQL({ + sourceTableName, + sourceTableSchema, + duplicatedTableName, + columnName: column.name, + }), }) }) } @@ -560,7 +470,10 @@ export const createTable = async ({ // 2. Enable RLS if configured if (isRLSEnabled) { - const enableRLSSQL = getEnableRLSSQL({ schema: payload.schema, table: payload.name }) + const enableRLSSQL = pgMeta.tableEditor.getEnableRLSSQL({ + schema: payload.schema, + table: payload.name, + }) sqlStatements.push(enableRLSSQL) } @@ -592,7 +505,7 @@ export const createTable = async ({ .filter((column) => column.isPrimaryKey) .map((column) => column.name) if (primaryKeyColumns.length > 0) { - const primaryKeySQL = getAddPrimaryKeySQL({ + const primaryKeySQL = pgMeta.tableEditor.getAddPrimaryKeySQL({ schema: payload.schema, table: payload.name, columns: primaryKeyColumns, @@ -602,7 +515,7 @@ export const createTable = async ({ // 5. Add foreign key constraints if (foreignKeyRelations.length > 0) { - const fkSql = getAddForeignKeySQL({ + const fkSql = pgMeta.tableEditor.getAddForeignKeySQL({ table: { schema: payload.schema, name: payload.name }, foreignKeys: foreignKeyRelations, }) @@ -795,7 +708,7 @@ export const createTable = async ({ if (identityColumns.length > 0) { const updateSequenceSQL = identityColumns .map((column) => - getUpdateIdentitySequenceSQL({ + pgMeta.tableEditor.getUpdateIdentitySequenceSQL({ schema: table.schema, table: table.name, column: column.name, diff --git a/packages/pg-meta/src/index.ts b/packages/pg-meta/src/index.ts index 8a5e8a29d7b..dd90a2b168e 100644 --- a/packages/pg-meta/src/index.ts +++ b/packages/pg-meta/src/index.ts @@ -25,6 +25,8 @@ export { getIndexWorkerStatusSQL } from './sql/studio/auth/get-index-worker-stat export { type OptimizedSearchColumns } from './sql/studio/auth/get-users-types' export { getPaginatedUsersSQL, type UsersCursor } from './sql/studio/auth/get-users-paginated' export { getUsersCountSQL } from './sql/studio/auth/get-users-count' +import * as tableEditor from './sql/studio/table-editor' +export type { ForeignKey } from './sql/studio/table-editor' export { getLargestSizeLimitBucketsSqlUnoptimized, LARGEST_SIZE_LIMIT_BUCKETS_COUNT, @@ -71,4 +73,5 @@ export default { indexes, columnPrivileges, query, + tableEditor, } diff --git a/packages/pg-meta/src/sql/studio/table-editor/constraints.ts b/packages/pg-meta/src/sql/studio/table-editor/constraints.ts new file mode 100644 index 00000000000..82354ee2474 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/constraints.ts @@ -0,0 +1,11 @@ +import { ident } from '../../../pg-format' + +export const getDropConstraintSQL = ({ + schema, + table, + name, +}: { + schema: string + table: string + name: string +}) => `ALTER TABLE ${ident(schema)}.${ident(table)} DROP CONSTRAINT ${ident(name)}` diff --git a/packages/pg-meta/src/sql/studio/table-editor/foreign-keys.ts b/packages/pg-meta/src/sql/studio/table-editor/foreign-keys.ts new file mode 100644 index 00000000000..617d0cde241 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/foreign-keys.ts @@ -0,0 +1,86 @@ +import { ident } from '../../../pg-format' + +export const getAddForeignKeySQL = ({ + table, + foreignKeys, +}: { + table: { schema: string; name: string } + foreignKeys: ForeignKey[] +}) => { + const getOnDeleteSql = (action: string) => + action === FOREIGN_KEY_CASCADE_ACTION.CASCADE + ? 'ON DELETE CASCADE' + : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT + ? 'ON DELETE RESTRICT' + : action === FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT + ? 'ON DELETE SET DEFAULT' + : action === FOREIGN_KEY_CASCADE_ACTION.SET_NULL + ? 'ON DELETE SET NULL' + : '' + const getOnUpdateSql = (action: string) => + action === FOREIGN_KEY_CASCADE_ACTION.CASCADE + ? 'ON UPDATE CASCADE' + : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT + ? 'ON UPDATE RESTRICT' + : '' + return ( + foreignKeys + .map((relation) => { + const { deletionAction, updateAction } = relation + const onDeleteSql = getOnDeleteSql(deletionAction) + const onUpdateSql = getOnUpdateSql(updateAction) + return ` + ALTER TABLE ${ident(table.schema)}.${ident(table.name)} + ADD FOREIGN KEY (${relation.columns.map((column) => ident(column.source)).join(', ')}) + REFERENCES ${ident(relation.schema)}.${ident(relation.table)} (${relation.columns.map((column) => ident(column.target)).join(', ')}) + ${onUpdateSql} + ${onDeleteSql} + ` + .replace(/\s+/g, ' ') + .trim() + }) + .join(';') + ';' + ) +} + +export const getRemoveForeignKeySQL = ({ + table, + foreignKeys, +}: { + table: { schema: string; name: string } + foreignKeys: ForeignKey[] +}) => { + return ( + foreignKeys + .map((relation) => + ` +ALTER TABLE IF EXISTS ${ident(table.schema)}.${ident(table.name)} +DROP CONSTRAINT IF EXISTS ${ident(relation.name)} +` + .replace(/\s+/g, ' ') + .trim() + ) + .join(';') + ';' + ) +} + +export interface ForeignKey { + id?: number | string + name?: string + tableId?: number + + schema: string + table: string + columns: { source: string; sourceType?: string; target: string; targetType?: string }[] + deletionAction: string + updateAction: string + toRemove?: boolean +} + +export enum FOREIGN_KEY_CASCADE_ACTION { + NO_ACTION = 'a', + RESTRICT = 'r', + CASCADE = 'c', + SET_NULL = 'n', + SET_DEFAULT = 'd', +} diff --git a/packages/pg-meta/src/sql/studio/table-editor/identity.ts b/packages/pg-meta/src/sql/studio/table-editor/identity.ts new file mode 100644 index 00000000000..3e0a833a3ea --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/identity.ts @@ -0,0 +1,27 @@ +import { ident } from '../../../pg-format' + +export const getUpdateIdentitySequenceSQL = ({ + schema, + table, + column, +}: { + schema: string + table: string + column: string +}) => { + return `SELECT setval('${ident(schema)}.${ident(`${table}_${column}_seq`)}', (SELECT COALESCE(MAX(${ident(column)}), 1) FROM ${ident(schema)}.${ident(table)}))` +} + +export const getDuplicateIdentitySequenceSQL = ({ + columnName, + duplicatedTableName, + sourceTableName, + sourceTableSchema, +}: { + columnName: string + duplicatedTableName: string + sourceTableName: string + sourceTableSchema: string +}) => { + return `SELECT setval('${ident(sourceTableSchema)}.${ident(`${duplicatedTableName}_${columnName}_seq`)}', (SELECT MAX(${ident(columnName)}) FROM ${ident(sourceTableSchema)}.${ident(sourceTableName)}));` +} diff --git a/packages/pg-meta/src/sql/studio/table-editor/index.ts b/packages/pg-meta/src/sql/studio/table-editor/index.ts new file mode 100644 index 00000000000..25e6f5d6c98 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/index.ts @@ -0,0 +1,6 @@ +export * from './constraints' +export * from './identity' +export * from './foreign-keys' +export * from './primary-keys' +export * from './rls' +export * from './table' diff --git a/packages/pg-meta/src/sql/studio/table-editor/primary-keys.ts b/packages/pg-meta/src/sql/studio/table-editor/primary-keys.ts new file mode 100644 index 00000000000..53fc0185d05 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/primary-keys.ts @@ -0,0 +1,18 @@ +import { ident } from '../../../pg-format' + +/** + * The functions below are basically just queries but may be supported directly + * from the pg-meta library in the future + */ +export const getAddPrimaryKeySQL = ({ + schema, + table, + columns, +}: { + schema: string + table: string + columns: string[] +}) => { + const primaryKeyColumns = columns.map((col) => ident(col)).join(', ') + return `ALTER TABLE ${ident(schema)}.${ident(table)} ADD PRIMARY KEY (${primaryKeyColumns})` +} diff --git a/packages/pg-meta/src/sql/studio/table-editor/rls.ts b/packages/pg-meta/src/sql/studio/table-editor/rls.ts new file mode 100644 index 00000000000..1d142971025 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/rls.ts @@ -0,0 +1,5 @@ +import { ident } from '../../../pg-format' + +export const getEnableRLSSQL = ({ schema, table }: { schema: string; table: string }) => { + return `ALTER TABLE ${ident(schema)}.${ident(table)} ENABLE ROW LEVEL SECURITY` +} diff --git a/packages/pg-meta/src/sql/studio/table-editor/table.ts b/packages/pg-meta/src/sql/studio/table-editor/table.ts new file mode 100644 index 00000000000..71e313551da --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/table.ts @@ -0,0 +1,32 @@ +import { ident } from '../../../pg-format' + +export const getDuplicateTableSQL = ({ + comment, + duplicatedTableName, + sourceTableName, + sourceTableSchema, +}: { + comment?: string | null + duplicatedTableName: string + sourceTableName: string + sourceTableSchema: string +}) => { + return [ + `CREATE TABLE ${ident(sourceTableSchema)}.${ident(duplicatedTableName)} (LIKE ${ident(sourceTableSchema)}.${ident(sourceTableName)} INCLUDING ALL);`, + comment != undefined + ? `comment on table ${ident(sourceTableSchema)}.${ident(duplicatedTableName)} is '${comment}';` + : '', + ].join('\n') +} + +export const getDuplicateRowsSQL = ({ + duplicatedTableName, + sourceTableName, + sourceTableSchema, +}: { + duplicatedTableName: string + sourceTableName: string + sourceTableSchema: string +}) => { + return `INSERT INTO ${ident(sourceTableSchema)}.${ident(duplicatedTableName)} SELECT * FROM ${ident(sourceTableSchema)}.${ident(sourceTableName)};` +}