feat: fallback to ctid for editing tables without primary key

When a table has no PRIMARY KEY, use PostgreSQL's ctid (physical row ID)
to identify rows for UPDATE/DELETE operations instead of blocking edits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 16:14:26 +03:00
parent e76a96deb8
commit baa794b66a
5 changed files with 133 additions and 52 deletions

View File

@@ -94,30 +94,46 @@ export function TableDataView({ connectionId, schema, table }: Props) {
[pendingChanges, isReadOnly]
);
const usesCtid = pkColumns.length === 0;
const handleCommit = async () => {
if (!data || pkColumns.length === 0) {
toast.error("Cannot save: no primary key detected");
if (!data) return;
if (pkColumns.length === 0 && (!data.ctids || data.ctids.length === 0)) {
toast.error("Cannot save: no primary key and no ctid available");
return;
}
setIsSaving(true);
try {
for (const [_key, change] of pendingChanges) {
const row = data.rows[change.rowIndex];
const pkValues = pkColumns.map((pkCol) => {
const idx = data.columns.indexOf(pkCol);
return row[idx];
});
const colName = data.columns[change.colIndex];
await updateRowApi({
connectionId,
schema,
table,
pkColumns,
pkValues: pkValues as unknown[],
column: colName,
value: change.value,
});
if (usesCtid) {
await updateRowApi({
connectionId,
schema,
table,
pkColumns: [],
pkValues: [],
column: colName,
value: change.value,
ctid: data.ctids[change.rowIndex],
});
} else {
const pkValues = pkColumns.map((pkCol) => {
const idx = data.columns.indexOf(pkCol);
return row[idx];
});
await updateRowApi({
connectionId,
schema,
table,
pkColumns,
pkValues: pkValues as unknown[],
column: colName,
value: change.value,
});
}
}
setPendingChanges(new Map());
queryClient.invalidateQueries({
@@ -182,6 +198,14 @@ export function TableDataView({ connectionId, schema, table }: Props) {
Read-Only
</span>
)}
{!isReadOnly && usesCtid && (
<span
className="rounded bg-orange-500/10 px-1.5 py-0.5 text-[10px] font-medium text-orange-600 dark:text-orange-400"
title="This table has no primary key. Edits use physical row ID (ctid), which may change after VACUUM or concurrent writes."
>
No PK using ctid
</span>
)}
<Filter className="h-3.5 w-3.5 text-muted-foreground" />
<Input
placeholder="WHERE clause (e.g. id > 10)"