Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 45 additions & 43 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **File > Close All Tabs**, **Close Other Tabs**, and **Close Tabs for Other Databases** close a group of tabs in one step instead of one at a time. Closing every tab leaves the window open on its empty state with the connection still live, and each closed tab stays in **Recently Closed**. None of the three has a shortcut out of the box; bind them in **Settings > Keyboard**. (#1972)
- AI Explain, Optimize, and Fix Error now answer with a walkthrough in the chat panel: a before/after SQL diff you can switch between unified and split, numbered steps anchored to the lines they change, and a follow-up prompt on any step. Optimize and Fix add an Apply to Editor button that asks before replacing your query. (#1945)
- Create a connection from a project folder. Pick one from the welcome screen or File > Open Project Folder..., and TablePro reads the database settings it finds in `.env` files, `wp-config.php`, `prisma/schema.prisma`, `config/database.yml`, `docker-compose.yml`, `application.properties`, `application.yml`, and `appsettings.json`. A project that uses more than one engine gets a row for each. Review what it found, pick one, and the connection form opens filled in. Nothing is saved or connected until you save it. (#1959)
- A tool call limit you can raise or turn off, in **Settings > AI**. It defaults to 25 per reply, up from a fixed 10. Reaching it now pauses the reply and keeps everything the AI found, with **Continue** to carry on and **Adjust Limit** to change the setting. Before, a long investigation ended in "AI made too many tool calls in one response" and the partial answer was thrown away. Copilot runs its own tool loop and ignores this setting. (#1987)
- A pin button on result tabs. It shows on the tab you are on and when you point at a tab, and stays visible once the result is pinned. Pinned results move to the front of the strip. Pinning was already there, but only as a right-click item and a View menu command, so nobody found it. (#1982)

### Changed

- The welcome screen groups the ways to add an existing connection into one **Add from Existing** menu, next to **Create Connection**. Import from URL now has a home in that menu and in the File menu, where it had none. **Try Sample Database** moved to the connection list, which already offers it when the list is empty.
- A failed iCloud sync on iPhone and iPad now says what went wrong instead of showing a raw error. Out of storage, no network, and a rejected change each get their own message, the same ones the Mac already showed. The Mac and the mobile app now run the same sync engine, so a sync fix on one reaches the other. (#1990)

### Fixed

- Editing a connection on one device no longer overwrites a different field of the same connection edited on another. Each device sent the whole connection on every sync, so the last one to sync won outright. Sync now sends only the fields that actually changed and merges the rest. (#643)
- A per-connection query timeout set on iPhone or iPad no longer stops that connection from syncing at all. (#643)
- A device catching up on a lot of iCloud changes at once now downloads all of them. Only the first page of changes was read and the rest were skipped, and the sync position was still moved past them, so those changes never arrived. (#643)
- iCloud Sync reports a failed sync instead of always reporting success. A rejected change was logged and then dropped, the status went back to idle, and the Last Synced time was stamped as though everything had worked, so there was no way to tell sync was broken. (#643)
- One rejected change no longer stops every other change in the same batch from syncing. Each change is now confirmed on its own, and only the ones iCloud accepted are marked as sent, so the rest are retried instead of being silently discarded. (#643)
- Connection changes made on the Mac reach the iPhone and iPad app again. The Mac was sending fields that its iCloud database does not hold, so iCloud rejected every connection it sent while the app still reported the sync as successful. Changes made on iPhone or iPad always reached the Mac, which is why sync looked like it worked in one direction only. (#643)
- A connection synced from iPhone or iPad keeps its safe mode setting on the Mac. Read-only and confirm-writes connections arrived with no protection at all, because the Mac did not recognise the values the mobile app writes and fell back to Silent. An unrecognised value now asks for confirmation before a write instead of allowing it. (#643)
- A connection's colour tag set on iPhone or iPad no longer overwrites the colour label on the Mac. Both were stored in the same field despite meaning different things. (#643)
- **Primary Key**, **Nullable**, **Auto Inc**, and the index **Unique** flag now change when you pick a value. The cell read YES or NO but the menu offered true and false, so picking true did nothing on PostgreSQL, DuckDB, BigQuery, Snowflake, Cassandra, Trino, and SurrealDB. The menu now offers YES and NO to match the cell, in both the new table editor and the structure editor, and no longer offers Set NULL for a flag that has no NULL state. Setting Primary Key to YES now also sets Nullable to NO, since a primary key column cannot hold NULL. (#1991)
- A message sent in the AI panel right after launch is no longer replaced by the last saved conversation. Restoring that conversation reads from disk in the background, and it used to overwrite whatever you had already typed and sent.
- A PostgreSQL partitioned table is listed once instead of once per partition. Expand it in the sidebar to see its partitions, and expand one again if it is subpartitioned. A table split into hundreds of partitions no longer buries the rest of the schema, and those partitions no longer fill up query autocomplete, Cmd+K, and the export picker. (#1984)
- An SSH tunnel that cannot reach the database now says so, instead of leaving the database driver to report a timeout reading the server greeting. MySQL showed this as "Lost connection to server at 'handshake: reading initial communication packet', system error: 35", which named no cause. TablePro now checks the destination is reachable before the tunnel is handed over, and reports whether it was refused or timed out. The most common cause is the **Host** field holding the server's public address while the database only listens on `127.0.0.1`; the SSH server resolves that address from where it sits, so **Host** should be `localhost`. (#1981)
- The macOS local network prompt now says why TablePro wants access. The app never declared a reason, so the prompt showed a generic message that was easy to deny, which left SSH tunnels and databases on your own network failing with "no route to host".
- An SSH tunnel no longer dies while it is busy. The keep-alive read a send that had to wait as a dead tunnel, so heavy traffic through the tunnel could tear it down along with every connection using it.
- A `ProxyCommand` line in `~/.ssh/config` no longer passes without a word. TablePro cannot run the command, so it connects straight to the hostname and can land somewhere `ssh` never would. It now logs that it skipped the directive. `ProxyJump` is still followed as before.
- Right-clicking below the SQL editor opens the menu for what you clicked. A query taller than the editor pane made the editor answer right-clicks on the result tabs, the data grid, and the status bar with its own menu, which put **Pin Result**, the row actions, and **Clear Results** out of reach. (#1982)
- **View > Pin Result** is enabled only when there is a result tab to pin. It stayed on in JSON view, during Explain, and in Structure view, where it either did nothing or pinned a result you could not see. Result tabs now also show in JSON view, so a multi-statement run lets you switch between its results there. (#1982)
- Closing a window that holds more than one tab now asks about unsaved changes in any of them, not just the tab on screen. (#1972)
- MongoDB no longer fails with an authentication error when you open a database your credentials do not belong to. TablePro authenticated against whichever database you were browsing instead of the one set on the connection, which left the connection broken until you deleted and recreated it. Creating a database hit this every time. (#1970)
- Creating a MongoDB database now asks for its first collection and keeps it. The database was created and then removed again, because MongoDB drops a database as soon as its last collection goes. (#1970)
- A MongoDB authentication error now names the user and the database it tried to authenticate against, instead of a bare driver code. (#1970)
- Exporting a MongoDB collection works again. Every format failed with "Unsupported MongoDB method: getCollection" before any data was read. You can now also write `db.getCollection("name")` in the query editor, which is the only way to reach collections whose names contain dots or spaces, start with a digit, or match a database method such as `stats`. (#1971)
- Exported MongoDB collections keep every field. TablePro took the column list from the first document alone, so a field that showed up later was dropped from JSON exports and pushed CSV rows out of line with their header. (#1971)
- The `postgres` database shows in the database list again. It was marked as a system database, which hid it from the sidebar, Cmd+K, the database filter, and the Backup and Restore Dump pickers. PostgreSQL creates it for users and applications, so nothing about it is internal. CockroachDB's `defaultdb` and Redshift's `dev` were hidden the same way and now show too. (#1967)
- The database a connection is using always shows in the sidebar and the database switchers, even when it is a system database or the database filter excludes it. (#1967)
- The license activation sheet now opens when you click **Activate License**. It was built and then failed to appear, and once that happened further clicks did nothing at all.
- File > New Connection... and ⌘N now open the connection chooser once you have a connection open. They did nothing at all, because the welcome window closes as soon as you connect and the command was addressed to it. Import from Other App..., Open Project Folder..., Import Connections..., Import from URL... and Export Connections... had the same fault and are fixed with it. (#1975)
- The tooltip on the welcome screen's **+** button shows the shortcut you actually have bound for New Connection instead of always claiming ⌘N.
- The AI chat panel stays inside the right panel when you drag it narrow. The model name, tool names, and code block headers now truncate, long chat messages and code wrap, wide tables scroll inside their own box, and the composer text follows the panel width instead of running under the editor. (#1956)
- **Close All Tabs**, **Close Other Tabs**, and **Close Tabs for Other Databases** in the File menu, bindable in **Settings > Keyboard**. (#1972)
- AI Explain, Optimize, and Fix Error answer with numbered steps and a before/after SQL diff you can apply to the editor. (#1945)
- Create a connection from a project folder, filled in from `.env`, `wp-config.php`, `prisma/schema.prisma`, `config/database.yml`, `docker-compose.yml`, `application.properties`, `application.yml`, or `appsettings.json`. (#1959)
- An AI tool call limit in **Settings > AI**, now 25 per reply instead of a fixed 10, with **Continue** to resume a paused reply. (#1987)
- A pin button on result tabs. (#1982)
- The inspector edits the selected column, index, or foreign key in the Structure tab, and its edits join the grid's pending changes.

### Changed

- The welcome screen groups Import from Other App, Open Project Folder, Import Connections, and Import from URL into one **Add from Existing** menu, and offers **Try Sample Database** in the connection list.
- A failed iCloud sync on iPhone and iPad names the cause: out of storage, no network, or a rejected change. (#1990)

### Fixed

- Selecting a row in the Structure tab shows that row in the inspector instead of the data row in the same position. An edit made in the inspector there, and Duplicate Row and Copy with Headers, no longer act on a data row you never selected.
- Editing a connection on two devices no longer overwrites the field the other one changed. (#643)
- A per-connection query timeout set on iPhone or iPad no longer stops that connection from syncing. (#643)
- iCloud Sync downloads every page of changes when catching up, instead of only the first. (#643)
- iCloud Sync reports a failed sync instead of always reporting success. (#643)
- A change iCloud rejects no longer stops the rest of its batch from syncing. (#643)
- Connection changes made on the Mac reach the iPhone and iPad app again. (#643)
- A connection synced from iPhone or iPad keeps its safe mode setting on the Mac. (#643)
- A connection's colour tag set on iPhone or iPad no longer overwrites its colour label on the Mac. (#643)
- **Primary Key**, **Nullable**, **Auto Inc**, and the index **Unique** flag now change when you pick a value, and no longer offer Set NULL. (#1991)
- A message sent in the AI panel right after launch is no longer replaced by the restored conversation.
- A PostgreSQL partitioned table is listed once, with its partitions nested under it. (#1984)
- An SSH tunnel that cannot reach the database reports whether the connection was refused or timed out. (#1981)
- The macOS local network prompt says why TablePro needs access.
- An SSH tunnel no longer drops under heavy traffic.
- A `ProxyCommand` line in `~/.ssh/config` is logged as skipped instead of ignored without a word.
- Right-clicking below the SQL editor opens the menu for the result tabs, data grid, or status bar instead of the editor's. (#1982)
- **View > Pin Result** is enabled only when there is a result tab to pin, and result tabs now show in JSON view. (#1982)
- Closing a window asks about unsaved changes in every tab, not just the one on screen. (#1972)
- MongoDB authenticates against the database set on the connection, not the one you are browsing. (#1970)
- Creating a MongoDB database asks for its first collection, so the database is kept. (#1970)
- A MongoDB authentication error names the user and the database it tried. (#1970)
- Exporting a MongoDB collection works again, and `db.getCollection("name")` is accepted in the query editor. (#1971)
- A MongoDB export includes fields that first appear after the opening document. (#1971)
- `postgres`, CockroachDB's `defaultdb`, and Redshift's `dev` are no longer hidden as system databases. (#1967)
- The database a connection is using always shows in the sidebar and the database switchers. (#1967)
- The license activation sheet opens when you click **Activate License**.
- File > New Connection..., ⌘N, and the other File menu connection commands work once a connection is open. (#1975)
- The tooltip on the welcome screen's **+** button shows the shortcut bound to New Connection.
- The AI chat panel stays inside the right panel at narrow widths. (#1956)
- BigQuery `REPEATED` columns (including repeated `STRUCT`) no longer show every element as `null`. (#1963)
- BigQuery `STRUCT` cells now show nested structs and arrays as real JSON instead of escaped text, and quotes, backslashes, tabs, and newlines inside values are escaped correctly. (#1963)
- BigQuery `STRUCT` cells show nested structs and arrays as JSON instead of escaped text. (#1963)

## [0.60.1] - 2026-07-25

Expand Down
12 changes: 12 additions & 0 deletions TablePro/Core/DataGrid/RowDisplayBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ final class RowDisplayCache {
totalCost = 0
}

/// Drops one row's formatted values while keeping its box, so the next read
/// reformats from the current cell values. Row ids are positional, so a row
/// whose content changed in place keeps its id and would otherwise be served
/// its pre-edit text.
func clearValues(forID id: RowID) {
guard let box = storage[id] else { return }
totalCost -= rowCost(box.values)
for index in box.values.indices {
box.values[index] = nil
}
}

private func evictIfNeeded() {
while storage.count > countLimit || totalCost > costLimit {
guard insertionHead < insertionOrder.count else { break }
Expand Down
19 changes: 19 additions & 0 deletions TablePro/Models/UI/FieldEditorKind.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// FieldEditorKind.swift
// TablePro
//

import Foundation

internal enum FieldEditorKind: Equatable {
case json
case phpSerialized
case blobHex
case boolean
case enumPicker(values: [String])
case setPicker(values: [String])
case typePicker
case schemaText
case multiLine
case singleLine
}
28 changes: 28 additions & 0 deletions TablePro/Models/UI/GridSelectionOwner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// GridSelectionOwner.swift
// TablePro
//
// Resolves which grid owns the row selection in `GridSelectionState`. The data grid,
// the structure grid, and the new-table grid all publish into that one channel, so
// every consumer has to agree on whose display positions the indices are.
//

import Foundation

internal enum GridSelectionOwner: Equatable {
case dataGrid
case schemaGrid
case none

static func resolve(tabType: TabType?, resultsViewMode: ResultsViewMode?) -> GridSelectionOwner {
guard let tabType else { return .none }
if tabType == .createTable { return .schemaGrid }
if resultsViewMode == .structure { return .schemaGrid }
switch tabType {
case .table, .query:
return .dataGrid
case .createTable, .erDiagram, .serverDashboard, .usersRoles:
return .none
}
}
}
35 changes: 35 additions & 0 deletions TablePro/Models/UI/InspectorRowSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// InspectorRowSource.swift
// TablePro
//
// Lets the grid that owns the current selection supply the inspector's
// fields and receive its edits, instead of the inspector assuming every
// selection indexes the data tab's rows.
//

import Foundation

internal struct InspectorRowField: Equatable {
let name: String
let value: String?
let editor: FieldEditorKind
let isModified: Bool

init(name: String, value: String?, editor: FieldEditorKind, isModified: Bool = false) {
self.name = name
self.value = value
self.editor = editor
self.isModified = isModified
}
}

internal struct InspectorRow: Equatable {
let fields: [InspectorRowField]
let isEditable: Bool
}

@MainActor
internal protocol InspectorRowSource: AnyObject {
func inspectorRow(atDisplayRow displayRow: Int) -> InspectorRow?
func commitInspectorField(displayRow: Int, fieldIndex: Int, value: String?)
}
Loading
Loading