-
+ columns.map((column) => {
+ const { icon: TypeIcon, label: typeLabel } =
+ getColumnTypeAffordancePresentation(column)
+
+ const constraintTokens = [
+ primaryKeyColumns.has(column.name) ? (
+
}
+ label="Primary"
+ variant="primary"
+ />
+ ) : null,
+ foreignKeyColumns.has(column.name) ? (
+
+ }
+ label="Foreign key"
+ />
+ ) : null,
+ column.is_unique || uniqueIndexColumns.has(column.name) ? (
+
+ }
+ label="Unique"
+ />
+ ) : null,
+ column.is_identity ? (
+
+ }
+ label="Identity"
+ />
+ ) : null,
+
+ }
+ label={column.is_nullable ? 'Nullable' : 'Non-nullable'}
+ variant="secondary"
+ />,
+ ].filter(Boolean)
+
+ return (
+
+
+
+
+ {TypeIcon}
+
+
+
+ {column.data_type}
+ {column.format !== column.data_type && (
+
+ {column.format}
+
+ )}
+
+
+
+
+
+
+
{column.name}
+ {column.comment !== null ? (
+
+ {column.comment}
+
+ ) : null}
- ) : (
-
-
-
- )}
-
-
- {!isSchemaLocked && isTableEntity && (
-
-
onEditColumn(column)}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canUpdateColumns
- ? 'Additional permissions required to edit column'
- : undefined,
- },
- }}
- >
- Edit
-
-
-
- } />
-
-
-
-
- onDeleteColumn(column)}
- className="space-x-2"
- >
-
- Delete column
-
-
- {!canUpdateColumns && (
-
- Additional permissions required to delete column
-
- )}
-
-
-
-
- )}
-
-
- ))}
+
+
+ {column.format}
+
+
+ {constraintTokens}
+
+
+ {!isSchemaLocked && isTableEntity && (
+
+
onEditColumn(column)}
+ tooltip={{
+ content: {
+ side: 'bottom',
+ text: !canUpdateColumns
+ ? 'Additional permissions required to edit column'
+ : undefined,
+ },
+ }}
+ >
+ Edit
+
+
+
+ } />
+
+
+ onDeleteColumn(column)}
+ className="gap-x-2"
+ tooltip={{
+ content: {
+ side: 'left',
+ text: deleteColumnTooltipText,
+ },
+ }}
+ >
+
+ Delete column
+
+
+
+
+ )}
+
+
+ )
+ })}
{isSuccess && (
diff --git a/apps/studio/components/interfaces/Database/Tables/ColumnList.utils.test.ts b/apps/studio/components/interfaces/Database/Tables/ColumnList.utils.test.ts
new file mode 100644
index 00000000000..5167108f7f3
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/ColumnList.utils.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ getColumnTypeAffordance,
+ getForeignKeyColumnNames,
+ getPrimaryKeyColumnNames,
+ getUniqueIndexColumnNames,
+} from './ColumnList.utils'
+
+describe('ColumnList.utils', () => {
+ it('normalises quoted array formats before resolving the affordance kind', () => {
+ expect(getColumnTypeAffordance('"uuid"[]')).toEqual({
+ kind: 'text',
+ label: 'Text',
+ })
+ })
+
+ it('maps recognised Postgres formats to their affordance labels', () => {
+ expect(getColumnTypeAffordance('timestamptz')).toEqual({
+ kind: 'time',
+ label: 'Date / time',
+ })
+ expect(getColumnTypeAffordance('jsonb')).toEqual({
+ kind: 'json',
+ label: 'JSON',
+ })
+ })
+
+ it('falls back to the other affordance for unrecognised formats', () => {
+ expect(getColumnTypeAffordance('citext')).toEqual({
+ kind: 'other',
+ label: 'Other',
+ })
+ })
+
+ it('derives only source-table foreign key column names', () => {
+ const table = {
+ schema: 'public',
+ name: 'orders',
+ primary_keys: [{ name: 'id' }],
+ relationships: [
+ {
+ source_schema: 'public',
+ source_table_name: 'orders',
+ source_column_name: 'customer_id',
+ target_table_schema: 'public',
+ target_table_name: 'customers',
+ target_column_name: 'id',
+ },
+ {
+ source_schema: 'public',
+ source_table_name: 'customers',
+ source_column_name: 'account_id',
+ target_table_schema: 'public',
+ target_table_name: 'accounts',
+ target_column_name: 'id',
+ },
+ ],
+ unique_indexes: [{ columns: ['reference'] }, { columns: ['customer_id', 'reference'] }],
+ } as const
+
+ expect([...getPrimaryKeyColumnNames(table)]).toEqual(['id'])
+ expect([...getForeignKeyColumnNames(table)]).toEqual(['customer_id'])
+ expect([...getUniqueIndexColumnNames(table)]).toEqual(['reference'])
+ })
+})
diff --git a/apps/studio/components/interfaces/Database/Tables/ColumnList.utils.ts b/apps/studio/components/interfaces/Database/Tables/ColumnList.utils.ts
new file mode 100644
index 00000000000..bfd03662aa5
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/ColumnList.utils.ts
@@ -0,0 +1,78 @@
+import { POSTGRES_DATA_TYPE_OPTIONS } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants'
+
+type TableConstraintSource = {
+ schema: string
+ name: string
+ primary_keys: ReadonlyArray<{ name: string }>
+ relationships: ReadonlyArray<{
+ source_schema: string
+ source_table_name: string
+ source_column_name: string
+ }>
+ unique_indexes?: ReadonlyArray<{ columns: ReadonlyArray }>
+}
+
+export type ColumnAffordanceKind = 'number' | 'time' | 'text' | 'json' | 'bool' | 'other'
+
+export interface ColumnTypeAffordance {
+ kind: ColumnAffordanceKind
+ label: string
+}
+
+const COLUMN_AFFORDANCE_LABELS: Record = {
+ number: 'Numeric',
+ time: 'Date / time',
+ text: 'Text',
+ json: 'JSON',
+ bool: 'Boolean',
+ other: 'Other',
+}
+
+const normalizeColumnFormat = (format: string) => format.replaceAll('"', '').replace(/\[\]$/, '')
+
+export function getColumnTypeAffordance(format: string): ColumnTypeAffordance {
+ const normalizedFormat = normalizeColumnFormat(format)
+ const optionType = POSTGRES_DATA_TYPE_OPTIONS.find(
+ (option) => option.name === normalizedFormat
+ )?.type
+
+ switch (optionType) {
+ case 'number':
+ case 'time':
+ case 'text':
+ case 'json':
+ case 'bool':
+ return { kind: optionType, label: COLUMN_AFFORDANCE_LABELS[optionType] }
+ default:
+ return { kind: 'other', label: COLUMN_AFFORDANCE_LABELS.other }
+ }
+}
+
+export function getPrimaryKeyColumnNames(table?: TableConstraintSource) {
+ return new Set(table?.primary_keys.map((primaryKey) => primaryKey.name) ?? [])
+}
+
+export function getForeignKeyColumnNames(table?: TableConstraintSource) {
+ if (!table) {
+ return new Set()
+ }
+
+ const { schema, name, relationships } = table
+
+ return new Set(
+ relationships
+ .filter(
+ (relationship) =>
+ relationship.source_schema === schema && relationship.source_table_name === name
+ )
+ .map((relationship) => relationship.source_column_name)
+ )
+}
+
+export function getUniqueIndexColumnNames(table?: TableConstraintSource) {
+ return new Set(
+ table?.unique_indexes
+ ?.filter((uniqueIndex) => uniqueIndex.columns.length === 1)
+ .flatMap((uniqueIndex) => uniqueIndex.columns) ?? []
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/ConstraintToken.tsx b/apps/studio/components/interfaces/Database/Tables/ConstraintToken.tsx
new file mode 100644
index 00000000000..6bcb0db01b1
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/ConstraintToken.tsx
@@ -0,0 +1,44 @@
+import { ReactNode } from 'react'
+import { cn } from 'ui'
+
+interface ConstraintTokenProps {
+ icon?: ReactNode
+ label: string
+ variant?: 'default' | 'secondary' | 'primary'
+}
+
+const constraintTokenClassName = cn(
+ 'inline-flex items-center justify-center rounded-md text-center font-mono uppercase whitespace-nowrap font-medium',
+ 'tracking-[0.06em] text-[11px] leading-[1.1] px-[5.5px] h-[21px]',
+ 'transition-all border'
+)
+
+export const ConstraintToken = ({ icon, label, variant = 'default' }: ConstraintTokenProps) => {
+ const tokenToneClassName =
+ variant === 'primary'
+ ? 'bg-brand bg-opacity-10 text-brand-600 border-brand-500'
+ : variant === 'default'
+ ? 'bg-surface-75 text-foreground-light border-strong'
+ : 'bg-surface-75 bg-opacity-50 text-foreground-light border-strong'
+
+ return (
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+ {label}
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
index a8bf7d7106e..8e9e0b69d60 100644
--- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx
+++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
@@ -306,22 +306,14 @@ export const TableList = ({
-
+
Name
-
- Columns
-
-
- Rows (Estimated)
-
-
- Size (Estimated)
-
-
- Realtime Enabled
-
+ Columns
+ Rows (Estimated)
+ Size (Estimated)
+ Realtime
@@ -375,10 +367,12 @@ export const TableList = ({
{entities.length > 0 &&
entities.map((x) => (
-
+
-
-
+
+
+
+
{formatTooltipText(x.type)}
@@ -412,35 +406,37 @@ export const TableList = ({
) : null}
-
+
{x.columns.length.toLocaleString()}
-
+
{x.rows !== undefined ? (
{x.rows.toLocaleString()}
) : (
–
)}
-
+
{x.size !== undefined ? (
{x.size}
) : (
–
)}
-
+
{(realtimePublication?.tables ?? []).find(
(table) => table.id === x.id
) ? (
-