From a485cf7ee3777886f60f000030c3e779fa259e94 Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Thu, 7 May 2026 00:27:24 +0300 Subject: [PATCH] fix(greenplum): drop ctid from table-data SELECT and friendly-fail PK-less edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browsing any GP table threw "column ctid does not exist" because the table-viewer's pagination query unconditionally appended `ctid::text` to the SELECT. AO and AOCO tables (the dominant storage in GP) genuinely have no ctid, and even on heap tables the value isn't reliable across segments — so editing by ctid would be wrong even when the column exists. - get_table_data: skip the ctid column entirely on Greenplum (`SELECT *` instead of `SELECT *, ctid::text`); ctids field stays as an empty Vec. - update_row / delete_rows: when the table has no PK on a GP connection, return an actionable error instead of falling through to the ctid path. Tells the user to add a PK or use raw SQL. Vanilla Postgres behavior is unchanged. --- src-tauri/src/commands/data.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/commands/data.rs b/src-tauri/src/commands/data.rs index e718530..0bb6c2b 100644 --- a/src-tauri/src/commands/data.rs +++ b/src-tauri/src/commands/data.rs @@ -136,9 +136,19 @@ pub async fn get_table_data( let offset = (page.saturating_sub(1)) * page_size; + // Greenplum AO / AOCO tables don't expose `ctid` and even on heap tables + // it isn't a reliable per-row identifier across segments — so on GP we + // skip the ctid column entirely. Edits without a PK will be rejected by + // update_row/delete_rows below with a friendly error. + let include_ctid = !matches!(flavor, DbFlavor::Greenplum); + let select_clause = if include_ctid { + "*, ctid::text" + } else { + "*" + }; let data_sql = format!( - "SELECT *, ctid::text FROM {}{}{} LIMIT {} OFFSET {}", - qualified, where_clause, order_clause, page_size, offset + "SELECT {} FROM {}{}{} LIMIT {} OFFSET {}", + select_clause, qualified, where_clause, order_clause, page_size, offset ); let count_sql = format!("SELECT COUNT(*) FROM {}{}", qualified, where_clause); @@ -248,6 +258,13 @@ pub async fn update_row( let set_clause = format!("{} = $1", escape_ident(&column)); if pk_columns.is_empty() { + if matches!(state.get_flavor(&connection_id).await, DbFlavor::Greenplum) { + return Err(TuskError::Custom( + "Greenplum doesn't expose a stable ctid (AO/AOCO tables have none, \ + heap-table ctid isn't unique across segments). Inline edit requires \ + a primary key — add one or use a SQL UPDATE.".into(), + )); + } // Fallback: use ctid for row identification let ctid_val = ctid.ok_or_else(|| { TuskError::Custom("Cannot update: no primary key and no ctid provided".into()) @@ -354,6 +371,13 @@ pub async fn delete_rows( let mut tx = pool.begin().await.map_err(TuskError::Database)?; if pk_columns.is_empty() { + if matches!(state.get_flavor(&connection_id).await, DbFlavor::Greenplum) { + return Err(TuskError::Custom( + "Greenplum doesn't expose a stable ctid (AO/AOCO tables have none, \ + heap-table ctid isn't unique across segments). Inline delete requires \ + a primary key — add one or use a SQL DELETE.".into(), + )); + } // Fallback: use ctids for row identification let ctid_list = ctids.ok_or_else(|| { TuskError::Custom("Cannot delete: no primary key and no ctids provided".into())