Skip to content

Fix/bug#6

Merged
datlechin merged 6 commits intomainfrom
fix/bug
Dec 25, 2025
Merged

Fix/bug#6
datlechin merged 6 commits intomainfrom
fix/bug

Conversation

@datlechin
Copy link
Copy Markdown
Member

No description provided.

Resolved race condition where opening a table tab as the first tab (no other tabs open) would not trigger data loading.

Root cause: When two async runQuery() calls occurred (one from openTableData, one from lazy load in handleTabChange), the second call would cancel the first task via currentQueryTask?.cancel() before checking the isExecuting guard. This left the first query cancelled without starting a new one.

Fix: Moved currentQueryTask?.cancel() after the isExecuting guard check, ensuring we only cancel previous tasks when actually starting a new query.

Changes:
- Moved task cancellation logic after isExecuting guard in runQuery()
- Removed excessive debug logging added during investigation
- Kept only critical warning logs for errors
Copilot AI review requested due to automatic review settings December 24, 2025 12:36
Removed debug print statements from:
- TabStateStorage: tab saving/loading logs
- MainContentView: window close, connection ready, query update logs

Kept only critical error/warning logs.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements comprehensive fixes for tab state persistence and query execution timing issues. The changes address race conditions during window close, prevent duplicate query executions, and ensure reliable tab state restoration across app restarts.

Key Changes:

  • Implemented debounced tab state saving with proper cleanup on window close to prevent data loss
  • Added lazy loading mechanism for table tabs that waits for database connection before executing queries
  • Refactored tab restoration to prioritize disk-persisted state over session state for better reliability across app restarts

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 14 comments.

File Description
OpenTable/Views/MainContentView.swift Core logic changes: added state flags for dismissal/restoration tracking, implemented debounced save mechanism, added connection-ready detection, refactored query execution timing and tab restoration
OpenTable/OpenTableApp.swift Added notification name for window close events
OpenTable/Core/Storage/TabStateStorage.swift Added debug logging to track empty query saves and aid troubleshooting
OpenTable/AppDelegate.swift Modified window close handler to post notification and wait for view cleanup before disconnecting
Comments suppressed due to low confidence (5)

OpenTable/Views/MainContentView.swift:427

  • This complex retry logic with magic numbers and timing-sensitive behavior lacks comprehensive documentation. The function would benefit from a doc comment explaining: 1) Why tab restoration needs to wait for connection, 2) Why 5 seconds is the chosen timeout, 3) What happens if timeout occurs, and 4) How this interacts with the lazy load mechanism. This would help future maintainers understand the design decisions.
                        
                        // Wait for connection to be established
                        var retryCount = 0
                        while retryCount < 50 { // Max 5 seconds
                            if let session = DatabaseManager.shared.currentSession,
                               session.isConnected {
                                // Small delay to ensure everything is initialized
                                try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s
                                await MainActor.run {
                                    justRestoredTab = true  // Prevent lazy load from executing again
                                    runQuery()
                                }
                                break
                            }
                            
                            // Wait 100ms and retry
                            try? await Task.sleep(nanoseconds: 100_000_000)
                            retryCount += 1
                        }
                        
                        if retryCount >= 50 {
                            print("[MainContentView] ⚠️ Connection timeout, query not executed")
                        }
                    }
                }
            }
            .onChange(of: selectedTables) { oldTables, newTables in
                // Find newly added table to open
                let added = newTables.subtracting(oldTables)

OpenTable/Core/Storage/TabStateStorage.swift:42

  • The debug logging prints call stack symbols when an empty query is being saved. However, this only shows the first 10 stack frames and uses a simple forEach with print. Consider using joined(separator: "\n") for better formatted output, or using a proper logging framework that can capture full stack traces. Additionally, this debug code will execute in DEBUG builds for every save, which could impact performance if saves are frequent.
            // Silent failure - encoding errors are rare and non-critical
        }
    }
    
    /// Load tab state for a connection

OpenTable/Views/MainContentView.swift:601

  • The debounce task captures the current tabs state (line 586) but doesn't capture the connection ID. If the connection object changes or becomes invalid during the 500ms delay, the save operation could target the wrong connection or fail silently. Consider capturing connection.id as a local constant alongside tabsToSave and selectedId.
                                    
                                    // Only save if not cancelled and view not being dismissed
                                    guard !Task.isCancelled && !isDismissing else { return }
                                    
                                    // Save the captured tabs state (NOT current state which may have changed)
                                    TabStateStorage.shared.saveTabState(
                                        connectionId: connection.id,
                                        tabs: tabsToSave,
                                        selectedTabId: selectedId
                                    )
                                }
                            }
                        }
                    ),
                    cursorPosition: $cursorPosition,
                    onExecute: runQuery,
                    schemaProvider: schemaProvider

OpenTable/Views/MainContentView.swift:426

  • If the connection fails to establish within the timeout period (retryCount >= 50), the needsLazyLoad flag is never set and the query is not executed. However, the flag might have been set elsewhere (e.g., in handleTabChange). This means if the connection was never ready during tab restoration but later becomes ready, the query won't execute because needsLazyLoad was never set to true. Consider setting needsLazyLoad = true in the timeout case to allow the onChange handler to retry when the connection eventually becomes ready.
            }
            .onChange(of: selectedTables) { oldTables, newTables in
                // Find newly added table to open

OpenTable/Views/MainContentView.swift:1852

  • The code checks for sessionId and the flags isRestoringTabs and isDismissing, but only saves to disk without updating the session. This is inconsistent with the pattern used in other places (e.g., lines 218-221, 243-246) where both session and disk are updated. Either update the session here as well, or remove the sessionId check since disk saving doesn't require an active session.
        }
        
        // Save state to the old tab before switching
        if let oldId = oldTabId,
            let oldIndex = tabManager.tabs.firstIndex(where: { $0.id == oldId })
        {
            // Save pending changes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread OpenTable/Views/MainContentView.swift
Comment thread OpenTable/AppDelegate.swift Outdated
Comment thread OpenTable/Views/MainContentView.swift
Comment thread OpenTable/Views/MainContentView.swift Outdated
Comment thread OpenTable/Views/MainContentView.swift
Comment thread OpenTable/AppDelegate.swift Outdated
Comment thread OpenTable/Views/MainContentView.swift
Comment thread OpenTable/Views/MainContentView.swift
Comment thread OpenTable/Views/MainContentView.swift
Comment thread OpenTable/Views/MainContentView.swift
@datlechin
Copy link
Copy Markdown
Member Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown

Copilot AI commented Dec 24, 2025

@datlechin I've opened a new pull request, #7, to work on those changes. Once the pull request is ready, I'll request review from you.

Improvements:
- Extract magic numbers to named constants (tab save delay, connection check delay)
- Clear currentQueryTask reference on completion to avoid stale references
- Add isDismissing check in retry loop to prevent queries after view dismissal
- Simplify lazy load logic (remove redundant if check)
- Remove extra blank lines for consistency

These changes improve code maintainability and prevent potential edge case bugs.
Major refactoring to improve code organization and maintainability:

Phase 1 - DataChange.swift (1,095 → 4 files):
- SQLStatementGenerator: SQL generation logic
- DataChangeUndoManager: Undo/redo stack management
- DataChangeModels: Pure data models
- DataChangeManager: Refactored change tracking

Phase 2 - FilterPanelView.swift (758 → 260 lines):
- FilterRowView: Single filter row component
- SQLPreviewSheet: SQL preview modal
- FilterSettingsPopover: Settings popover
- QuickSearchField: Quick search component

Phase 3 - DataGridView.swift (1,706 → 700 lines):
- KeyHandlingTableView: NSTableView subclass
- TableRowViewWithMenu: Row view with context menu
- CellTextField: Custom text field
- DataGridCellFactory: Cell creation factory

Phase 4 - HistoryListViewController.swift (1,325 → 550 lines):
- HistoryDataProvider: Data fetching/filtering
- HistoryRowView: Table cell view
- HistoryTableView: Custom NSTableView

Phase 5 - MainContentView.swift (2,365 → 2,113 lines):
- QueryExecutionService: Query execution logic
- RowOperationsManager: Row operations (add/delete/undo)
- MainStatusBarView: Status bar component
- QueryTabContentView/TableTabContentView: Tab content views
@datlechin
Copy link
Copy Markdown
Member Author

@copilot review pr

Copy link
Copy Markdown

Copilot AI commented Dec 25, 2025

@datlechin I've opened a new pull request, #8, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 31 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@datlechin datlechin merged commit 1194d1f into main Dec 25, 2025
6 checks passed
@datlechin datlechin deleted the fix/bug branch December 25, 2025 01:48
datlechin added a commit that referenced this pull request Apr 28, 2026
Replace the placeholder StatusPage with the working ViewSwitcher
editor. The Structure tab now provides the full DDL editing surface
the audit asked for.

UI layout (adw::ToolbarView):

- Top: AdwHeaderBar with table-name Entry + driver badge.
- Content: AdwViewSwitcher + AdwViewStack with four pages:
    Columns       — one editable row per column: Name (Entry),
                    Type (ComboBoxText with curated per-driver
                    suggestions + free-text), Nullable / PK / Auto
                    (CheckButtons), Default (Entry), Remove (trash
                    icon). Add Column button trailing the rows.
    Indexes       — read-only ColumnView-like rows with Name /
                    Cols / UNIQUE/PRIMARY badge / Remove. Primary
                    index is non-removable (it's owned by the PK
                    column constraint).
    Foreign Keys  — read-only rows with Name / Cols / "→ ref"
                    pointer / Remove. Add Foreign Key button opens
                    an AdwAlertDialog form for name + col multiselect
                    + ref table + ref cols + ON DELETE / UPDATE
                    drop-downs.
    SQL Preview   — sourceview5::View read-only with sql syntax
                    highlighting. Regenerates on every tracker
                    OpsChanged event by calling materialize().
- Bottom: existing ActionBar with Discard / Save / Drop Table.

Editing flow:

- ColumnEdited / TableNameEdited / Add* / Remove* events mutate
  the in-memory model (Rc<RefCell<Vec<…>>>) and then push the
  matching StructureOp + inverse onto the per-tab tracker.
- New mode collapses everything into a single CreateTable op which
  is rebuilt on every mutation; Edit mode pushes per-attribute
  AlterColumn / AddColumn / DropColumn / AddIndex / DropIndex /
  AddForeignKey / DropForeignKey / RenameTable / RenameColumn ops.
  materialize() in StructureChangeTracker handles dialect quirks
  (MySQL alter-grouping, statement ordering).
- StructureTabInput::Refresh tears down the three list views and
  rebuilds them after every mutation. The lists are short enough
  (a handful of columns / indexes / FKs) that full rebuild is
  cheaper than per-row diffing.
- Save runs validate_save() first: rejects empty table name on New
  mode, missing column types, duplicate column names, nullable PK
  columns. Validation errors surface as toasts; structural errors
  from materialize() (e.g. SqliteNotSupported) surface as alerts.
- Discard restores the original DraftColumns from
  ColumnInfo.original (Edit mode) or empties the model (New mode).

SQLite limit handling: rows with original.is_some() get
sensitive=false + tooltip on the Type / Nullable / Default cells
when the driver is sqlite. The trash icon stays enabled (DROP COLUMN
landed in 3.35; older SQLite errors at execute time). Drop Foreign
Key buttons are disabled across the board for SQLite.

What's still deferred to follow-ups:
- MySQL drag-reorder of columns (driver_supports_reorder helper is
  in place; the gtk::DragSource + DropTarget wiring is non-trivial
  and benefits from manual GUI verification).
- update_added_columns_ops surgical replace — for now multiple
  AddColumn ops on the same column may stack; materialize-time
  ordering keeps the final SQL correct, but dirty-count is slightly
  inflated.
- Live drop-down of available tables in the Add Foreign Key dialog
  (today the user types "schema.table" free-text into the entry).

136 tests still green, clippy clean, build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants