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:
@@ -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)"
|
||||
|
||||
@@ -153,6 +153,7 @@ export const updateRow = (params: {
|
||||
pkValues: unknown[];
|
||||
column: string;
|
||||
value: unknown;
|
||||
ctid?: string;
|
||||
}) => invoke<void>("update_row", params);
|
||||
|
||||
export const insertRow = (params: {
|
||||
@@ -169,6 +170,7 @@ export const deleteRows = (params: {
|
||||
table: string;
|
||||
pkColumns: string[];
|
||||
pkValuesList: unknown[][];
|
||||
ctids?: string[];
|
||||
}) => invoke<number>("delete_rows", params);
|
||||
|
||||
// History
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface PaginatedQueryResult extends QueryResult {
|
||||
total_rows: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
ctids: string[];
|
||||
}
|
||||
|
||||
export interface SchemaObject {
|
||||
|
||||
Reference in New Issue
Block a user