@@ -1677,6 +1677,19 @@ impl SimpleComponent for BrowseTab {
16771677 match msg {
16781678 BrowseTabInput :: RowsLoaded { offset, result } => {
16791679 self . current_offset = offset;
1680+ // Driver fallback: every shipping driver derives the
1681+ // `QueryResult.columns` list from the FIRST returned
1682+ // row, so a zero-row page comes back with an empty
1683+ // columns vector. The grid factory iterates that vector
1684+ // to build column-view columns, which left the empty
1685+ // table rendering as a column-less dark rectangle.
1686+ // information_schema (via ColumnsLoaded) already gave
1687+ // us the authoritative column list — substitute it in
1688+ // so the headers render even when the page is empty.
1689+ let mut result = result;
1690+ if result. columns . is_empty ( ) && !self . current_columns . is_empty ( ) {
1691+ result. columns = self . current_columns . clone ( ) ;
1692+ }
16801693 self . current_result = Some ( result) ;
16811694 // Defer rendering until columns are also loaded — the
16821695 // QueryResult's ColumnInfo lacks `primary_key` /
@@ -1690,6 +1703,16 @@ impl SimpleComponent for BrowseTab {
16901703 BrowseTabInput :: ColumnsLoaded ( columns) => {
16911704 let words: Vec < String > = columns. iter ( ) . map ( |c| c. name . clone ( ) ) . collect ( ) ;
16921705 self . current_columns = columns. clone ( ) ;
1706+ // Late-arriving columns: if RowsLoaded already cached a
1707+ // zero-row result with an empty `columns` vector (the
1708+ // driver derives it from the first row), refill it now
1709+ // so the upcoming `render_grid_if_ready` builds headers
1710+ // against the real schema instead of an empty list.
1711+ if let Some ( result) = self . current_result . as_mut ( )
1712+ && result. columns . is_empty ( )
1713+ {
1714+ result. columns = columns. clone ( ) ;
1715+ }
16931716 self . refresh_crud_buttons ( ) ;
16941717 // Filter strip rebuilds against the new schema —
16951718 // operator allowlists narrow per type, so a column
0 commit comments