Skip to content

Commit 6a6ec3a

Browse files
authored
Merge branch 'main' into perf/optimize-table-loading
Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
2 parents 3ccea20 + f2384b2 commit 6a6ec3a

95 files changed

Lines changed: 21758 additions & 980 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/fix-issue/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description: >-
77
scope, no quick patches. It runs a parallel investigation team (codebase tracing + Apple
88
platform research + competitor/UX research), synthesizes a refactor-aware implementation
99
blueprint, gets the user's approval, then implements to TablePro's standards. Trigger on
10-
things like "fix issue #1234", "fix this bug", "this should behave like TablePlus", "do this
10+
things like "fix issue #1234", "fix this bug", "this should behave like a native app", "do this
1111
properly / natively", or any non-trivial defect or behaviour gap in the app. Prefer this over
1212
an ad-hoc fix when the change touches UI behaviour, architecture, or anything the user expects
1313
to match Apple conventions.
@@ -59,7 +59,7 @@ Then spawn the three roles in a single turn so they run concurrently. Full chart
5959
| --- | --- | --- |
6060
| Codebase Analyzer | `feature-dev:code-explorer` | How does the relevant code actually work today? Which files, types, and call paths are involved? Where is the real cause? |
6161
| Apple Platform Researcher | `general-purpose` | What does Apple's documentation (HIG, AppKit, SwiftUI) say the correct behaviour and the right API are? |
62-
| Competitor / UX Researcher | `general-purpose` | How do TablePlus, DataGrip, Postico, and similar native clients handle this? What's the expected UX? |
62+
| Competitor / UX Researcher | `general-purpose` | How do DataGrip, Postico, Sequel Ace, and similar native clients handle this? What's the expected UX? |
6363

6464
Give each investigator the Phase 0 problem statement and a sharp question. Tell them to report findings as a structured message back to you, citing concrete evidence (`file:line` for code, doc URLs or API names for platform research). `references/research-sources.md` lists the documentation map and the tools each researcher should reach for (`mcp__xcode__DocumentationSearch`, `WebSearch`, `WebFetch`).
6565

.claude/skills/fix-issue/references/research-sources.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ Prefer the Xcode docs tool for API questions: it's authoritative and version-mat
2424

2525
Native macOS database clients worth checking for expected behaviour:
2626

27-
- **TablePlus** — the closest comparison; TablePro positions against it. `https://docs.tableplus.com`.
2827
- **DataGrip** — JetBrains, feature-rich; good for data-grid and SQL-editor behaviour.
2928
- **Postico** — native macOS Postgres client, strong HIG conformance; good model for "what feels native on macOS".
3029
- **Sequel Ace** — open-source MySQL client; behaviour is inspectable.

.claude/skills/fix-issue/references/team-roles.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ structured message with citations.
7272

7373
```
7474
You are the Competitor / UX Researcher on a TablePro fix investigation. TablePro is a native
75-
macOS database client, a fast alternative to TablePlus.
75+
macOS database client.
7676
7777
Problem statement:
7878
<PROBLEM STATEMENT>
7979
8080
Research how mature native DB clients handle this interaction. I need:
81-
1. How TablePlus, DataGrip, Postico, and Sequel Ace handle this specific behaviour or UI, as
81+
1. How DataGrip, Postico, and Sequel Ace handle this specific behaviour or UI, as
8282
concretely as you can (documented behaviour, help docs, release notes, reviews, screenshots).
8383
2. The interaction pattern users expect: what the control looks like, what the keyboard/mouse
8484
affordances are, edge cases these tools handle.

CHANGELOG.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- The tree sidebar can show only the databases you pick. Use the filter button to check the ones you want, with a search box for long lists. The choice is saved per connection. (#1667)
12+
- The table structure view has a Triggers tab for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, libSQL, and Cloudflare D1. It lists each trigger with its timing and event (plus enabled state where the engine reports it), with a filter field and sortable columns. Selecting a trigger shows its full definition in a read-only syntax-highlighted viewer. (#1695)
13+
- Traditional Chinese (繁體中文) language in Settings > General with full UI translation
14+
- An Add button in the table status bar inserts a new row at the end of the grid and starts editing it.
15+
16+
### Changed
17+
18+
- Selecting a Redis namespace in the sidebar key tree now filters the open database view to that prefix, with paging, instead of opening a separate tab limited to one batch of keys. (#1701)
19+
20+
### Fixed
21+
22+
- Redis entries no longer disappear after the connection sits idle. The health check was running `SELECT 1`, which on Redis switches the active database, so a later refresh scanned the wrong database. (#1701)
23+
- Redis key browsing now lists every key in a database or namespace and pages through them correctly. It was reading only the first SCAN batch, so large keyspaces showed a partial, fixed set of keys. (#1701)
24+
- A dropped Redis connection now reconnects on the next command and replays auth and the selected database, instead of failing until the next health check. (#1701)
25+
- DuckDB VARIANT columns now show their value as text instead of an empty cell.
26+
27+
## [0.51.1] - 2026-06-16
28+
29+
### Added
30+
31+
- The tree sidebar can filter to only the databases you pick, saved per connection. (#1667)
32+
- Closing a query tab no longer loses unsaved SQL. The next blank query tab for the same connection restores the last closed draft. (#1686)
33+
- A checkbox in the filter panel header turns every filter row on or off at once, with a dash when only some are on.
34+
35+
### Changed
36+
37+
- The filter panel's "Unset" button is now "Clear". It keeps your filter rows and only drops the applied state. To remove the rows, use "Remove All Filters" in the filter options menu.
38+
- A row's right-click menu now has "Apply Only This Filter". The inline per-row Apply button is gone.
1339

1440
### Changed
1541

@@ -19,6 +45,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1945

2046
- Expanding or collapsing a database or schema in the tree sidebar while its tables were still loading could crash the app. The tree now updates its rows without rebuilding the outline structure.
2147
- MongoDB filters on `_id` and other ObjectId fields now match. A 24-character hex value is matched as an ObjectId as well as a string, so filtering by `_id` returns the row instead of nothing. (#1682)
48+
- Shift+Arrow in the data grid now starts and extends a cell selection from the focused cell. Cmd+Shift+Arrow extends to the row or column edge.
49+
- Delete key now removes all rows covered by a cell-range selection instead of ignoring it.
50+
- Right-clicking inside a multi-row or cell-range selection no longer collapses the selection first.
51+
- Oracle connections no longer crash during connect when the server sends a short or unexpected handshake packet. (#1683)
52+
- MongoDB filters on `_id` and other ObjectId fields now match. A 24-character hex value is matched as an ObjectId as well as a string. (#1682)
53+
- The sidebar and inspector keep their width per connection, the sidebar its collapsed state, and the inspector its selected tab, across quit and reopen.
2254

2355
## [0.51.0] - 2026-06-13
2456

@@ -2281,7 +2313,8 @@ TablePro is a native macOS database client built with SwiftUI and AppKit, design
22812313
- Custom SQL query templates
22822314
- Performance optimized for large datasets
22832315

2284-
[Unreleased]: https://github.com/TableProApp/TablePro/compare/v0.51.0...HEAD
2316+
[Unreleased]: https://github.com/TableProApp/TablePro/compare/v0.51.1...HEAD
2317+
[0.51.1]: https://github.com/TableProApp/TablePro/compare/v0.51.0...v0.51.1
22852318
[0.51.0]: https://github.com/TableProApp/TablePro/compare/v0.50.0...v0.51.0
22862319
[0.50.0]: https://github.com/TableProApp/TablePro/compare/v0.49.1...v0.50.0
22872320
[0.49.1]: https://github.com/TableProApp/TablePro/compare/v0.49.0...v0.49.1

Plugins/CloudflareD1DriverPlugin/CloudflareD1Plugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ final class CloudflareD1Plugin: NSObject, TableProPlugin, DriverPlugin {
2626
static let isDownloadable = true
2727
static let supportsImport = false
2828
static let supportsSchemaEditing = true
29+
static let supportsTriggers = true
2930
static let databaseGroupingStrategy: GroupingStrategy = .flat
3031
static let brandColorHex = "#F6821F"
3132
static let urlSchemes: [String] = ["d1"]

Plugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,27 @@ final class CloudflareD1PluginDriver: PluginDatabaseDriver, @unchecked Sendable
433433
}
434434
}
435435

436+
func fetchTriggers(table: String, schema: String?) async throws -> [PluginTriggerInfo] {
437+
let safeTable = escapeStringLiteral(table)
438+
let query = """
439+
SELECT name, sql FROM sqlite_master
440+
WHERE type = 'trigger' AND tbl_name = '\(safeTable)'
441+
AND name NOT GLOB '_cf_*'
442+
ORDER BY name
443+
"""
444+
let result = try await execute(query: query)
445+
446+
return result.rows.compactMap { row -> PluginTriggerInfo? in
447+
guard row.count >= 2,
448+
let name = row[0].asText,
449+
let sql = row[1].asText else {
450+
return nil
451+
}
452+
let (timing, event) = TriggerSQLParser.timingAndEvent(from: sql)
453+
return PluginTriggerInfo(name: name, timing: timing, event: event, statement: sql)
454+
}
455+
}
456+
436457
func fetchTableDDL(table: String, schema: String?) async throws -> String {
437458
let safeTable = escapeStringLiteral(table)
438459
let query = """

Plugins/DuckDBDriverPlugin/DuckDBConnection.swift

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ actor DuckDBConnectionActor {
9191
}
9292

9393
var raw = Self.extractResult(from: &result, startTime: startTime)
94-
Self.patchTzColumns(&raw, query: query, connection: conn)
94+
Self.patchCastedColumns(&raw, query: query, connection: conn)
9595
return raw
9696
}
9797

@@ -163,7 +163,7 @@ actor DuckDBConnectionActor {
163163
}
164164

165165
var raw = Self.extractResult(from: &result, startTime: startTime)
166-
Self.patchTzColumns(&raw, query: query, connection: conn)
166+
Self.patchCastedColumns(&raw, query: query, connection: conn)
167167
return raw
168168
}
169169

@@ -204,7 +204,7 @@ actor DuckDBConnectionActor {
204204
columnTypeNames.append(Self.typeName(for: colType))
205205
}
206206

207-
if columnTypes.contains(where: Self.isUnrenderable) {
207+
if columnTypes.contains(where: Self.requiresTextCast) {
208208
duckdb_destroy_result(&result)
209209
try Self.streamWrappedQuery(
210210
query: query,
@@ -235,7 +235,7 @@ actor DuckDBConnectionActor {
235235
continuation: AsyncThrowingStream<PluginStreamElement, Error>.Continuation
236236
) throws {
237237
let castExprs = columns.enumerated().map { i, name in
238-
castExpression(for: columnTypes[i], column: name)
238+
projection(for: columnTypes[i], column: name)
239239
}
240240
let wrappedQuery = buildWrappedQuery(originalQuery: query, castExprs: castExprs)
241241

@@ -484,16 +484,16 @@ actor DuckDBConnectionActor {
484484
}
485485
}
486486

487-
static func patchTzColumns(
487+
static func patchCastedColumns(
488488
_ raw: inout DuckDBRawResult, query: String, connection: duckdb_connection
489489
) {
490490
let patchedColIndices = raw.columnTypes.enumerated().compactMap { idx, type in
491-
isUnrenderable(type) ? idx : nil
491+
requiresTextCast(type) ? idx : nil
492492
}
493493
guard !patchedColIndices.isEmpty, !raw.rows.isEmpty else { return }
494494

495495
let castExprs = raw.columns.enumerated().map { i, name in
496-
castExpression(for: raw.columnTypes[i], column: name)
496+
projection(for: raw.columnTypes[i], column: name)
497497
}
498498
let wrappedQuery = buildWrappedQuery(originalQuery: query, castExprs: castExprs)
499499

@@ -514,25 +514,36 @@ actor DuckDBConnectionActor {
514514
}
515515
}
516516

517-
static func isUnrenderable(_ type: duckdb_type) -> Bool {
517+
static func isNativelyRenderable(_ type: duckdb_type) -> Bool {
518518
switch type {
519-
case DUCKDB_TYPE_TIMESTAMP_TZ, DUCKDB_TYPE_TIME_TZ, DUCKDB_TYPE_GEOMETRY:
519+
case DUCKDB_TYPE_BOOLEAN,
520+
DUCKDB_TYPE_TINYINT, DUCKDB_TYPE_SMALLINT, DUCKDB_TYPE_INTEGER, DUCKDB_TYPE_BIGINT, DUCKDB_TYPE_HUGEINT,
521+
DUCKDB_TYPE_UTINYINT, DUCKDB_TYPE_USMALLINT, DUCKDB_TYPE_UINTEGER, DUCKDB_TYPE_UBIGINT, DUCKDB_TYPE_UHUGEINT,
522+
DUCKDB_TYPE_FLOAT, DUCKDB_TYPE_DOUBLE, DUCKDB_TYPE_DECIMAL,
523+
DUCKDB_TYPE_VARCHAR, DUCKDB_TYPE_BLOB, DUCKDB_TYPE_UUID, DUCKDB_TYPE_BIT, DUCKDB_TYPE_ENUM,
524+
DUCKDB_TYPE_DATE, DUCKDB_TYPE_TIME, DUCKDB_TYPE_TIME_NS, DUCKDB_TYPE_INTERVAL,
525+
DUCKDB_TYPE_TIMESTAMP, DUCKDB_TYPE_TIMESTAMP_S, DUCKDB_TYPE_TIMESTAMP_MS, DUCKDB_TYPE_TIMESTAMP_NS,
526+
DUCKDB_TYPE_LIST, DUCKDB_TYPE_STRUCT, DUCKDB_TYPE_MAP, DUCKDB_TYPE_ARRAY, DUCKDB_TYPE_UNION:
520527
return true
521528
default:
522529
return false
523530
}
524531
}
525532

533+
static func requiresTextCast(_ type: duckdb_type) -> Bool {
534+
!isNativelyRenderable(type)
535+
}
536+
526537
static func castExpression(for type: duckdb_type, column: String) -> String {
527538
let quoted = quoteIdentifier(column)
528-
switch type {
529-
case DUCKDB_TYPE_GEOMETRY:
539+
if type == DUCKDB_TYPE_GEOMETRY {
530540
return "CASE WHEN \(quoted) IS NULL THEN NULL ELSE ST_AsText(\(quoted)) END AS \(quoted)"
531-
case DUCKDB_TYPE_TIMESTAMP_TZ, DUCKDB_TYPE_TIME_TZ:
532-
return "CASE WHEN \(quoted) IS NULL THEN NULL ELSE CAST(\(quoted) AS VARCHAR) END AS \(quoted)"
533-
default:
534-
return quoted
535541
}
542+
return "CASE WHEN \(quoted) IS NULL THEN NULL ELSE CAST(\(quoted) AS VARCHAR) END AS \(quoted)"
543+
}
544+
545+
static func projection(for type: duckdb_type, column: String) -> String {
546+
requiresTextCast(type) ? castExpression(for: type, column: column) : quoteIdentifier(column)
536547
}
537548

538549
static func buildWrappedQuery(originalQuery: String, castExprs: [String]) -> String {

Plugins/LibSQLDriverPlugin/LibSQLPlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ final class LibSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
2828
static let isDownloadable = true
2929
static let supportsImport = false
3030
static let supportsSchemaEditing = true
31+
static let supportsTriggers = true
3132
static let supportsDropDatabase = false
3233
static let supportsDatabaseSwitching = false
3334
static let databaseGroupingStrategy: GroupingStrategy = .flat

Plugins/LibSQLDriverPlugin/LibSQLPluginDriver.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,26 @@ final class LibSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
505505
}
506506
}
507507

508+
func fetchTriggers(table: String, schema: String?) async throws -> [PluginTriggerInfo] {
509+
let safeTable = escapeStringLiteral(table)
510+
let query = """
511+
SELECT name, sql FROM sqlite_master
512+
WHERE type = 'trigger' AND tbl_name = '\(safeTable)'
513+
ORDER BY name
514+
"""
515+
let result = try await execute(query: query)
516+
517+
return result.rows.compactMap { row -> PluginTriggerInfo? in
518+
guard row.count >= 2,
519+
let name = row[0].asText,
520+
let sql = row[1].asText else {
521+
return nil
522+
}
523+
let (timing, event) = TriggerSQLParser.timingAndEvent(from: sql)
524+
return PluginTriggerInfo(name: name, timing: timing, event: event, statement: sql)
525+
}
526+
}
527+
508528
func fetchTableDDL(table: String, schema: String?) async throws -> String {
509529
let safeTable = escapeStringLiteral(table)
510530
let query = """

Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
150150
)
151151

152152
static let supportsDropDatabase = true
153+
static let supportsTriggers = true
153154

154155
func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver {
155156
MSSQLPluginDriver(config: config)

0 commit comments

Comments
 (0)