From 1759dcf045d6ca67890ec7703c091be90a85a3b7 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 13:09:07 +0700 Subject: [PATCH 1/9] feat: add current statement highlighting and large document safety caps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Highlight the background of the SQL statement under cursor using EmphasisManager (outline with fill). Debounced at 150ms with generation counter to prevent stale updates. - Skip highlighting for single statements (no semicolons), multi-cursor, or documents >5MB. - Add maxHighlightableLength (5MB) guard to Highlighter — documents exceeding this are shown as plain text. - Cap highlight chunks to 2 per cycle (8192 chars) for documents >50KB to keep the editor responsive on large SQL dumps. - Add currentStatementHighlight theme color (light: #F0F4FA, dark: #1A2332). --- CHANGELOG.md | 4 + .../HighlightProviderState.swift | 7 +- .../Highlighting/Highlighter.swift | 4 + TablePro/Theme/ResolvedThemeColors.swift | 4 + TablePro/Theme/ThemeDefinition.swift | 6 + .../Editor/CurrentStatementHighlighter.swift | 117 ++++++++++++++++++ .../Views/Editor/SQLEditorCoordinator.swift | 8 ++ 7 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 TablePro/Views/Editor/CurrentStatementHighlighter.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index dc483082..f0f1369d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Current SQL statement background highlighting in query editor + ### Fixed - MongoDB Atlas connections failing to authenticate (#438) diff --git a/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/HighlightProviding/HighlightProviderState.swift b/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/HighlightProviding/HighlightProviderState.swift index c4ae800d..a7501384 100644 --- a/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/HighlightProviding/HighlightProviderState.swift +++ b/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/HighlightProviding/HighlightProviderState.swift @@ -32,6 +32,8 @@ class HighlightProviderState { /// The length to chunk ranges into when passing to the highlighter. private static let rangeChunkLimit = 4096 + private static let largeDocThreshold = 50_000 + // MARK: - State /// A unique identifier for this provider. Used by the delegate to determine the source of results. @@ -120,8 +122,11 @@ class HighlightProviderState { /// Accumulates all pending ranges and calls `queryHighlights`. func highlightInvalidRanges() { + let docLength = visibleRangeProvider?.documentRange.length ?? 0 + let maxRanges = docLength > Self.largeDocThreshold ? 2 : Int.max + var ranges: [NSRange] = [] - while let nextRange = getNextRange() { + while ranges.count < maxRanges, let nextRange = getNextRange() { ranges.append(nextRange) pendingSet.insert(range: nextRange) } diff --git a/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift b/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift index cefd82bc..a2599f71 100644 --- a/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift +++ b/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift @@ -81,6 +81,8 @@ class Highlighter: NSObject { /// Counts upwards to provide unique IDs for new highlight providers. private var providerIdCounter: Int + public var maxHighlightableLength: Int = 5_000_000 + // MARK: - Init init( @@ -226,6 +228,7 @@ extension Highlighter: @preconcurrency NSTextStorageDelegate { // This method is called whenever attributes are updated, so to avoid re-highlighting the entire document // each time an attribute is applied, we check to make sure this is in response to an edit. guard editedMask.contains(.editedCharacters) else { return } + guard textView?.textStorage.length ?? 0 <= maxHighlightableLength else { return } styleContainer.storageUpdated(editedRange: editedRange, changeInLength: delta) @@ -276,6 +279,7 @@ extension Highlighter: StyledRangeContainerDelegate { extension Highlighter: VisibleRangeProviderDelegate { func visibleSetDidUpdate(_ newIndices: IndexSet) { + guard textView?.textStorage.length ?? 0 <= maxHighlightableLength else { return } highlightProviders.forEach { $0.highlightInvalidRanges() } } } diff --git a/TablePro/Theme/ResolvedThemeColors.swift b/TablePro/Theme/ResolvedThemeColors.swift index 156b387e..e99da931 100644 --- a/TablePro/Theme/ResolvedThemeColors.swift +++ b/TablePro/Theme/ResolvedThemeColors.swift @@ -16,6 +16,8 @@ struct ResolvedEditorColors { let lineNumberSwiftUI: Color let invisibles: NSColor let invisiblesSwiftUI: Color + let currentStatementHighlight: NSColor + let currentStatementHighlightSwiftUI: Color let keyword: NSColor let keywordSwiftUI: Color @@ -49,6 +51,8 @@ struct ResolvedEditorColors { lineNumberSwiftUI = colors.lineNumber.swiftUIColor invisibles = colors.invisibles.nsColor invisiblesSwiftUI = colors.invisibles.swiftUIColor + currentStatementHighlight = colors.currentStatementHighlight.nsColor + currentStatementHighlightSwiftUI = colors.currentStatementHighlight.swiftUIColor keyword = colors.syntax.keyword.nsColor keywordSwiftUI = colors.syntax.keyword.swiftUIColor diff --git a/TablePro/Theme/ThemeDefinition.swift b/TablePro/Theme/ThemeDefinition.swift index ea7e1d80..f7ff507a 100644 --- a/TablePro/Theme/ThemeDefinition.swift +++ b/TablePro/Theme/ThemeDefinition.swift @@ -177,6 +177,7 @@ internal struct EditorThemeColors: Codable, Equatable, Sendable { var selection: String var lineNumber: String var invisibles: String + var currentStatementHighlight: String var syntax: SyntaxColors static let defaultLight = EditorThemeColors( @@ -187,6 +188,7 @@ internal struct EditorThemeColors: Codable, Equatable, Sendable { selection: "#B4D8FD", lineNumber: "#747478", invisibles: "#D6D6D6", + currentStatementHighlight: "#F0F4FA", syntax: .defaultLight ) @@ -198,6 +200,7 @@ internal struct EditorThemeColors: Codable, Equatable, Sendable { selection: String, lineNumber: String, invisibles: String, + currentStatementHighlight: String, syntax: SyntaxColors ) { self.background = background @@ -207,6 +210,7 @@ internal struct EditorThemeColors: Codable, Equatable, Sendable { self.selection = selection self.lineNumber = lineNumber self.invisibles = invisibles + self.currentStatementHighlight = currentStatementHighlight self.syntax = syntax } @@ -222,6 +226,8 @@ internal struct EditorThemeColors: Codable, Equatable, Sendable { selection = try container.decodeIfPresent(String.self, forKey: .selection) ?? fallback.selection lineNumber = try container.decodeIfPresent(String.self, forKey: .lineNumber) ?? fallback.lineNumber invisibles = try container.decodeIfPresent(String.self, forKey: .invisibles) ?? fallback.invisibles + currentStatementHighlight = try container.decodeIfPresent(String.self, forKey: .currentStatementHighlight) + ?? fallback.currentStatementHighlight syntax = try container.decodeIfPresent(SyntaxColors.self, forKey: .syntax) ?? fallback.syntax } } diff --git a/TablePro/Views/Editor/CurrentStatementHighlighter.swift b/TablePro/Views/Editor/CurrentStatementHighlighter.swift new file mode 100644 index 00000000..ebb26ee4 --- /dev/null +++ b/TablePro/Views/Editor/CurrentStatementHighlighter.swift @@ -0,0 +1,117 @@ +// +// CurrentStatementHighlighter.swift +// TablePro +// +// Highlights the background of the SQL statement under the cursor. +// + +import AppKit +import CodeEditSourceEditor +import CodeEditTextView + +@MainActor +final class CurrentStatementHighlighter { + private static let groupId = "tablepro.currentStatement" + private static let debounceInterval: TimeInterval = 0.15 + private static let maxDocumentLength = 5_000_000 + + private weak var controller: TextViewController? + private var debounceWorkItem: DispatchWorkItem? + private var lastHighlightedRange: NSRange? + private var generation: UInt64 = 0 + + func install(controller: TextViewController) { + self.controller = controller + } + + func uninstall() { + debounceWorkItem?.cancel() + debounceWorkItem = nil + clearHighlight() + controller = nil + } + + func handleCursorChange() { + scheduleUpdate() + } + + func handleTextChange() { + // Text changed — invalidate cached range and schedule update + lastHighlightedRange = nil + scheduleUpdate() + } + + private func scheduleUpdate() { + debounceWorkItem?.cancel() + generation &+= 1 + let currentGeneration = generation + + let workItem = DispatchWorkItem { [weak self] in + guard let self, self.generation == currentGeneration else { return } + self.updateHighlight() + } + debounceWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + Self.debounceInterval, execute: workItem) + } + + private func updateHighlight() { + guard let controller, let textView = controller.textView else { + clearHighlight() + return + } + + let docLength = (textView.string as NSString).length + + // Skip for huge documents + guard docLength < Self.maxDocumentLength else { + clearHighlight() + return + } + + // Skip for multi-cursor (ambiguous which statement) + guard controller.cursorPositions.count == 1, + let cursor = controller.cursorPositions.first else { + clearHighlight() + return + } + + let cursorPos = cursor.range.location + + // Skip if single statement (no semicolons — highlighting everything is meaningless) + let nsString = textView.string as NSString + guard nsString.range(of: ";").location != NSNotFound else { + clearHighlight() + return + } + + let located = SQLStatementScanner.locatedStatementAtCursor( + in: textView.string, + cursorPosition: cursorPos + ) + + let stmtNS = located.sql as NSString + let stmtRange = NSRange(location: located.offset, length: stmtNS.length) + + guard stmtRange.length > 0 else { + clearHighlight() + return + } + + // Skip if same range as last time + if stmtRange == lastHighlightedRange { return } + lastHighlightedRange = stmtRange + + let color = ThemeEngine.shared.colors.editor.currentStatementHighlight + let emphasis = Emphasis( + range: stmtRange, + style: .outline(color: color, fill: true) + ) + textView.emphasisManager?.removeEmphases(for: Self.groupId) + textView.emphasisManager?.addEmphases([emphasis], for: Self.groupId) + } + + private func clearHighlight() { + lastHighlightedRange = nil + controller?.textView.emphasisManager?.removeEmphases(for: Self.groupId) + } +} diff --git a/TablePro/Views/Editor/SQLEditorCoordinator.swift b/TablePro/Views/Editor/SQLEditorCoordinator.swift index e19cfdb6..ffa84513 100644 --- a/TablePro/Views/Editor/SQLEditorCoordinator.swift +++ b/TablePro/Views/Editor/SQLEditorCoordinator.swift @@ -29,6 +29,7 @@ final class SQLEditorCoordinator: TextViewCoordinator { /// Debounce work item for frame-change notification to avoid /// triggering syntax highlight viewport recalculation on every keystroke. @ObservationIgnored private var frameChangeWorkItem: DispatchWorkItem? + @ObservationIgnored private var statementHighlighter: CurrentStatementHighlighter? @ObservationIgnored private var wasEditorFocused = false @ObservationIgnored private var didDestroy = false @@ -86,6 +87,8 @@ final class SQLEditorCoordinator: TextViewCoordinator { self.applyHorizontalScrollFix(controller: controller) self.installAIContextMenu(controller: controller) self.installInlineSuggestionManager(controller: controller) + self.statementHighlighter = CurrentStatementHighlighter() + self.statementHighlighter?.install(controller: controller) self.installVimModeIfEnabled(controller: controller) if let textView = controller.textView { EditorEventRouter.shared.register(self, textView: textView) @@ -101,6 +104,7 @@ final class SQLEditorCoordinator: TextViewCoordinator { DispatchQueue.main.async { [weak self] in self?.inlineSuggestionManager?.handleTextChange() self?.vimCursorManager?.updatePosition() + self?.statementHighlighter?.handleTextChange() } // Throttle frame-change notification — during rapid typing, only the @@ -122,6 +126,7 @@ final class SQLEditorCoordinator: TextViewCoordinator { } func textViewDidChangeSelection(controller: TextViewController, newPositions: [CursorPosition]) { + statementHighlighter?.handleCursorChange() inlineSuggestionManager?.handleSelectionChange() vimCursorManager?.updatePosition() @@ -143,6 +148,9 @@ final class SQLEditorCoordinator: TextViewCoordinator { uninstallVimKeyInterceptor() + statementHighlighter?.uninstall() + statementHighlighter = nil + inlineSuggestionManager?.uninstall() inlineSuggestionManager = nil From 477b6bfb578c1bd81cc5e474bbb4ffe0bf5b7b08 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 13:29:27 +0700 Subject: [PATCH 2/9] test: add tests for statement highlighting, theme colors, and located scanner - CurrentStatementHighlighterTests: 9 tests for statement detection logic (multi-statement, single statement, strings, comments, edge cases) - ThemeDefinitionTests: 7 tests for currentStatementHighlight color (defaults, round-trip, backward compatibility) - SQLStatementScannerLocatedTests: 13 tests for locatedStatementAtCursor (offsets, comments, backticks, large input, edge cases) - Fix pre-existing TabDiskActorTests compile error (save() now throws) --- .../Core/Storage/TabDiskActorTests.swift | 54 +++--- .../SQLStatementScannerLocatedTests.swift | 182 ++++++++++++++++++ .../Theme/ThemeDefinitionTests.swift | 137 +++++++++++++ .../CurrentStatementHighlighterTests.swift | 111 +++++++++++ 4 files changed, 457 insertions(+), 27 deletions(-) create mode 100644 TableProTests/Core/Utilities/SQLStatementScannerLocatedTests.swift create mode 100644 TableProTests/Theme/ThemeDefinitionTests.swift create mode 100644 TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift diff --git a/TableProTests/Core/Storage/TabDiskActorTests.swift b/TableProTests/Core/Storage/TabDiskActorTests.swift index 19f714f2..b36a14b0 100644 --- a/TableProTests/Core/Storage/TabDiskActorTests.swift +++ b/TableProTests/Core/Storage/TabDiskActorTests.swift @@ -36,12 +36,12 @@ struct TabDiskActorTests { // MARK: - save / load round-trip @Test("Save then load round-trips correctly") - func saveAndLoadRoundTrip() async { + func saveAndLoadRoundTrip() async throws { let connectionId = UUID() let tabId = UUID() let tab = makeTab(id: tabId, title: "My Tab", query: "SELECT * FROM users") - await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId) + try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId) let state = await actor.load(connectionId: connectionId) #expect(state != nil) @@ -58,7 +58,7 @@ struct TabDiskActorTests { // MARK: - load returns nil for unknown connectionId @Test("Load returns nil for unknown connectionId") - func loadReturnsNilForUnknown() async { + func loadReturnsNilForUnknown() async throws { let result = await actor.load(connectionId: UUID()) #expect(result == nil) } @@ -66,13 +66,13 @@ struct TabDiskActorTests { // MARK: - save overwrites previous state @Test("Save overwrites previous state") - func saveOverwritesPreviousState() async { + func saveOverwritesPreviousState() async throws { let connectionId = UUID() let tab1 = makeTab(title: "First") let tab2 = makeTab(title: "Second") - await actor.save(connectionId: connectionId, tabs: [tab1], selectedTabId: tab1.id) - await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: tab2.id) + try await actor.save(connectionId: connectionId, tabs: [tab1], selectedTabId: tab1.id) + try await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: tab2.id) let state = await actor.load(connectionId: connectionId) @@ -86,11 +86,11 @@ struct TabDiskActorTests { // MARK: - clear removes saved state @Test("Clear removes saved state") - func clearRemovesSavedState() async { + func clearRemovesSavedState() async throws { let connectionId = UUID() let tab = makeTab() - await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tab.id) + try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tab.id) await actor.clear(connectionId: connectionId) let state = await actor.load(connectionId: connectionId) @@ -100,21 +100,21 @@ struct TabDiskActorTests { // MARK: - clear on non-existent connectionId does not crash @Test("Clear on non-existent connectionId does not crash") - func clearNonExistentDoesNotCrash() async { + func clearNonExistentDoesNotCrash() async throws { await actor.clear(connectionId: UUID()) } // MARK: - Multiple connections are independent @Test("Multiple connections are independent") - func multipleConnectionsAreIndependent() async { + func multipleConnectionsAreIndependent() async throws { let connA = UUID() let connB = UUID() let tabA = makeTab(title: "Tab A") let tabB = makeTab(title: "Tab B") - await actor.save(connectionId: connA, tabs: [tabA], selectedTabId: tabA.id) - await actor.save(connectionId: connB, tabs: [tabB], selectedTabId: tabB.id) + try await actor.save(connectionId: connA, tabs: [tabA], selectedTabId: tabA.id) + try await actor.save(connectionId: connB, tabs: [tabB], selectedTabId: tabB.id) let stateA = await actor.load(connectionId: connA) let stateB = await actor.load(connectionId: connB) @@ -135,18 +135,18 @@ struct TabDiskActorTests { // MARK: - selectedTabId preservation @Test("selectedTabId is preserved correctly including nil") - func selectedTabIdPreserved() async { + func selectedTabIdPreserved() async throws { let connectionId = UUID() let tab = makeTab() - await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: nil) + try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: nil) let stateNil = await actor.load(connectionId: connectionId) #expect(stateNil?.selectedTabId == nil) #expect(stateNil?.tabs.count == 1) let specificId = UUID() let tab2 = makeTab(id: specificId) - await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: specificId) + try await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: specificId) let stateWithId = await actor.load(connectionId: connectionId) #expect(stateWithId?.selectedTabId == specificId) @@ -156,7 +156,7 @@ struct TabDiskActorTests { // MARK: - saveLastQuery / loadLastQuery round-trip @Test("saveLastQuery then loadLastQuery round-trips") - func lastQueryRoundTrip() async { + func lastQueryRoundTrip() async throws { let connectionId = UUID() let query = "SELECT * FROM products WHERE active = true" @@ -171,7 +171,7 @@ struct TabDiskActorTests { // MARK: - loadLastQuery returns nil for unknown connectionId @Test("loadLastQuery returns nil for unknown connectionId") - func loadLastQueryReturnsNilForUnknown() async { + func loadLastQueryReturnsNilForUnknown() async throws { let result = await actor.loadLastQuery(for: UUID()) #expect(result == nil) } @@ -179,7 +179,7 @@ struct TabDiskActorTests { // MARK: - saveLastQuery with empty string removes the file @Test("saveLastQuery with empty string removes the file") - func saveLastQueryEmptyRemovesFile() async { + func saveLastQueryEmptyRemovesFile() async throws { let connectionId = UUID() await actor.saveLastQuery("SELECT 1", for: connectionId) @@ -193,7 +193,7 @@ struct TabDiskActorTests { // MARK: - saveLastQuery with whitespace-only string removes the file @Test("saveLastQuery with whitespace-only string removes the file") - func saveLastQueryWhitespaceOnlyRemovesFile() async { + func saveLastQueryWhitespaceOnlyRemovesFile() async throws { let connectionId = UUID() await actor.saveLastQuery("SELECT 1", for: connectionId) @@ -206,7 +206,7 @@ struct TabDiskActorTests { // MARK: - saveLastQuery skips queries exceeding 500KB @Test("saveLastQuery skips queries exceeding 500KB") - func saveLastQuerySkipsLargeQueries() async { + func saveLastQuerySkipsLargeQueries() async throws { let connectionId = UUID() let smallQuery = "SELECT 1" @@ -225,7 +225,7 @@ struct TabDiskActorTests { // MARK: - Tab with all fields round-trips @Test("Tab with all fields including isView and databaseName round-trips") - func tabWithAllFieldsRoundTrips() async { + func tabWithAllFieldsRoundTrips() async throws { let connectionId = UUID() let tabId = UUID() let tab = makeTab( @@ -238,7 +238,7 @@ struct TabDiskActorTests { databaseName: "production" ) - await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId) + try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId) let state = await actor.load(connectionId: connectionId) #expect(state != nil) @@ -257,13 +257,13 @@ struct TabDiskActorTests { // MARK: - Multiple tabs in single save @Test("Multiple tabs in a single save round-trip correctly") - func multipleTabsRoundTrip() async { + func multipleTabsRoundTrip() async throws { let connectionId = UUID() let tab1 = makeTab(title: "Tab 1", tabType: .query) let tab2 = makeTab(title: "Tab 2", tabType: .table, tableName: "orders") let tab3 = makeTab(title: "Tab 3", tabType: .query) - await actor.save(connectionId: connectionId, tabs: [tab1, tab2, tab3], selectedTabId: tab2.id) + try await actor.save(connectionId: connectionId, tabs: [tab1, tab2, tab3], selectedTabId: tab2.id) let state = await actor.load(connectionId: connectionId) #expect(state?.tabs.count == 3) @@ -278,7 +278,7 @@ struct TabDiskActorTests { // MARK: - saveSync writes data readable by load @Test("saveSync writes data that load can read back") - func saveSyncWritesReadableData() async { + func saveSyncWritesReadableData() async throws { let connectionId = UUID() let tabId = UUID() let tab = makeTab(id: tabId, title: "Sync Tab", query: "SELECT 42", tabType: .table, tableName: "orders") @@ -301,10 +301,10 @@ struct TabDiskActorTests { // MARK: - Empty tabs array @Test("Saving empty tabs array round-trips") - func emptyTabsArrayRoundTrips() async { + func emptyTabsArrayRoundTrips() async throws { let connectionId = UUID() - await actor.save(connectionId: connectionId, tabs: [], selectedTabId: nil) + try await actor.save(connectionId: connectionId, tabs: [], selectedTabId: nil) let state = await actor.load(connectionId: connectionId) #expect(state != nil) diff --git a/TableProTests/Core/Utilities/SQLStatementScannerLocatedTests.swift b/TableProTests/Core/Utilities/SQLStatementScannerLocatedTests.swift new file mode 100644 index 00000000..aceed913 --- /dev/null +++ b/TableProTests/Core/Utilities/SQLStatementScannerLocatedTests.swift @@ -0,0 +1,182 @@ +// +// SQLStatementScannerLocatedTests.swift +// TableProTests +// +// Focused tests on locatedStatementAtCursor, the key function +// powering the current statement highlighter. +// + +import Foundation +import Testing +@testable import TablePro + +@Suite("SQL Statement Scanner — locatedStatementAtCursor") +struct SQLStatementScannerLocatedTests { + + // MARK: - Offset correctness + + @Test("Returns correct offset for each statement in multi-statement string") + func correctOffsetsForMultipleStatements() { + let sql = "SELECT 1; UPDATE t SET x=1; DELETE FROM t" + // 0123456789... + + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(first.offset == 0) + #expect(first.sql == "SELECT 1;") + + let second = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 12) + #expect(second.offset == 9) + #expect(second.sql == " UPDATE t SET x=1;") + + let third = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 30) + #expect(third.offset == 27) + #expect(third.sql == " DELETE FROM t") + } + + @Test("offset + sql.count covers the full statement range") + func offsetPlusSqlLengthCoversRange() { + let sql = "INSERT INTO t VALUES(1); SELECT * FROM t; DROP TABLE t" + + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) + let firstEnd = first.offset + (first.sql as NSString).length + #expect(firstEnd == 24) // "INSERT INTO t VALUES(1);" is 24 chars + + let second = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 30) + let secondEnd = second.offset + (second.sql as NSString).length + #expect(secondEnd == 41) // up to and including the second semicolon + + let third = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 50) + let thirdEnd = third.offset + (third.sql as NSString).length + #expect(thirdEnd == (sql as NSString).length) + } + + // MARK: - Leading whitespace handling + + @Test("Offset accounts for leading whitespace between statements") + func leadingWhitespaceIncludedInOffset() { + let sql = "SELECT 1; SELECT 2" + // ^ offset 9, then " SELECT 2" starts at 9 + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 15) + #expect(located.offset == 9) + // The raw SQL includes the leading spaces + #expect(located.sql.hasPrefix(" ")) + } + + @Test("Offset accounts for newlines between statements") + func newlinesBetweenStatements() { + let sql = "SELECT 1;\n\nSELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 15) + #expect(located.offset == 9) + #expect(located.sql == "\n\nSELECT 2") + } + + // MARK: - Trailing whitespace handling + + @Test("Trailing whitespace before semicolon is included in statement") + func trailingWhitespaceBeforeSemicolon() { + let sql = "SELECT 1 ; SELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) + #expect(located.sql == "SELECT 1 ;") + } + + // MARK: - Comment styles + + @Test("Works with line comments containing semicolons") + func lineCommentWithSemicolon() { + let sql = "SELECT 1 -- drop; table\n; SELECT 2" + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + // The semicolon in the comment is not a delimiter + #expect(first.sql == "SELECT 1 -- drop; table\n;") + #expect(first.offset == 0) + } + + @Test("Works with block comments containing semicolons") + func blockCommentWithSemicolon() { + let sql = "SELECT /* ; */ 1; SELECT 2" + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(first.sql == "SELECT /* ; */ 1;") + #expect(first.offset == 0) + } + + @Test("Works with mixed comment styles") + func mixedComments() { + // Semicolons inside comments are ignored; real delimiter is at pos 31 + let sql = "SELECT 1 /* block; */ -- line;\n; SELECT 2" + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(first.offset == 0) + #expect(first.sql.contains("SELECT 1")) + + let second = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 38) + #expect(second.sql.contains("SELECT 2")) + } + + // MARK: - Backtick-quoted identifiers + + @Test("Backtick-quoted identifiers containing semicolons do not split") + func backtickWithSemicolon() { + let sql = "SELECT `col;name`; SELECT 2" + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) + #expect(first.sql == "SELECT `col;name`;") + #expect(first.offset == 0) + } + + // MARK: - Edge cases + + @Test("Cursor at exact semicolon position belongs to current statement") + func cursorAtSemicolon() { + let sql = "SELECT 1; SELECT 2" + // Position 8 is the semicolon character + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 8) + #expect(located.offset == 0) + #expect(located.sql == "SELECT 1;") + } + + @Test("Cursor beyond end of string is clamped") + func cursorBeyondEnd() { + let sql = "SELECT 1; SELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 9999) + #expect(located.offset == 9) + #expect(located.sql == " SELECT 2") + } + + @Test("Handles very large input without crashing") + func largeInput() { + var parts: [String] = [] + for i in 0..<200 { + parts.append("SELECT \(i) FROM very_long_table_name_for_testing;") + } + let sql = parts.joined(separator: " ") + let nsSQL = sql as NSString + #expect(nsSQL.length > 10_000) + + let midpoint = nsSQL.length / 2 + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: midpoint) + #expect(!located.sql.isEmpty) + #expect(located.offset >= 0) + #expect(located.offset < nsSQL.length) + } + + @Test("Multiple consecutive semicolons produce empty-ish segments") + func consecutiveSemicolons() { + let sql = "SELECT 1;;; SELECT 2" + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(first.sql == "SELECT 1;") + #expect(first.offset == 0) + } + + @Test("Escaped quote inside string does not break parsing") + func escapedQuoteInString() { + let sql = "SELECT 'it\\'s here'; SELECT 2" + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(first.sql == "SELECT 'it\\'s here';") + #expect(first.offset == 0) + } + + @Test("Doubled quote escape inside string does not break parsing") + func doubledQuoteInString() { + let sql = "SELECT 'it''s here'; SELECT 2" + let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(first.sql == "SELECT 'it''s here';") + #expect(first.offset == 0) + } +} diff --git a/TableProTests/Theme/ThemeDefinitionTests.swift b/TableProTests/Theme/ThemeDefinitionTests.swift new file mode 100644 index 00000000..a394d4bc --- /dev/null +++ b/TableProTests/Theme/ThemeDefinitionTests.swift @@ -0,0 +1,137 @@ +// +// ThemeDefinitionTests.swift +// TableProTests +// +// Tests for ThemeDefinition and EditorThemeColors, focusing on the +// currentStatementHighlight field and Codable backward compatibility. +// + +import Foundation +import Testing +@testable import TablePro + +@Suite("Theme Definition") +struct ThemeDefinitionTests { + + // MARK: - Default light theme + + @Test("Default light editor colors include currentStatementHighlight") + func defaultLightHasCurrentStatementHighlight() { + let colors = EditorThemeColors.defaultLight + #expect(colors.currentStatementHighlight == "#F0F4FA") + } + + @Test("Default light editor colors have expected background") + func defaultLightBackground() { + let colors = EditorThemeColors.defaultLight + #expect(colors.background == "#FFFFFF") + } + + // MARK: - Codable round-trip + + @Test("EditorThemeColors survives encode-decode round-trip") + func editorThemeColorsRoundTrip() throws { + let original = EditorThemeColors.defaultLight + let encoder = JSONEncoder() + let data = try encoder.encode(original) + let decoder = JSONDecoder() + let decoded = try decoder.decode(EditorThemeColors.self, from: data) + + #expect(decoded.currentStatementHighlight == original.currentStatementHighlight) + #expect(decoded.background == original.background) + #expect(decoded.text == original.text) + #expect(decoded.cursor == original.cursor) + #expect(decoded.currentLineHighlight == original.currentLineHighlight) + #expect(decoded.selection == original.selection) + #expect(decoded.lineNumber == original.lineNumber) + #expect(decoded.invisibles == original.invisibles) + #expect(decoded == original) + } + + @Test("Full ThemeDefinition survives encode-decode round-trip") + func themeDefinitionRoundTrip() throws { + let original = ThemeDefinition.default + let encoder = JSONEncoder() + let data = try encoder.encode(original) + let decoder = JSONDecoder() + let decoded = try decoder.decode(ThemeDefinition.self, from: data) + + #expect(decoded.editor.currentStatementHighlight == original.editor.currentStatementHighlight) + #expect(decoded == original) + } + + // MARK: - Backward compatibility + + @Test("Decoding JSON missing currentStatementHighlight falls back to default") + func backwardCompatibilityMissingField() throws { + // JSON with all editor fields EXCEPT currentStatementHighlight + let json = """ + { + "background": "#1E1E1E", + "text": "#D4D4D4", + "cursor": "#AEAFAD", + "currentLineHighlight": "#2A2D2E", + "selection": "#264F78", + "lineNumber": "#858585", + "invisibles": "#3B3B3B", + "syntax": { + "keyword": "#569CD6", + "string": "#CE9178", + "number": "#B5CEA8", + "comment": "#6A9955", + "null": "#569CD6", + "operator": "#D4D4D4", + "function": "#DCDCAA", + "type": "#4EC9B0" + } + } + """ + let data = Data(json.utf8) + let decoded = try JSONDecoder().decode(EditorThemeColors.self, from: data) + + // Should fall back to defaultLight's value + #expect(decoded.currentStatementHighlight == EditorThemeColors.defaultLight.currentStatementHighlight) + // Other fields should use the provided values + #expect(decoded.background == "#1E1E1E") + #expect(decoded.text == "#D4D4D4") + } + + @Test("Decoding empty JSON falls back to all defaults") + func emptyJsonFallsBackToDefaults() throws { + let json = "{}" + let data = Data(json.utf8) + let decoded = try JSONDecoder().decode(EditorThemeColors.self, from: data) + + #expect(decoded == EditorThemeColors.defaultLight) + } + + @Test("Decoding JSON with currentStatementHighlight preserves custom value") + func customCurrentStatementHighlight() throws { + let json = """ + { + "background": "#FFFFFF", + "text": "#000000", + "cursor": "#000000", + "currentLineHighlight": "#ECF5FF", + "selection": "#B4D8FD", + "lineNumber": "#747478", + "invisibles": "#D6D6D6", + "currentStatementHighlight": "#AABBCC", + "syntax": { + "keyword": "#9B2393", + "string": "#C41A16", + "number": "#1C00CF", + "comment": "#5D6C79", + "null": "#9B2393", + "operator": "#000000", + "function": "#326D74", + "type": "#3F6E74" + } + } + """ + let data = Data(json.utf8) + let decoded = try JSONDecoder().decode(EditorThemeColors.self, from: data) + + #expect(decoded.currentStatementHighlight == "#AABBCC") + } +} diff --git a/TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift b/TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift new file mode 100644 index 00000000..b245d108 --- /dev/null +++ b/TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift @@ -0,0 +1,111 @@ +// +// CurrentStatementHighlighterTests.swift +// TableProTests +// +// Tests for the current statement highlighting feature. +// Since CurrentStatementHighlighter depends on TextViewController (CESS), +// we test the core logic indirectly through SQLStatementScanner which +// powers the highlighting, plus verify threshold constants. +// + +import Foundation +import Testing +@testable import TablePro + +@Suite("Current Statement Highlighter Logic") +struct CurrentStatementHighlighterTests { + + // MARK: - Multi-statement cursor detection + + @Test("Cursor at beginning of first statement returns first statement") + func cursorAtBeginningOfFirstStatement() { + let sql = "SELECT 1; SELECT 2; SELECT 3" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(located.offset == 0) + #expect(located.sql == "SELECT 1;") + } + + @Test("Cursor in middle of second statement returns second statement") + func cursorInMiddleOfSecondStatement() { + let sql = "SELECT 1; SELECT 2; SELECT 3" + // Position 13 is inside " SELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 13) + #expect(located.offset == 9) + #expect(located.sql == " SELECT 2;") + } + + @Test("Cursor at end of last statement returns last statement") + func cursorAtEndOfLastStatement() { + let sql = "SELECT 1; SELECT 2; SELECT 3" + let located = SQLStatementScanner.locatedStatementAtCursor( + in: sql, + cursorPosition: (sql as NSString).length + ) + #expect(located.offset == 19) + #expect(located.sql == " SELECT 3") + } + + @Test("Cursor after final semicolon returns trailing content") + func cursorAfterFinalSemicolon() { + let sql = "SELECT 1; SELECT 2;\n" + let located = SQLStatementScanner.locatedStatementAtCursor( + in: sql, + cursorPosition: 20 + ) + // After the last semicolon, the remaining text is "\n" + #expect(located.offset == 19) + #expect(located.sql == "\n") + } + + @Test("Single statement without semicolon returns entire text via fast path") + func singleStatementNoSemicolon() { + let sql = "SELECT * FROM users" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) + // The scanner's fast path returns the whole string when no semicolons + #expect(located.sql == sql) + #expect(located.offset == 0) + } + + @Test("Empty string returns empty located statement") + func emptyString() { + let located = SQLStatementScanner.locatedStatementAtCursor(in: "", cursorPosition: 0) + #expect(located.sql == "") + #expect(located.offset == 0) + } + + // MARK: - Strings containing semicolons + + @Test("Semicolons inside single-quoted strings do not split statements") + func semicolonInSingleQuotedString() { + let sql = "SELECT 'a;b'; SELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) + #expect(located.sql == "SELECT 'a;b';") + #expect(located.offset == 0) + } + + @Test("Semicolons inside double-quoted strings do not split statements") + func semicolonInDoubleQuotedString() { + let sql = "SELECT \"a;b\"; SELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) + #expect(located.sql == "SELECT \"a;b\";") + #expect(located.offset == 0) + } + + // MARK: - Comments containing semicolons + + @Test("Semicolons inside line comments do not split statements") + func semicolonInLineComment() { + let sql = "SELECT 1 -- comment; not a split\n; SELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(located.sql == "SELECT 1 -- comment; not a split\n;") + #expect(located.offset == 0) + } + + @Test("Semicolons inside block comments do not split statements") + func semicolonInBlockComment() { + let sql = "SELECT 1 /* ; */ ; SELECT 2" + let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) + #expect(located.sql == "SELECT 1 /* ; */ ;") + #expect(located.offset == 0) + } +} From 7c8cb37637aace1241824229d1f7625abed2173a Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 13:35:17 +0700 Subject: [PATCH 3/9] fix: prevent crash from stale emphasis range during text edits - Clear emphasis immediately on text change (before debounced update) to avoid drawing a stale range that extends beyond the document - Validate NSMaxRange(stmtRange) <= docLength before adding emphasis --- TablePro/Views/Editor/CurrentStatementHighlighter.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/TablePro/Views/Editor/CurrentStatementHighlighter.swift b/TablePro/Views/Editor/CurrentStatementHighlighter.swift index ebb26ee4..b03b6c14 100644 --- a/TablePro/Views/Editor/CurrentStatementHighlighter.swift +++ b/TablePro/Views/Editor/CurrentStatementHighlighter.swift @@ -36,8 +36,9 @@ final class CurrentStatementHighlighter { } func handleTextChange() { - // Text changed — invalidate cached range and schedule update - lastHighlightedRange = nil + // Clear emphasis immediately so stale ranges don't cause drawing crashes. + // The debounced update will re-apply with the correct range. + clearHighlight() scheduleUpdate() } @@ -92,7 +93,8 @@ final class CurrentStatementHighlighter { let stmtNS = located.sql as NSString let stmtRange = NSRange(location: located.offset, length: stmtNS.length) - guard stmtRange.length > 0 else { + // Validate range is within document bounds + guard stmtRange.length > 0, NSMaxRange(stmtRange) <= docLength else { clearHighlight() return } From 3d58f6e7eaf9411bcb5d1f7123d847a68fdb2e42 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 13:41:15 +0700 Subject: [PATCH 4/9] fix: use NSTextStorage backgroundColor instead of EmphasisManager EmphasisManager creates a CATextLayer on top with black foreground color, which overwrites syntax highlighting and hides the cursor. Switch to NSTextStorage.addAttribute(.backgroundColor) which draws behind text, preserving syntax colors and cursor visibility. --- .../Editor/CurrentStatementHighlighter.swift | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/TablePro/Views/Editor/CurrentStatementHighlighter.swift b/TablePro/Views/Editor/CurrentStatementHighlighter.swift index b03b6c14..d3f9fbb0 100644 --- a/TablePro/Views/Editor/CurrentStatementHighlighter.swift +++ b/TablePro/Views/Editor/CurrentStatementHighlighter.swift @@ -3,6 +3,7 @@ // TablePro // // Highlights the background of the SQL statement under the cursor. +// Uses NSTextStorage backgroundColor attribute so syntax colors are preserved. // import AppKit @@ -11,7 +12,6 @@ import CodeEditTextView @MainActor final class CurrentStatementHighlighter { - private static let groupId = "tablepro.currentStatement" private static let debounceInterval: TimeInterval = 0.15 private static let maxDocumentLength = 5_000_000 @@ -36,8 +36,6 @@ final class CurrentStatementHighlighter { } func handleTextChange() { - // Clear emphasis immediately so stale ranges don't cause drawing crashes. - // The debounced update will re-apply with the correct range. clearHighlight() scheduleUpdate() } @@ -61,15 +59,17 @@ final class CurrentStatementHighlighter { return } + guard let storage = textView.textStorage else { + clearHighlight() + return + } let docLength = (textView.string as NSString).length - // Skip for huge documents - guard docLength < Self.maxDocumentLength else { + guard docLength > 0, docLength < Self.maxDocumentLength else { clearHighlight() return } - // Skip for multi-cursor (ambiguous which statement) guard controller.cursorPositions.count == 1, let cursor = controller.cursorPositions.first else { clearHighlight() @@ -78,7 +78,7 @@ final class CurrentStatementHighlighter { let cursorPos = cursor.range.location - // Skip if single statement (no semicolons — highlighting everything is meaningless) + // Skip if single statement (no semicolons) let nsString = textView.string as NSString guard nsString.range(of: ";").location != NSNotFound else { clearHighlight() @@ -93,27 +93,29 @@ final class CurrentStatementHighlighter { let stmtNS = located.sql as NSString let stmtRange = NSRange(location: located.offset, length: stmtNS.length) - // Validate range is within document bounds guard stmtRange.length > 0, NSMaxRange(stmtRange) <= docLength else { clearHighlight() return } - // Skip if same range as last time if stmtRange == lastHighlightedRange { return } + + // Remove old highlight, apply new one + if let old = lastHighlightedRange, NSMaxRange(old) <= storage.length { + storage.removeAttribute(.backgroundColor, range: old) + } lastHighlightedRange = stmtRange let color = ThemeEngine.shared.colors.editor.currentStatementHighlight - let emphasis = Emphasis( - range: stmtRange, - style: .outline(color: color, fill: true) - ) - textView.emphasisManager?.removeEmphases(for: Self.groupId) - textView.emphasisManager?.addEmphases([emphasis], for: Self.groupId) + storage.addAttribute(.backgroundColor, value: color, range: stmtRange) } private func clearHighlight() { + if let storage = controller?.textView.textStorage, + let old = lastHighlightedRange, + NSMaxRange(old) <= storage.length { + storage.removeAttribute(.backgroundColor, range: old) + } lastHighlightedRange = nil - controller?.textView.emphasisManager?.removeEmphases(for: Self.groupId) } } From 9cab74e09629bb402b80eef0f60462fb344da7c2 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 13:48:17 +0700 Subject: [PATCH 5/9] fix: use CALayer behind text for statement highlight instead of text storage attribute NSTextStorage .backgroundColor conflicts with the syntax highlighting pipeline (Highlighter.setAttributes overwrites it) and stacks visually with the editor's selectedLineBackgroundColor. Using a CALayer with zPosition: -1 draws behind text, preserving syntax colors, cursor, and current line highlight independently. --- .../Editor/CurrentStatementHighlighter.swift | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/TablePro/Views/Editor/CurrentStatementHighlighter.swift b/TablePro/Views/Editor/CurrentStatementHighlighter.swift index d3f9fbb0..306a1d6c 100644 --- a/TablePro/Views/Editor/CurrentStatementHighlighter.swift +++ b/TablePro/Views/Editor/CurrentStatementHighlighter.swift @@ -3,7 +3,7 @@ // TablePro // // Highlights the background of the SQL statement under the cursor. -// Uses NSTextStorage backgroundColor attribute so syntax colors are preserved. +// Uses a CALayer behind text so syntax colors and cursor are preserved. // import AppKit @@ -19,15 +19,19 @@ final class CurrentStatementHighlighter { private var debounceWorkItem: DispatchWorkItem? private var lastHighlightedRange: NSRange? private var generation: UInt64 = 0 + private let backgroundLayer = CALayer() func install(controller: TextViewController) { self.controller = controller + backgroundLayer.zPosition = -1 + controller.textView.layer?.addSublayer(backgroundLayer) } func uninstall() { debounceWorkItem?.cancel() debounceWorkItem = nil - clearHighlight() + backgroundLayer.removeFromSuperlayer() + lastHighlightedRange = nil controller = nil } @@ -59,10 +63,6 @@ final class CurrentStatementHighlighter { return } - guard let storage = textView.textStorage else { - clearHighlight() - return - } let docLength = (textView.string as NSString).length guard docLength > 0, docLength < Self.maxDocumentLength else { @@ -78,7 +78,6 @@ final class CurrentStatementHighlighter { let cursorPos = cursor.range.location - // Skip if single statement (no semicolons) let nsString = textView.string as NSString guard nsString.range(of: ";").location != NSNotFound else { clearHighlight() @@ -99,23 +98,33 @@ final class CurrentStatementHighlighter { } if stmtRange == lastHighlightedRange { return } + lastHighlightedRange = stmtRange - // Remove old highlight, apply new one - if let old = lastHighlightedRange, NSMaxRange(old) <= storage.length { - storage.removeAttribute(.backgroundColor, range: old) + // Get bounding rect from layout manager via the rounded path + guard let path = textView.layoutManager?.roundedPathForRange(stmtRange, cornerRadius: 0) else { + clearHighlight() + return } - lastHighlightedRange = stmtRange + let rect = path.bounds + CATransaction.begin() + CATransaction.setDisableActions(true) let color = ThemeEngine.shared.colors.editor.currentStatementHighlight - storage.addAttribute(.backgroundColor, value: color, range: stmtRange) + backgroundLayer.backgroundColor = color.cgColor + backgroundLayer.cornerRadius = 3 + // Extend to full width of the text view + backgroundLayer.frame = CGRect( + x: 0, + y: rect.origin.y, + width: textView.frame.width, + height: rect.height + ) + backgroundLayer.isHidden = false + CATransaction.commit() } private func clearHighlight() { - if let storage = controller?.textView.textStorage, - let old = lastHighlightedRange, - NSMaxRange(old) <= storage.length { - storage.removeAttribute(.backgroundColor, range: old) - } lastHighlightedRange = nil + backgroundLayer.isHidden = true } } From eb866a684c67e9c77d890a74d866d6dbe6000b2f Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 13:50:05 +0700 Subject: [PATCH 6/9] fix: suppress current-line highlight when statement highlight is active The editor's selectedLineBackgroundColor stacks with our statement background, creating a double-highlight on the cursor line. Save and clear the line highlight color when statement highlighting is active, restore it when inactive (single statement or cleared). --- .../Editor/CurrentStatementHighlighter.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/TablePro/Views/Editor/CurrentStatementHighlighter.swift b/TablePro/Views/Editor/CurrentStatementHighlighter.swift index 306a1d6c..b8ec2912 100644 --- a/TablePro/Views/Editor/CurrentStatementHighlighter.swift +++ b/TablePro/Views/Editor/CurrentStatementHighlighter.swift @@ -20,6 +20,9 @@ final class CurrentStatementHighlighter { private var lastHighlightedRange: NSRange? private var generation: UInt64 = 0 private let backgroundLayer = CALayer() + /// Saved current-line highlight color to restore when statement highlight is inactive. + private var savedLineHighlightColor: NSColor? + private var isLineHighlightSuppressed = false func install(controller: TextViewController) { self.controller = controller @@ -30,6 +33,7 @@ final class CurrentStatementHighlighter { func uninstall() { debounceWorkItem?.cancel() debounceWorkItem = nil + restoreLineHighlight() backgroundLayer.removeFromSuperlayer() lastHighlightedRange = nil controller = nil @@ -121,10 +125,29 @@ final class CurrentStatementHighlighter { ) backgroundLayer.isHidden = false CATransaction.commit() + + suppressLineHighlight() } private func clearHighlight() { lastHighlightedRange = nil backgroundLayer.isHidden = true + restoreLineHighlight() + } + + /// Hide the editor's built-in current-line highlight to avoid double-highlight. + private func suppressLineHighlight() { + guard !isLineHighlightSuppressed else { return } + let selMgr = controller?.textView.selectionManager + savedLineHighlightColor = selMgr?.selectedLineBackgroundColor + selMgr?.selectedLineBackgroundColor = .clear + isLineHighlightSuppressed = true + } + + /// Restore the editor's built-in current-line highlight. + private func restoreLineHighlight() { + guard isLineHighlightSuppressed else { return } + controller?.textView.selectionManager?.selectedLineBackgroundColor = savedLineHighlightColor ?? .clear + isLineHighlightSuppressed = false } } From 37ca68e39eab7c6add0d594d7dce083bcec156c3 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 20:41:59 +0700 Subject: [PATCH 7/9] revert: remove statement highlighting feature (keep large doc safety caps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The statement highlighting fought CodeEditSourceEditor's rendering pipeline at every turn — EmphasisManager overwrites text colors, NSTextStorage attributes conflict with the highlighter, CALayer stacks with current-line highlight. The feature needs native CESS support (TextSelectionManager background ranges) to work correctly. Kept: large document safety caps (5MB skip, 50KB throttle), theme color definition, scanner located tests, theme definition tests. --- .claude/worktrees/agent-a1a2c85e | 1 + .claude/worktrees/agent-a45fa581 | 1 + .claude/worktrees/agent-a47a2ae2 | 1 + .claude/worktrees/agent-a52204c8 | 1 + .claude/worktrees/agent-a9fb7d7e | 1 + .claude/worktrees/agent-ab285578 | 1 + .claude/worktrees/agent-afb913a6 | 1 + CHANGELOG.md | 2 +- Sequel-Ace | 1 + .../Editor/CurrentStatementHighlighter.swift | 153 --- .../Views/Editor/SQLEditorCoordinator.swift | 8 - .../CurrentStatementHighlighterTests.swift | 111 --- docs/development/sequel-ace-learnings.md | 874 ++++++++++++++++++ licenseapp | 1 + sequel-ace-vs-tablepro-analysis.md | 427 +++++++++ 15 files changed, 1311 insertions(+), 273 deletions(-) create mode 160000 .claude/worktrees/agent-a1a2c85e create mode 160000 .claude/worktrees/agent-a45fa581 create mode 160000 .claude/worktrees/agent-a47a2ae2 create mode 160000 .claude/worktrees/agent-a52204c8 create mode 160000 .claude/worktrees/agent-a9fb7d7e create mode 160000 .claude/worktrees/agent-ab285578 create mode 160000 .claude/worktrees/agent-afb913a6 create mode 160000 Sequel-Ace delete mode 100644 TablePro/Views/Editor/CurrentStatementHighlighter.swift delete mode 100644 TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift create mode 100644 docs/development/sequel-ace-learnings.md create mode 160000 licenseapp create mode 100644 sequel-ace-vs-tablepro-analysis.md diff --git a/.claude/worktrees/agent-a1a2c85e b/.claude/worktrees/agent-a1a2c85e new file mode 160000 index 00000000..48e706ed --- /dev/null +++ b/.claude/worktrees/agent-a1a2c85e @@ -0,0 +1 @@ +Subproject commit 48e706ed534dfa3d3784267ea94191da8c89cda6 diff --git a/.claude/worktrees/agent-a45fa581 b/.claude/worktrees/agent-a45fa581 new file mode 160000 index 00000000..ebcded6d --- /dev/null +++ b/.claude/worktrees/agent-a45fa581 @@ -0,0 +1 @@ +Subproject commit ebcded6d18ab66158eca4492cb5907bd88e6c4f8 diff --git a/.claude/worktrees/agent-a47a2ae2 b/.claude/worktrees/agent-a47a2ae2 new file mode 160000 index 00000000..6e018092 --- /dev/null +++ b/.claude/worktrees/agent-a47a2ae2 @@ -0,0 +1 @@ +Subproject commit 6e018092e187f6b59782e1f18874b673912c4ed2 diff --git a/.claude/worktrees/agent-a52204c8 b/.claude/worktrees/agent-a52204c8 new file mode 160000 index 00000000..6e018092 --- /dev/null +++ b/.claude/worktrees/agent-a52204c8 @@ -0,0 +1 @@ +Subproject commit 6e018092e187f6b59782e1f18874b673912c4ed2 diff --git a/.claude/worktrees/agent-a9fb7d7e b/.claude/worktrees/agent-a9fb7d7e new file mode 160000 index 00000000..6e018092 --- /dev/null +++ b/.claude/worktrees/agent-a9fb7d7e @@ -0,0 +1 @@ +Subproject commit 6e018092e187f6b59782e1f18874b673912c4ed2 diff --git a/.claude/worktrees/agent-ab285578 b/.claude/worktrees/agent-ab285578 new file mode 160000 index 00000000..cb9a2721 --- /dev/null +++ b/.claude/worktrees/agent-ab285578 @@ -0,0 +1 @@ +Subproject commit cb9a2721ff9f83a2af724d0fb009f98792770083 diff --git a/.claude/worktrees/agent-afb913a6 b/.claude/worktrees/agent-afb913a6 new file mode 160000 index 00000000..48e706ed --- /dev/null +++ b/.claude/worktrees/agent-afb913a6 @@ -0,0 +1 @@ +Subproject commit 48e706ed534dfa3d3784267ea94191da8c89cda6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a8bb12..a9040eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Current SQL statement background highlighting in query editor +- Large document safety caps for syntax highlighting (skip >5MB, throttle >50KB) ## [0.23.2] - 2026-03-24 main diff --git a/Sequel-Ace b/Sequel-Ace new file mode 160000 index 00000000..175ae467 --- /dev/null +++ b/Sequel-Ace @@ -0,0 +1 @@ +Subproject commit 175ae46749d5c1d72b094e3b4d085a9b0dae15e2 diff --git a/TablePro/Views/Editor/CurrentStatementHighlighter.swift b/TablePro/Views/Editor/CurrentStatementHighlighter.swift deleted file mode 100644 index b8ec2912..00000000 --- a/TablePro/Views/Editor/CurrentStatementHighlighter.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// CurrentStatementHighlighter.swift -// TablePro -// -// Highlights the background of the SQL statement under the cursor. -// Uses a CALayer behind text so syntax colors and cursor are preserved. -// - -import AppKit -import CodeEditSourceEditor -import CodeEditTextView - -@MainActor -final class CurrentStatementHighlighter { - private static let debounceInterval: TimeInterval = 0.15 - private static let maxDocumentLength = 5_000_000 - - private weak var controller: TextViewController? - private var debounceWorkItem: DispatchWorkItem? - private var lastHighlightedRange: NSRange? - private var generation: UInt64 = 0 - private let backgroundLayer = CALayer() - /// Saved current-line highlight color to restore when statement highlight is inactive. - private var savedLineHighlightColor: NSColor? - private var isLineHighlightSuppressed = false - - func install(controller: TextViewController) { - self.controller = controller - backgroundLayer.zPosition = -1 - controller.textView.layer?.addSublayer(backgroundLayer) - } - - func uninstall() { - debounceWorkItem?.cancel() - debounceWorkItem = nil - restoreLineHighlight() - backgroundLayer.removeFromSuperlayer() - lastHighlightedRange = nil - controller = nil - } - - func handleCursorChange() { - scheduleUpdate() - } - - func handleTextChange() { - clearHighlight() - scheduleUpdate() - } - - private func scheduleUpdate() { - debounceWorkItem?.cancel() - generation &+= 1 - let currentGeneration = generation - - let workItem = DispatchWorkItem { [weak self] in - guard let self, self.generation == currentGeneration else { return } - self.updateHighlight() - } - debounceWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + Self.debounceInterval, execute: workItem) - } - - private func updateHighlight() { - guard let controller, let textView = controller.textView else { - clearHighlight() - return - } - - let docLength = (textView.string as NSString).length - - guard docLength > 0, docLength < Self.maxDocumentLength else { - clearHighlight() - return - } - - guard controller.cursorPositions.count == 1, - let cursor = controller.cursorPositions.first else { - clearHighlight() - return - } - - let cursorPos = cursor.range.location - - let nsString = textView.string as NSString - guard nsString.range(of: ";").location != NSNotFound else { - clearHighlight() - return - } - - let located = SQLStatementScanner.locatedStatementAtCursor( - in: textView.string, - cursorPosition: cursorPos - ) - - let stmtNS = located.sql as NSString - let stmtRange = NSRange(location: located.offset, length: stmtNS.length) - - guard stmtRange.length > 0, NSMaxRange(stmtRange) <= docLength else { - clearHighlight() - return - } - - if stmtRange == lastHighlightedRange { return } - lastHighlightedRange = stmtRange - - // Get bounding rect from layout manager via the rounded path - guard let path = textView.layoutManager?.roundedPathForRange(stmtRange, cornerRadius: 0) else { - clearHighlight() - return - } - let rect = path.bounds - - CATransaction.begin() - CATransaction.setDisableActions(true) - let color = ThemeEngine.shared.colors.editor.currentStatementHighlight - backgroundLayer.backgroundColor = color.cgColor - backgroundLayer.cornerRadius = 3 - // Extend to full width of the text view - backgroundLayer.frame = CGRect( - x: 0, - y: rect.origin.y, - width: textView.frame.width, - height: rect.height - ) - backgroundLayer.isHidden = false - CATransaction.commit() - - suppressLineHighlight() - } - - private func clearHighlight() { - lastHighlightedRange = nil - backgroundLayer.isHidden = true - restoreLineHighlight() - } - - /// Hide the editor's built-in current-line highlight to avoid double-highlight. - private func suppressLineHighlight() { - guard !isLineHighlightSuppressed else { return } - let selMgr = controller?.textView.selectionManager - savedLineHighlightColor = selMgr?.selectedLineBackgroundColor - selMgr?.selectedLineBackgroundColor = .clear - isLineHighlightSuppressed = true - } - - /// Restore the editor's built-in current-line highlight. - private func restoreLineHighlight() { - guard isLineHighlightSuppressed else { return } - controller?.textView.selectionManager?.selectedLineBackgroundColor = savedLineHighlightColor ?? .clear - isLineHighlightSuppressed = false - } -} diff --git a/TablePro/Views/Editor/SQLEditorCoordinator.swift b/TablePro/Views/Editor/SQLEditorCoordinator.swift index ffa84513..e19cfdb6 100644 --- a/TablePro/Views/Editor/SQLEditorCoordinator.swift +++ b/TablePro/Views/Editor/SQLEditorCoordinator.swift @@ -29,7 +29,6 @@ final class SQLEditorCoordinator: TextViewCoordinator { /// Debounce work item for frame-change notification to avoid /// triggering syntax highlight viewport recalculation on every keystroke. @ObservationIgnored private var frameChangeWorkItem: DispatchWorkItem? - @ObservationIgnored private var statementHighlighter: CurrentStatementHighlighter? @ObservationIgnored private var wasEditorFocused = false @ObservationIgnored private var didDestroy = false @@ -87,8 +86,6 @@ final class SQLEditorCoordinator: TextViewCoordinator { self.applyHorizontalScrollFix(controller: controller) self.installAIContextMenu(controller: controller) self.installInlineSuggestionManager(controller: controller) - self.statementHighlighter = CurrentStatementHighlighter() - self.statementHighlighter?.install(controller: controller) self.installVimModeIfEnabled(controller: controller) if let textView = controller.textView { EditorEventRouter.shared.register(self, textView: textView) @@ -104,7 +101,6 @@ final class SQLEditorCoordinator: TextViewCoordinator { DispatchQueue.main.async { [weak self] in self?.inlineSuggestionManager?.handleTextChange() self?.vimCursorManager?.updatePosition() - self?.statementHighlighter?.handleTextChange() } // Throttle frame-change notification — during rapid typing, only the @@ -126,7 +122,6 @@ final class SQLEditorCoordinator: TextViewCoordinator { } func textViewDidChangeSelection(controller: TextViewController, newPositions: [CursorPosition]) { - statementHighlighter?.handleCursorChange() inlineSuggestionManager?.handleSelectionChange() vimCursorManager?.updatePosition() @@ -148,9 +143,6 @@ final class SQLEditorCoordinator: TextViewCoordinator { uninstallVimKeyInterceptor() - statementHighlighter?.uninstall() - statementHighlighter = nil - inlineSuggestionManager?.uninstall() inlineSuggestionManager = nil diff --git a/TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift b/TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift deleted file mode 100644 index b245d108..00000000 --- a/TableProTests/Views/Editor/CurrentStatementHighlighterTests.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// CurrentStatementHighlighterTests.swift -// TableProTests -// -// Tests for the current statement highlighting feature. -// Since CurrentStatementHighlighter depends on TextViewController (CESS), -// we test the core logic indirectly through SQLStatementScanner which -// powers the highlighting, plus verify threshold constants. -// - -import Foundation -import Testing -@testable import TablePro - -@Suite("Current Statement Highlighter Logic") -struct CurrentStatementHighlighterTests { - - // MARK: - Multi-statement cursor detection - - @Test("Cursor at beginning of first statement returns first statement") - func cursorAtBeginningOfFirstStatement() { - let sql = "SELECT 1; SELECT 2; SELECT 3" - let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) - #expect(located.offset == 0) - #expect(located.sql == "SELECT 1;") - } - - @Test("Cursor in middle of second statement returns second statement") - func cursorInMiddleOfSecondStatement() { - let sql = "SELECT 1; SELECT 2; SELECT 3" - // Position 13 is inside " SELECT 2" - let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 13) - #expect(located.offset == 9) - #expect(located.sql == " SELECT 2;") - } - - @Test("Cursor at end of last statement returns last statement") - func cursorAtEndOfLastStatement() { - let sql = "SELECT 1; SELECT 2; SELECT 3" - let located = SQLStatementScanner.locatedStatementAtCursor( - in: sql, - cursorPosition: (sql as NSString).length - ) - #expect(located.offset == 19) - #expect(located.sql == " SELECT 3") - } - - @Test("Cursor after final semicolon returns trailing content") - func cursorAfterFinalSemicolon() { - let sql = "SELECT 1; SELECT 2;\n" - let located = SQLStatementScanner.locatedStatementAtCursor( - in: sql, - cursorPosition: 20 - ) - // After the last semicolon, the remaining text is "\n" - #expect(located.offset == 19) - #expect(located.sql == "\n") - } - - @Test("Single statement without semicolon returns entire text via fast path") - func singleStatementNoSemicolon() { - let sql = "SELECT * FROM users" - let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) - // The scanner's fast path returns the whole string when no semicolons - #expect(located.sql == sql) - #expect(located.offset == 0) - } - - @Test("Empty string returns empty located statement") - func emptyString() { - let located = SQLStatementScanner.locatedStatementAtCursor(in: "", cursorPosition: 0) - #expect(located.sql == "") - #expect(located.offset == 0) - } - - // MARK: - Strings containing semicolons - - @Test("Semicolons inside single-quoted strings do not split statements") - func semicolonInSingleQuotedString() { - let sql = "SELECT 'a;b'; SELECT 2" - let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) - #expect(located.sql == "SELECT 'a;b';") - #expect(located.offset == 0) - } - - @Test("Semicolons inside double-quoted strings do not split statements") - func semicolonInDoubleQuotedString() { - let sql = "SELECT \"a;b\"; SELECT 2" - let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5) - #expect(located.sql == "SELECT \"a;b\";") - #expect(located.offset == 0) - } - - // MARK: - Comments containing semicolons - - @Test("Semicolons inside line comments do not split statements") - func semicolonInLineComment() { - let sql = "SELECT 1 -- comment; not a split\n; SELECT 2" - let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) - #expect(located.sql == "SELECT 1 -- comment; not a split\n;") - #expect(located.offset == 0) - } - - @Test("Semicolons inside block comments do not split statements") - func semicolonInBlockComment() { - let sql = "SELECT 1 /* ; */ ; SELECT 2" - let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0) - #expect(located.sql == "SELECT 1 /* ; */ ;") - #expect(located.offset == 0) - } -} diff --git a/docs/development/sequel-ace-learnings.md b/docs/development/sequel-ace-learnings.md new file mode 100644 index 00000000..3f696c2c --- /dev/null +++ b/docs/development/sequel-ace-learnings.md @@ -0,0 +1,874 @@ +# What TablePro Can Learn from Sequel-Ace + +> Analysis date: 2026-03-22 | Source: Deep codebase analysis of Sequel-Ace v5.2.0 + +This document captures actionable features, patterns, and architectural ideas from Sequel-Ace that could improve TablePro. Each item includes implementation notes, effort estimates, and priority ranking. + +--- + +## Table of Contents + +1. [Advanced Field Editor](#1-advanced-field-editor) +2. [Template-Driven Filter Operator System](#2-template-driven-filter-operator-system) +3. [CSV Import with Field Mapping](#3-csv-import-with-field-mapping) +4. [Database Admin Tools](#4-database-admin-tools) +5. [Streaming Result & Lazy Conversion](#5-streaming-result--lazy-conversion) +6. [Export Pipeline Improvements](#6-export-pipeline-improvements) +7. [Script/Command Extensibility](#7-scriptcommand-extensibility) +8. [Geometry Data Visualization](#8-geometry-data-visualization) +9. [SSH Tunnel Improvements](#9-ssh-tunnel-improvements) +10. [Design Patterns Worth Adopting](#10-design-patterns-worth-adopting) +11. [Priority Matrix](#11-priority-matrix) +12. [Implementation Tracking](#12-implementation-tracking) + +--- + +## 1. Advanced Field Editor + +**Status**: Not started +**Effort**: Medium | **Impact**: High +**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPFieldEditorController.h/m` + +### Current State (TablePro) + +TablePro has basic cell editing — inline text editing in the data grid. No dedicated modal editor for complex data types. + +### What Sequel-Ace Does + +A 5-mode tabbed field editor sheet: + +#### 1.1 Hex Editor + +- Formatted hex dump: `ADDRESS HEX_BYTES ASCII_REPRESENTATION` (16 bytes/line) +- Non-printable bytes shown as `.` +- Bidirectional: binary data → hex display, hex input → binary data +- Supports MySQL `X'...'` syntax, `0x...` prefix, plain hex +- Lazy-loaded on demand (only renders when user clicks hex tab) + +**Sequel-Ace reference**: `Source/Other/CategoryAdditions/SPDataAdditions.m` lines 331-544 + +**Implementation notes for TablePro**: +- SwiftUI view with `Canvas` or monospaced `Text` grid +- `Data` extension for hex formatting/parsing +- Editable hex input field with validation +- Could use existing `NSViewRepresentable` pattern for performance on large BLOBs + +#### 1.2 Bit Field Editor + +- 64 individual toggle buttons (one per bit) +- Synchronized representations: decimal, hexadecimal, octal text fields +- Bit operations: Set All, Clear All, Negate, Shift Left/Right, Rotate Left/Right +- NULL support with dedicated toggle + +**Implementation notes for TablePro**: +- SwiftUI `Grid` layout with `Toggle` buttons (no need for 64 IBOutlets) +- `@State var bits: UInt64` as single source of truth +- Computed properties for decimal/hex/octal display +- Toolbar with operation buttons + +#### 1.3 JSON Formatter + +- Custom tokenizer preserving float precision and key ordering +- `NSJSONSerialization` rounds floats and reorders keys — Sequel-Ace avoids this +- Configurable indentation (tabs or 1-32 spaces) +- Format/unformat toggle (pretty-print ↔ compact) + +**Sequel-Ace reference**: `Source/Other/Parsing/SPJSONFormatter.h/m` + +**Implementation notes for TablePro**: +- Could use tree-sitter JSON grammar (already have tree-sitter via CESS) +- Or a custom Swift tokenizer that preserves numeric precision +- Integrate with existing CodeEditSourceEditor for syntax highlighting + +#### 1.4 Image/BLOB Preview with QuickLook + +- Detects file type from BLOB data (images, PDFs, audio, video, Word docs) +- Temp file creation + `QLPreviewPanel` for instant preview +- Drag-and-drop image import +- Paste from clipboard +- User-extensible type list via preferences (`EditorQuickLookTypes.plist`) + +**Sequel-Ace reference**: `Resources/EditorQuickLookTypes.plist` + +**Built-in preview types**: +| Type | Extension | +|------|-----------| +| Image | icns | +| Sound | m4a, mp3, wav | +| Movie | mov | +| PDF | pdf | +| HTML | html | +| Word | doc, docx | +| RTF | rtf | + +**Implementation notes for TablePro**: +- Use `QLPreviewPanel` (AppKit) or `QuickLookPreview` (SwiftUI, macOS 13+) +- File type detection via `UTType` from first N bytes (magic bytes) +- Temp file in `NSTemporaryDirectory()` with alternating names to avoid cache + +#### 1.5 Geometry Visualization + +See [Section 8](#8-geometry-data-visualization) for dedicated coverage. + +### Proposed TablePro Architecture + +``` +FieldEditorView (SwiftUI Sheet) +├── Picker: [Text, Hex, Image, JSON, Bit] +├── TextEditorTab +│ └── CodeEditSourceEditor (reuse existing) +├── HexEditorTab +│ └── HexDumpView (Canvas-based, monospaced) +├── ImagePreviewTab +│ └── QLPreviewPanel / QuickLookPreview +├── JSONEditorTab +│ └── CodeEditSourceEditor with JSON grammar +└── BitEditorTab + └── BitFieldGrid (SwiftUI Grid + Toggle) +``` + +**Data flow**: Cell double-click → detect type → open appropriate tab → edit → validate → return to DataChangeManager. + +--- + +## 2. Template-Driven Filter Operator System + +**Status**: Not started +**Effort**: Medium | **Impact**: High +**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPRuleFilterController.h/m`, `Resources/Plists/ContentFilters.plist` + +### Current State (TablePro) + +TablePro has basic text-based filtering. No type-aware operators, no visual rule builder, no nested AND/OR logic. + +### What Sequel-Ace Does + +#### 2.1 Operator Definition via Plist/JSON + +Each filter operator is a template: + +```json +{ + "MenuLabel": "contains", + "NumberOfArguments": 1, + "Clause": "LIKE $BINARY '%${}%'", + "ConjunctionLabels": [], + "Tooltip": "Searches for values containing the given text" +} +``` + +**Template placeholders**: +- `${}` → user argument (escaped, quoted) +- `$CURRENT_FIELD` → backtick-quoted column name +- `$BINARY` → `BINARY` keyword (if case-sensitive) or empty string + +**Type-to-operator mapping**: + +| Column Type | Operators | +|------------|-----------| +| `string` | =, ≠, LIKE, NOT LIKE, contains, starts with, ends with, REGEXP, IN, BETWEEN, IS NULL, IS NOT NULL, is empty | +| `number` | =, ≠, >, <, ≥, ≤, IN, LIKE, BETWEEN, IS NULL, IS NOT NULL | +| `date` | =, ≠, is after, is before, ≥, ≤, BETWEEN, IS NULL, IS NOT NULL | +| `spatial` | MBRContains, MBRWithin, MBRDisjoint, MBREqual, MBRIntersects, MBROverlaps, MBRTouches, IS NULL, IS NOT NULL | + +#### 2.2 User-Extensible + +- Custom operators saved globally (UserDefaults) or per-document +- Layering: bundled defaults → global custom → document custom +- Users can add operators without code changes + +#### 2.3 Visual Rule Builder (NSRuleEditor) + +- Nested AND/OR groups with visual indentation +- Per-rule enable/disable checkbox (tri-state hierarchy) +- Column selector → type-aware operator dropdown → argument fields +- Flattening pass eliminates redundant nesting before SQL generation + +#### 2.4 Quick Filter Table + +- Grid: one column per DB column, type filter directly +- Auto-detects operators from input: `">= 100"` → `field >= '100'`, `"NULL"` → `IS NULL` +- Same-row = AND, multiple-rows = OR +- DISTINCT and NEGATE toggles +- Live search mode (filter on every keystroke) +- Configurable default operator per session + +#### 2.5 SQL Generation Pipeline + +``` +Rule Editor rows + → Serialize to intermediate dict (filterClass, column, operator, values) + → Flatten (merge redundant AND/OR groups) + → Recursive SQL generation with proper parenthesization + → SPTableFilterParser handles escaping and placeholder substitution +``` + +### Proposed TablePro Architecture + +``` +FilterOperators/ +├── FilterOperatorDefinition.swift // Codable struct matching template format +├── FilterOperatorRegistry.swift // Loads from JSON, keyed by DatabaseType + column type +├── Dialects/ +│ ├── mysql-filters.json +│ ├── postgresql-filters.json // ILIKE, ~, ~*, SIMILAR TO, etc. +│ ├── sqlite-filters.json // GLOB, LIKE (case-insensitive by default) +│ ├── mongodb-filters.json // $regex, $gt, $lt, $in, etc. +│ └── redis-filters.json // Pattern matching (MATCH) +├── FilterRuleBuilder.swift // Visual rule builder (SwiftUI) +├── FilterQuickGrid.swift // Column-per-column quick filter +├── FilterSQLGenerator.swift // Template → SQL with proper escaping +└── FilterPresetManager.swift // Save/load named filter presets +``` + +**Key design decisions**: +- JSON files per database dialect (not one giant plist) +- `DatabaseType` extension provides `filterDialect` property +- Plugin-extensible: plugins can bundle their own filter JSON +- Backward-compatible: existing text filter becomes a "quick filter" mode + +--- + +## 3. CSV Import with Field Mapping + +**Status**: Not started +**Effort**: Medium | **Impact**: High +**Sequel-Ace reference**: `Source/Controllers/DataImport/SPFieldMapperController.h/m`, `Source/Controllers/DataImport/SPDataImport.m` + +### Current State (TablePro) + +TablePro has CSV export (plugin) but no CSV import with field mapping UI. + +### What Sequel-Ace Does + +#### 3.1 Field Mapper UI + +Visual column mapping interface: + +| Source CSV Column | → | Target Table Column | Operator | +|---|---|---|---| +| `email_address` | → | `email` | Import | +| `full_name` | → | `name` | Import | +| (none) | → | `created_at` | Global Value: `NOW()` | +| `old_id` | → | `id` | Match (for UPDATE) | +| `legacy_field` | → | (skip) | Do Not Import | + +**Alignment modes**: +- **By Name**: auto-match source→target by column name similarity +- **By Index**: map column 1→1, 2→2, etc. +- **Custom**: manual drag-and-drop or dropdown selection + +#### 3.2 Import Methods + +| Method | SQL | Use Case | +|--------|-----|----------| +| INSERT | `INSERT INTO ... VALUES (...)` | New rows | +| REPLACE | `REPLACE INTO ... VALUES (...)` | Upsert by primary key | +| UPDATE | `UPDATE ... SET ... WHERE match_col = match_val` | Update existing rows | + +**Advanced options**: IGNORE, DELAYED, LOW_PRIORITY, HIGH_PRIORITY, ON DUPLICATE KEY UPDATE + +#### 3.3 Global Values + +- SQL expressions applied to all rows: `NOW()`, `CURDATE()`, `UUID()` +- Column references: `$1 + $2` (source column 1 + column 2) +- `$N` syntax triggers SQL mode (expression not quoted) + +#### 3.4 Data Type Handling + +- **Geometry fields**: WKT parsing for spatial types +- **Bit fields**: integer conversion +- **Nullable numerics**: NULL handling for empty strings +- **New table mode**: creates table from CSV structure with editable column types/names + +#### 3.5 Streaming Import + +- 1MB chunk-based file reading +- Background thread for processing +- `SPCSVParser` for row/cell parsing +- Progress bar shows bytes processed vs total +- Error handling: Ask/Ignore/Abort on per-row errors + +### Proposed TablePro Architecture + +``` +ImportService/ +├── CSVImportController.swift // Main import coordinator +├── FieldMapperView.swift // SwiftUI field mapping UI +├── FieldMapping.swift // Mapping model (source index → target column) +├── ImportMethod.swift // INSERT/REPLACE/UPDATE enum +├── GlobalValueExpression.swift // SQL expression for unmapped columns +├── CSVChunkReader.swift // Streaming 1MB chunk reader +└── ImportProgressTracker.swift // Progress reporting +``` + +**Plugin integration**: Import could be a new plugin capability — `PluginDatabaseDriver` gains an optional `importRows(_:into:mapping:method:)` method with default implementation generating standard SQL. + +--- + +## 4. Database Admin Tools + +**Status**: Not started +**Effort**: Low-Medium | **Impact**: Medium + +### 4.1 Process List Viewer + +**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPProcessListController.h/m` (801 lines) + +**Features**: +- `SHOW [FULL] PROCESSLIST` with configurable auto-refresh (1s/5s/10s/30s/custom) +- Kill Query / Kill Connection (TiDB-aware: `KILL TIDB QUERY id`) +- Real-time filtering across all columns (Id, User, Host, Db, Command, Time, State, Info) +- Save process list to file +- Toggle Process ID column, toggle full process list mode + +**Implementation notes for TablePro**: +- Simple SwiftUI `Table` + `Timer.publish` for auto-refresh +- Add to `PluginDatabaseDriver` protocol: `func getProcessList() async throws -> [[String: String]]` +- Database-specific: MySQL (`SHOW PROCESSLIST`), PostgreSQL (`pg_stat_activity`), Redis (`CLIENT LIST`) +- Could be a floating panel or a tab in the main view + +**Estimated effort**: 2-3 days (UI + protocol method + MySQL/PostgreSQL implementations) + +### 4.2 Server Variables Inspector + +**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPServerVariablesController.h/m` (~350 lines) + +**Features**: +- `SHOW VARIABLES` with real-time search +- Copy name/value/both +- Save as `.cnf` file +- Read-only display + +**Implementation notes for TablePro**: +- Even simpler than Process List — just a searchable table +- Add to protocol: `func getServerVariables() async throws -> [(name: String, value: String)]` +- Database-specific: MySQL (`SHOW VARIABLES`), PostgreSQL (`SHOW ALL`), Redis (`CONFIG GET *`) + +**Estimated effort**: 1-2 days + +### 4.3 User/Role Manager + +**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPUserManager.h/m` (>1000 lines) + +**Features**: +- Tree view: user → user@host children +- 4 tabs: General, Global Privileges, Resources, Schema Privileges +- Grant/Revoke SQL generation from checkbox matrix +- MySQL 5.7.6+ vs pre-5.7.6 password handling +- MariaDB-specific privilege mapping + +**Implementation notes for TablePro**: +- Complex, database-specific — lower priority +- Could be generalized: MySQL users/grants, PostgreSQL roles/privileges, Redis ACLs +- Significant effort per database type +- Consider: is this better as a dedicated admin tool or part of a database client? + +**Estimated effort**: 1-2 weeks (per database type) + +### 4.4 Database Copy/Rename + +**Sequel-Ace reference**: `Source/Other/DatabaseActions/SPDatabaseCopy.h/m`, `SPTableCopy.h/m` + +**Features**: +- Clone database: `SHOW CREATE TABLE` + `INSERT INTO ... SELECT` for each table +- FK-aware: disables `foreign_key_checks` during copy +- Table move via `ALTER TABLE ... RENAME` +- Preserves encoding/collation + +**Implementation notes for TablePro**: +- Add to protocol: `func copyDatabase(from:to:withContent:)`, `func renameDatabase(from:to:)` +- MySQL: straightforward with SHOW CREATE TABLE +- PostgreSQL: `CREATE DATABASE ... TEMPLATE source_db` (simpler) +- Could add to right-click context menu on database sidebar + +**Estimated effort**: 3-5 days + +--- + +## 5. Streaming Result & Lazy Conversion + +**Status**: Not started +**Effort**: High | **Impact**: Medium +**Sequel-Ace reference**: `Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.h/m`, `Source/Other/CategoryAdditions/SPDataStorage.h/m` + +### What Sequel-Ace Does + +#### 5.1 Three-Tier Streaming + +| Tier | Memory | Access | Use Case | +|------|--------|--------|----------| +| On-demand (`SPMySQLStreamingResult`) | O(1) | Sequential only | Simple queries | +| Buffered (`SPMySQLFastStreamingResult`) | O(k) | Sequential, freed after read | Large exports | +| Full cache (`SPMySQLStreamingResultStore`) | O(n) | Random O(1) | Data grid browsing | + +#### 5.2 Custom Malloc Zone + +```c +dataStorage = malloc_create_zone(64 * 1024, 0); // 64KB dedicated heap +// ... store all row data in this zone ... +malloc_destroy_zone(dataStorage); // Instant cleanup on reload +``` + +- All row data in isolated heap → zone-destroy on reload = instant cleanup (no per-object deallocation) +- Capacity doubling: 100 → 200 → 400 → ... rows + +#### 5.3 Variable-Size Row Metadata + +Dynamically chooses metadata width based on row data size: +- Row < 255 bytes → 1 byte per field offset (UCHAR) +- Row < 65535 bytes → 2 bytes per field offset (USHORT) +- Row ≥ 65535 bytes → 8 bytes per field offset (ULONG) + +Saves ~50% metadata overhead for typical small rows. + +#### 5.4 Lazy String Conversion + +``` +Raw MySQL row (char** pointers) + → stored as raw bytes + field lengths in result store + → NSString/NSData conversion ONLY when cell is accessed by UI +``` + +Avoids allocating String objects for cells never scrolled into view. + +#### 5.5 Sparse Edit Overlay (SPDataStorage) + +``` +cellDataAtRow:column: + if editedRows[rowIndex] exists: + return editedRows[rowIndex][columnIndex] // edited copy + else: + return streamingStore[rowIndex][columnIndex] // original data +``` + +- `NSPointerArray editedRows` — only stores rows that user has edited +- Copy-on-edit: first edit copies row from streaming store +- Unloaded columns return `SPNotLoaded` sentinel (for lazy BLOB loading) + +#### 5.6 Cached Method Pointers + +```c +static inline id SPMySQLResultStoreGetRow(SPMySQLStreamingResultStore* self, NSUInteger rowIndex) { + typedef id (*SPMSRSRowFetchMethodPtr)(...); + static SPMSRSRowFetchMethodPtr SPMSRSRowFetch; + if (!SPMSRSRowFetch) SPMSRSRowFetch = (SPMSRSRowFetchMethodPtr)[...]; + return SPMSRSRowFetch(...); +} +``` + +Bypasses Objective-C message dispatch in tight loops (data grid scrolling). + +### Applicability to TablePro + +TablePro's `RowBuffer` already handles some of this, but could benefit from: + +1. **Lazy string conversion** — store raw `Data` from database drivers, convert to `String` only when `DataGridView` requests the cell value. Biggest win for large result sets where user only scrolls through a fraction. + +2. **Malloc zone isolation** — for the RowBuffer backing store. `malloc_create_zone` + `malloc_destroy_zone` on tab switch/reload is faster than deallocating thousands of individual arrays. + +3. **Variable-width metadata** — if building a custom compact row format for RowBuffer. + +4. **Sparse edit tracking** — instead of copying entire row arrays on edit, only store the diff. TablePro's `DataChangeManager` already tracks changes but the underlying row data could be more memory-efficient. + +**Trade-off**: These are C-level optimizations that add complexity. Only worth it if profiling shows memory/performance issues with large result sets (100K+ rows). + +--- + +## 6. Export Pipeline Improvements + +**Status**: Not started +**Effort**: Medium | **Impact**: Medium +**Sequel-Ace reference**: `Source/Controllers/DataExport/SPExportController.h/m`, `Source/Controllers/DataExport/Exporters/` + +### What Sequel-Ace Does Better + +#### 6.1 NSOperation-Based Concurrent Export + +``` +SPExporter : NSOperation +├── SPCSVExporter +├── SPSQLExporter +├── SPXMLExporter +├── SPDotExporter +├── SPPDFExporter +└── SPHTMLExporter + +NSOperationQueue handles concurrent multi-table exports +``` + +Each exporter runs as an independent operation — multiple tables export simultaneously. + +#### 6.2 Streaming Export + +- Uses `SPMySQLFastStreamingResult` — never buffers full table in memory +- Row-by-row write to output file +- Progress tracked by rows processed vs total + +#### 6.3 Transparent Compression + +- `SPFileHandle` wraps file I/O with gzip/bzip2 support +- `setCompressionFormat:` before writing — compression happens at write time +- No separate compression step needed + +#### 6.4 Template-Based Filenames + +Tokens for multi-file export: +- `{database}` → current database name +- `{table}` → current table name +- `{date}` → export date +- `{time}` → export time +- `{host}` → connection host + +#### 6.5 Additional Export Formats + +Formats TablePro doesn't have: +- **XML** — structured data with schema information +- **Dot/GraphViz** — database relationship diagrams (ER diagrams) +- **PDF** — formatted table data for printing/sharing +- **HTML** — styled table data for web viewing + +### Proposed TablePro Improvements + +1. **Concurrent export** — use Swift structured concurrency (`TaskGroup`) for multi-table export +2. **Streaming** — add `exportRows(streaming:)` to plugin protocol, iterate without buffering +3. **Compression** — add gzip option to export UI (use `Foundation.Data.compress`) +4. **Filename templates** — for multi-table/multi-file exports +5. **Dot/ER diagram export** — generate relationship graphs from foreign key metadata + +--- + +## 7. Script/Command Extensibility + +**Status**: Not started +**Effort**: Medium | **Impact**: Medium +**Sequel-Ace reference**: `Source/Controllers/Other/SPBundleManager.h/m`, `Source/Other/Utility/SABundleRunner.h/m` + +### What Sequel-Ace Does + +#### 7.1 Bundle System + +Scripts stored as `.sequelbundle` directories: + +``` +MyCommand.sequelbundle/ +├── info.plist // metadata: name, scope, trigger, I/O config +└── command.sh // executable script +``` + +#### 7.2 Execution Context + +Scripts receive context via environment variables: +- `$SP_DATABASE_NAME` — current database +- `$SP_SELECTED_TABLE` — active table +- `$SP_QUERY_FILE` — path to temp file with current query/selection +- `$SP_QUERY_RESULT_FILE` — path to query result data +- `$SP_CURRENT_EDITED_COLUMN_NAME` — column being edited + +Input delivered via stdin redirect from temp file. + +#### 7.3 Output Disposition via Exit Code + +| Exit Code | Action | +|-----------|--------| +| 200 | No action | +| 201 | Replace selection | +| 202 | Replace all content | +| 203 | Insert as text | +| 205 | Show as HTML in floating window | +| 207 | Show as text tooltip | +| 208 | Show as HTML tooltip | + +#### 7.4 Scopes + +Scripts bound to execution contexts: +- **QueryEditor** — runs with editor selection/content +- **DataTable** — runs with table row data +- **InputField** — runs with field editor content +- **General** — runs with no specific context + +#### 7.5 HTML Output Window + +- WebKit-based floating window for rich script output +- Zoom, navigation, save, print support +- Scripts can generate charts, reports, formatted data + +### Proposed TablePro Architecture + +``` +CustomCommands/ +├── CommandDefinition.swift // name, scope, trigger, I/O config +├── CommandRunner.swift // NSTask/Process execution +├── CommandEnvironment.swift // Environment variable injection +├── CommandOutputHandler.swift // Exit code → action dispatch +├── CommandManagerView.swift // UI for managing commands +└── HTMLOutputPanel.swift // WKWebView floating window +``` + +**Modern improvements over Sequel-Ace**: +- Use `Process` (Swift) instead of `NSTask` (ObjC) +- JSON-based command definitions instead of plist +- Structured concurrency for async execution +- SwiftUI settings pane for command management +- Keyboard shortcut assignment via `KeyboardShortcut` + +--- + +## 8. Geometry Data Visualization + +**Status**: Not started +**Effort**: Low | **Impact**: Low (niche but differentiating) +**Sequel-Ace reference**: `Source/Views/SPGeometryDataView.h/m` + +### What Sequel-Ace Does + +Renders MySQL GEOMETRY types as visual diagrams: + +**Supported types**: POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, GEOMETRYCOLLECTION + +**Rendering**: +- Auto-scaling to fit target dimension (default 400px) +- 10px margin border +- Color scheme: Points (red fill + gray stroke), Lines (black), Polygons (alternating cyan/lime/red with 10% alpha fill) +- NSBezierPath-based drawing +- PDF export via `-pdfData` + +**Input**: WKT-parsed coordinate dictionary with `type`, `coordinates`, `bbox` keys. + +### Proposed TablePro Implementation + +```swift +struct GeometryView: View { + let geometry: GeometryData // parsed WKT + + var body: some View { + Canvas { context, size in + let transform = calculateTransform(bbox: geometry.bbox, targetSize: size) + switch geometry.type { + case .point: drawPoints(context, geometry.coordinates, transform) + case .lineString: drawLineString(context, geometry.coordinates, transform) + case .polygon: drawPolygon(context, geometry.coordinates, transform) + // ... etc + } + } + } +} +``` + +**Integration**: Show in field editor's Image tab when column type is geometry. Also useful as a cell renderer (thumbnail in data grid). + +**Database support**: MySQL (WKT/WKB), PostgreSQL/PostGIS (ST_AsText, ST_AsBinary), SQLite/SpatiaLite. + +--- + +## 9. SSH Tunnel Improvements + +**Status**: Not started +**Effort**: Low-Medium | **Impact**: Low-Medium +**Sequel-Ace reference**: `Source/Other/SSHTunnel/SPSSHTunnel.h/m`, `Source/Other/SSHTunnel/SequelAceTunnelAssistant.m` + +### What Sequel-Ace Does + +#### 9.1 Separate Tunnel Assistant Process + +- `SequelAceTunnelAssistant` is a standalone helper executable +- Acts as `SSH_ASKPASS` for interactive password/passphrase prompts +- Communicates with main app via `NSConnection` RPC (deprecated → use XPC) +- Checks Keychain first (silent auth), then prompts UI if needed + +#### 9.2 Connection Muxing + +- `ControlMaster=auto` with hashed control path +- Reuses SSH connections across multiple database connections to same host +- Disabled by default due to stability issues + +#### 9.3 Keepalive + +- `TCPKeepAlive=yes` +- `ServerAliveInterval` (configurable) +- `ServerAliveCountMax=3` + +#### 9.4 Port Allocation + +- Random local port via `getRandomPort()` +- Fallback port for host failover + +### Applicability to TablePro + +- **XPC Service** for SSH tunnel (modern replacement for NSConnection RPC) +- **Connection muxing** for users connecting to multiple databases on same host +- **Keepalive configuration** exposed in connection settings + +--- + +## 10. Design Patterns Worth Adopting + +### 10.1 Sparse Edit Overlay + +**Pattern**: Only copy/store rows that the user has edited. Unedited rows read directly from the underlying result store. + +**Sequel-Ace**: `NSPointerArray editedRows` — sparse array, only non-nil at edited indices. + +**TablePro application**: Could optimize `DataChangeManager` to avoid duplicating entire row arrays for single-cell edits. + +### 10.2 Template-Based SQL Generation + +**Pattern**: Define SQL fragments as templates with placeholders, interpolate at runtime. + +**Sequel-Ace**: `ContentFilters.plist` with `$CURRENT_FIELD`, `$BINARY`, `${}` placeholders. + +**TablePro application**: Filter operators, query builders, statement generators. Makes SQL generation database-agnostic and user-extensible. + +### 10.3 Tri-State Checkbox Hierarchy + +**Pattern**: Parent checkbox reflects aggregate state of children (all checked, all unchecked, mixed). + +**Sequel-Ace**: Filter rule enable/disable, with parent OR/AND groups showing mixed state. + +**TablePro application**: Filter preset management, multi-select operations, column visibility toggles. + +### 10.4 NSOperation Export Pipeline + +**Pattern**: Each export format is an `NSOperation` subclass. Queue handles concurrency and cancellation. + +**Sequel-Ace**: `SPExporter` base class → `SPCSVExporter`, `SPSQLExporter`, etc. + +**TablePro application**: Use Swift `Operation` subclasses or structured concurrency `TaskGroup` for concurrent multi-table export. + +### 10.5 Lazy Cell Conversion + +**Pattern**: Store raw bytes from database. Convert to String/Number only when UI requests the cell value. + +**Sequel-Ace**: C char arrays stored in malloc zone → NSString created on `cellDataAtRow:column:` call. + +**TablePro application**: Store `Data` in RowBuffer, convert to display type in `DataGridView` cell provider. Saves memory for cells never scrolled into view. + +### 10.6 Custom Malloc Zone + +**Pattern**: Allocate all result data in a dedicated malloc zone. Destroy zone on result reload for instant cleanup. + +**Sequel-Ace**: `malloc_create_zone(64 * 1024, 0)` per result store. + +**TablePro application**: Consider for RowBuffer backing store if profiling shows deallocation overhead for large result sets. + +### 10.7 Exit-Code-Driven Output Dispatch + +**Pattern**: Script exit code determines what happens with the output (replace selection, show as HTML, insert as text, etc.). + +**Sequel-Ace**: Exit codes 200-208 mapped to specific UI actions. + +**TablePro application**: For custom command/script extensibility system. + +--- + +## 11. Priority Matrix + +| # | Feature | Effort | Impact | Priority | Dependencies | +|---|---------|--------|--------|----------|-------------| +| 1 | Template-driven filter operators | Medium (1-2 weeks) | High | **P0** | None | +| 2 | Advanced field editor (hex/JSON/bit/QL) | Medium (1-2 weeks) | High | **P0** | None | +| 3 | CSV import with field mapping | Medium (1-2 weeks) | High | **P1** | Import plugin protocol | +| 4 | Process list viewer | Low (2-3 days) | Medium | **P1** | Driver protocol addition | +| 5 | Server variables inspector | Low (1-2 days) | Medium | **P2** | Driver protocol addition | +| 6 | Export pipeline (concurrent/streaming) | Medium (1 week) | Medium | **P2** | None | +| 7 | Database copy/rename | Low-Medium (3-5 days) | Medium | **P2** | Driver protocol addition | +| 8 | Script/command extensibility | Medium (1-2 weeks) | Medium | **P3** | None | +| 9 | Geometry visualization | Low (2-3 days) | Low | **P3** | WKT parser | +| 10 | Streaming/lazy conversion | High (2-3 weeks) | Medium | **P3** | RowBuffer refactor | +| 11 | SSH tunnel improvements | Low-Medium (3-5 days) | Low | **P4** | XPC service | +| 12 | User/role manager | High (1-2 weeks per DB) | Low-Medium | **P4** | Per-database implementation | + +**Priority key**: P0 = Next sprint, P1 = This quarter, P2 = Next quarter, P3 = Backlog, P4 = Nice-to-have + +--- + +## 12. Implementation Tracking + +### P0 — Next Sprint + +- [ ] **Template-driven filter operators** + - [ ] Define `FilterOperatorDefinition` Codable struct + - [ ] Create `mysql-filters.json`, `postgresql-filters.json`, `sqlite-filters.json` + - [ ] Implement `FilterOperatorRegistry` (loads JSON, keyed by DatabaseType + column type) + - [ ] Implement `FilterSQLGenerator` (template interpolation with escaping) + - [ ] Build `FilterRuleBuilderView` (SwiftUI, nested AND/OR groups) + - [ ] Build `FilterQuickGridView` (column-per-column fast filter) + - [ ] Integrate with existing filtering in `MainContentCoordinator+Filtering` + - [ ] Add filter preset save/load + - [ ] Add per-rule enable/disable checkboxes + - [ ] Plugin support: plugins can bundle their own filter JSON + +- [ ] **Advanced field editor** + - [ ] Design `FieldEditorView` (SwiftUI sheet with tab picker) + - [ ] Implement `HexEditorTab` (hex dump view + editable hex input) + - [ ] Implement `JSONEditorTab` (CodeEditSourceEditor with JSON grammar) + - [ ] Implement `BitFieldTab` (SwiftUI Grid with toggles + decimal/hex/octal sync) + - [ ] Implement `ImagePreviewTab` (QLPreviewPanel integration) + - [ ] Add file type detection from BLOB data (UTType magic bytes) + - [ ] Wire up to DataChangeManager for edit flow + - [ ] Double-click cell → detect type → open appropriate tab + +### P1 — This Quarter + +- [ ] **CSV import with field mapping** + - [ ] Design `FieldMapperView` (SwiftUI) + - [ ] Implement `CSVChunkReader` (streaming 1MB chunks) + - [ ] Implement field alignment modes (by name, by index, custom) + - [ ] Implement import methods (INSERT, REPLACE, UPDATE) + - [ ] Add global value expressions (`NOW()`, `$1 + $2`) + - [ ] Add progress tracking and error handling (ask/ignore/abort) + - [ ] Add to `PluginDatabaseDriver` protocol: `importRows` method + +- [ ] **Process list viewer** + - [ ] Add `getProcessList()` to `PluginDatabaseDriver` protocol + - [ ] Implement for MySQL (`SHOW PROCESSLIST`) + - [ ] Implement for PostgreSQL (`SELECT * FROM pg_stat_activity`) + - [ ] Implement for Redis (`CLIENT LIST`) + - [ ] Build `ProcessListView` (SwiftUI Table + auto-refresh timer) + - [ ] Add kill query/connection support + - [ ] Add filtering and save-to-file + +### P2 — Next Quarter + +- [ ] **Server variables inspector** + - [ ] Add `getServerVariables()` to `PluginDatabaseDriver` protocol + - [ ] Implement for MySQL, PostgreSQL, Redis + - [ ] Build `ServerVariablesView` (searchable SwiftUI Table) + +- [ ] **Export pipeline improvements** + - [ ] Add streaming export support to plugin protocol + - [ ] Implement concurrent multi-table export (TaskGroup) + - [ ] Add gzip compression option + - [ ] Add filename template system for multi-file export + +- [ ] **Database copy/rename** + - [ ] Add protocol methods + - [ ] Implement for MySQL, PostgreSQL + - [ ] Add to sidebar context menu + +### P3 — Backlog + +- [ ] **Script/command extensibility** + - [ ] Design command definition format (JSON) + - [ ] Implement `CommandRunner` (Process-based execution) + - [ ] Implement environment variable injection + - [ ] Implement exit-code-driven output dispatch + - [ ] Build command manager UI + - [ ] Add HTML output panel (WKWebView) + +- [ ] **Geometry visualization** + - [ ] Implement WKT parser + - [ ] Build `GeometryView` (SwiftUI Canvas) + - [ ] Integrate with field editor Image tab + - [ ] Support MySQL, PostGIS, SpatiaLite + +- [ ] **Streaming/lazy conversion** + - [ ] Profile current RowBuffer performance with 100K+ rows + - [ ] Prototype lazy string conversion (store Data, convert on access) + - [ ] Evaluate malloc zone isolation for RowBuffer + - [ ] Implement sparse edit overlay if profiling justifies + +### P4 — Nice-to-Have + +- [ ] **SSH tunnel improvements** (XPC service, connection muxing) +- [ ] **User/role manager** (MySQL grants, PostgreSQL roles, Redis ACLs) diff --git a/licenseapp b/licenseapp new file mode 160000 index 00000000..6a5d06b9 --- /dev/null +++ b/licenseapp @@ -0,0 +1 @@ +Subproject commit 6a5d06b9f1c4c11e51f748065345311c589f2178 diff --git a/sequel-ace-vs-tablepro-analysis.md b/sequel-ace-vs-tablepro-analysis.md new file mode 100644 index 00000000..db42e795 --- /dev/null +++ b/sequel-ace-vs-tablepro-analysis.md @@ -0,0 +1,427 @@ +# Sequel-Ace vs TablePro — Deep Comparative Analysis + +> Generated: 2026-03-22 | Sequel-Ace v5.2.0 | TablePro (current main) + +--- + +## 1. Project Overview + +| Aspect | Sequel-Ace | TablePro | +| ---------------- | ------------------------------------------ | -------------------------------------------------- | +| **Type** | macOS database client (MySQL/MariaDB only) | macOS database client (multi-database) | +| **Origin** | Fork of Sequel Pro (~20+ years lineage) | Built from scratch | +| **Language** | ~75% Objective-C, ~25% Swift | 100% Swift | +| **UI Framework** | AppKit + Interface Builder (XIB/NIB) | SwiftUI + AppKit interop | +| **Min macOS** | 12.0 (Monterey) | 14.0 (Sonoma) | +| **Architecture** | Universal Binary (arm64 + x86_64) | Universal Binary (arm64 + x86_64) | +| **Distribution** | Mac App Store + Homebrew (free) | Direct download + Sparkle auto-update (commercial) | +| **License** | MIT (open-source) | Proprietary (commercial) | + +--- + +## 2. Database Support + +| Database | Sequel-Ace | TablePro | +| ---------- | ------------------------------ | -------------------------------- | +| MySQL | Yes (native SPMySQL.framework) | Yes (plugin, CMariaDB) | +| MariaDB | Yes (via MySQL driver) | Yes (plugin, CMariaDB) | +| PostgreSQL | No | Yes (plugin, CLibPQ) | +| Redshift | No | Yes (via PostgreSQL plugin) | +| SQLite | No\* | Yes (plugin, Foundation sqlite3) | +| Redis | No | Yes (plugin, CRedis) | +| MongoDB | No | Yes (plugin, CLibMongoc) | +| ClickHouse | No | Yes (plugin, URLSession HTTP) | +| SQL Server | No | Yes (plugin, CFreeTDS) | +| Oracle | No | Yes (plugin, OracleNIO SPM) | +| DuckDB | No | Yes (plugin, CDuckDB) | + +\*Sequel-Ace uses FMDB/SQLite internally for query history storage, not as a user-facing database driver. + +**Key Difference:** Sequel-Ace is deeply specialized for MySQL/MariaDB. TablePro supports 11 database types through a modular plugin architecture. + +--- + +## 3. Architecture Comparison + +### 3.1 Application Architecture + +| Aspect | Sequel-Ace | TablePro | +| ------------------------ | --------------------------------------------- | -------------------------------------------------------- | +| **Pattern** | Classic MVC (Cocoa) | MVVM + Coordinator | +| **Document Model** | SPDatabaseDocument (monolithic, ~6,665 lines) | MainContentCoordinator (split across 7+ extension files) | +| **Window Model** | NSDocument-based, custom SPWindow | Native macOS window tabs (tabbingIdentifier) | +| **State Management** | NSNotificationCenter + delegates | Combine/ObservableObject + SwiftUI bindings | +| **Dependency Injection** | IBOutlets + manual wiring | Protocol-based + environment objects | + +### 3.2 Driver/Plugin Architecture + +**Sequel-Ace:** + +- Monolithic driver: SPMySQL.framework (custom Objective-C framework wrapping libmysqlclient) +- No plugin system for database drivers +- Bundle system exists but for user scripts/commands only (TextMate-style) +- QueryKit.framework for SQL query building (another custom framework) +- C libraries shipped as dynamic libraries (libmysqlclient.24.dylib, libssl.3.dylib, libcrypto.3.dylib) + +**TablePro:** + +- `.tableplugin` bundles loaded at runtime by PluginManager +- TableProPluginKit shared framework defines `PluginDatabaseDriver` protocol +- PluginDriverAdapter bridges plugin drivers to app's `DatabaseDriver` protocol +- C bridges per plugin (CMariaDB, CLibPQ, CFreeTDS, CLibMongoc, CRedis, CDuckDB) +- Static libraries (.a files) downloaded from GitHub Releases +- 11 Xcode targets (app + tests + PluginKit + 8 plugins) + +### 3.3 Query Editor + +**Sequel-Ace:** + +- `SPTextView` — Custom NSTextView subclass (~3,865 lines) +- Hand-built SQL syntax highlighting via NSTextStorage +- Custom autocomplete (SPNarrowDownCompletion) +- Bracket highlighting (SPBracketHighlighter) +- Code snippets with mirroring +- SQL tokenizer (SPSQLTokenizer, flex-based lexer SPEditorTokens.l) + +**TablePro:** + +- CodeEditSourceEditor (SPM, tree-sitter based) +- SQLEditorCoordinator bridges all features +- Tree-sitter syntax highlighting (handled by CESS) +- CompletionEngine with SQLCompletionAdapter +- SQLContextAnalyzer for context-aware completion +- Vim key interceptor, inline AI suggestions + +### 3.4 Data Grid + +**Sequel-Ace:** + +- `SPCopyTable` → `SPTableView` → NSTableView (Objective-C subclass chain) +- SPDataStorage wraps SPMySQLStreamingResultStore +- Inline cell editing with SPFieldEditorController +- Pagination via ContentPaginationViewController +- SPRuleFilterController for visual query builder + +**TablePro:** + +- DataGridView (NSTableView wrapped in SwiftUI) +- Identity-based update guard to prevent redundant reloads +- RowBuffer (class) avoids CoW on large arrays +- Generation counter pattern prevents out-of-order result flashes +- RowBuffer eviction keeps only 2 most recently-executed tabs in memory + +--- + +## 4. Feature Comparison + +### 4.1 Connection Management + +| Feature | Sequel-Ace | TablePro | +| ----------------------- | ------------------------------------------- | ------------------------------------------------------------ | +| Standard TCP/IP | Yes | Yes | +| Unix Socket | Yes | N/A (multi-db) | +| SSH Tunneling | Yes (full, with dedicated tunnel assistant) | Yes | +| SSL/TLS | Yes (certificate config) | Yes | +| AWS IAM Auth | Yes (with MFA token) | No | +| Keychain Storage | Yes | Yes (ConnectionStorage) | +| Color-coded Connections | Yes (7 colors) | Yes | +| Favorites/Groups | Yes (tree-based, SPFavoriteNode) | Yes | +| Connection Pooling | Yes | Yes (DatabaseManager) | +| Health Monitoring | Yes (keep-alive ping) | Yes (ConnectionHealthMonitor, 30s ping, exponential backoff) | +| Auto-reconnect | Yes (retry logic) | Yes (exponential backoff) | + +### 4.2 Query Features + +| Feature | Sequel-Ace | TablePro | +| ---------------------- | --------------------------------- | --------------------------------------------- | +| SQL Editor | Yes (SPTextView, ~3.8K lines) | Yes (CodeEditSourceEditor, tree-sitter) | +| Syntax Highlighting | Yes (NSTextStorage-based) | Yes (tree-sitter) | +| Autocomplete | Yes (SPNarrowDownCompletion) | Yes (CompletionEngine + SQLCompletionAdapter) | +| Bracket Matching | Yes (SPBracketHighlighter) | Yes (CESS built-in) | +| Query Favorites | Yes (SPQueryFavoriteManager) | Yes | +| Query History | Yes (SQLiteHistoryManager, FTS) | Yes (QueryHistoryStorage, SQLite FTS5) | +| Multi-query Execution | Yes (range detection) | Yes | +| MySQL Help Integration | Yes (showMySQLHelpForCurrentWord) | No | +| Code Snippets | Yes (with mirroring) | No | +| Vim Mode | No | Yes (VimKeyInterceptor) | +| AI Suggestions | No | Yes (InlineSuggestionManager) | +| Multi-cursor | No | Yes (CodeEditSourceEditor) | + +### 4.3 Data Viewing & Editing + +| Feature | Sequel-Ace | TablePro | +| ------------------------ | ---------------------------------------- | ---------------------------------- | +| Table Data Grid | Yes (SPCopyTable/NSTableView) | Yes (DataGridView/NSTableView) | +| Cell Editing | Yes (inline + sheet-based) | Yes (inline) | +| BLOB Handling | Yes (hex view, image preview, QuickLook) | Yes | +| JSON Formatting | Yes (SPJSONFormatter) | Yes | +| Geometry Data View | Yes (SPGeometryDataView) | No | +| Bit Field Editor | Yes (visual bit toggles) | No | +| Row Add/Duplicate/Delete | Yes | Yes | +| Pagination | Yes (ContentPaginationView) | Yes | +| Filtering | Yes (rule-based SPRuleFilterController) | Yes | +| Copy as CSV/SQL/Tab | Yes (SPCopyTable built-in) | Yes | +| Change Tracking | Manual | Yes (DataChangeManager, undo/redo) | + +### 4.4 Schema Management + +| Feature | Sequel-Ace | TablePro | +| ---------------------- | ------------------------------------------- | -------- | +| View Table Structure | Yes (SPTableStructure) | Yes | +| Edit Columns | Yes | Yes | +| Create Table | Yes | Yes | +| View CREATE Syntax | Yes | Yes | +| Triggers | Yes (SPTableTriggers) | Yes | +| Relations/Foreign Keys | Yes (SPTableRelations) | Yes | +| Indexes | Yes (SPIndexesController) | Yes | +| User Management | Yes (SPUserManager with Core Data) | No | +| Server Variables | Yes (SPServerVariablesController) | No | +| Process List | Yes (SPProcessListController, kill queries) | No | + +### 4.5 Export/Import + +| Feature | Sequel-Ace | TablePro | +| ----------- | --------------------------- | ---------------------------- | +| CSV Export | Yes | Yes (plugin) | +| JSON Export | Yes | Yes (plugin) | +| SQL Export | Yes (51KB implementation) | Yes (plugin) | +| XML Export | Yes | No | +| PDF Export | Yes | No | +| HTML Export | Yes | No | +| Dot Export | Yes (graph visualization) | No | +| XLSX Export | No | Yes (plugin) | +| CSV Import | Yes (with field mapping UI) | No (separate plugin planned) | +| SQL Import | No | Yes (plugin) | +| MQL Import | No | Yes (plugin, MongoDB) | + +### 4.6 Administrative Features + +| Feature | Sequel-Ace | TablePro | +| ----------------- | --------------------------- | -------- | +| User Management | Yes (full GRANT management) | No | +| Process List | Yes (with kill capability) | No | +| Server Variables | Yes (view/filter) | No | +| Database Copy | Yes (SPDatabaseCopy) | No | +| Database Rename | Yes (SPDatabaseRename) | No | +| Table Duplication | Yes (with data option) | No | +| Console/Query Log | Yes (SPConsoleMessage) | Yes | + +### 4.7 Unique to Each + +**Sequel-Ace Only:** + +- MySQL-specific admin tools (user management, process list, server variables) +- Bundle/script system (TextMate-style extensibility) +- AWS IAM/RDS authentication with MFA +- Geometry data visualization +- Bit field visual editor +- AppleScript support +- PDF/HTML/XML/Dot export +- CSV import with field mapping UI +- Database copy/rename operations +- 18-language localization + +**TablePro Only:** + +- Multi-database support (11 database types) +- Plugin architecture for extensible drivers +- SwiftUI-based modern UI +- Vim mode in editor +- AI-powered inline suggestions +- Multi-cursor editing (via CodeEditSourceEditor) +- Tree-sitter syntax highlighting +- Redis key tree navigation +- MongoDB query builder +- XLSX export +- Sparkle auto-update +- Tab persistence with snapshot/restore +- RowBuffer memory optimization (eviction policy) + +--- + +## 5. UI & Design + +### 5.1 UI Technology + +| Aspect | Sequel-Ace | TablePro | +| --------------------- | ---------------------------------------------------------- | ------------------------------------------------- | +| **Primary Framework** | AppKit + Interface Builder | SwiftUI + AppKit interop | +| **Interface Files** | 30 XIBs + 1 Storyboard | SwiftUI views (code-only) | +| **SwiftUI Usage** | None (pure AppKit) | Primary UI framework | +| **Custom Controls** | SPSplitView, SPWindow, SPTableView, SPTextView | DataGridView (NSTableView wrapper), SQLEditorView | +| **Theming** | NSAppearance (light/dark/system) + color assets (17 sets) | SQLEditorTheme + TableProEditorTheme adapter | +| **Editor Themes** | Customizable via preference pane (export/import/duplicate) | Built-in theme system | + +### 5.2 Window Architecture + +**Sequel-Ace:** + +- Single main window (MainWindow.xib, 1200x630 default) +- Horizontal SPSplitView: left panel (table navigator) + right panel (NSTabView with 7 tabs) +- Tabs: Structure, Content, Relations, Triggers, Custom Query, Indexes, Extended Info +- NSToolbar for navigation +- Preference window: toolbar-based 6-pane switcher + +**TablePro:** + +- Native macOS window tabs (each tab = separate NSWindow in tab group) +- MainContentView (SwiftUI) with sidebar + content area +- MainContentCoordinator split across 7+ extension files +- EditorTabBar (pure SwiftUI) for query tabs within each connection + +### 5.3 Localization + +| Aspect | Sequel-Ace | TablePro | +| ----------------- | -------------------------------------------------------------------------------- | ------------------------------------ | +| **Languages** | 18 (en, ar, cs, de, eo, es, fr, it, ja, pt, pt-BR, ru, tr, vi, zh-Hans, zh-Hant) | 2 (en, vi via Localizable.xcstrings) | +| **Format** | .strings files (~3,530 strings) | Localizable.xcstrings (Xcode 15+) | +| **Documentation** | N/A (app-only) | Mintlify docs (en + vi) | + +--- + +## 6. Build System & CI/CD + +### 6.1 Build Configuration + +| Aspect | Sequel-Ace | TablePro | +| --------------------- | ---------------------------------------------------------------- | -------------------------------------------------------- | +| **Project Type** | .xcodeproj (3 sub-projects) | .xcodeproj (11 targets) | +| **Targets** | 4 (app, tests, xibPostprocessor, tunnelAssistant) | 11 (app, tests, PluginKit, 8 plugins) | +| **SPM Dependencies** | 6 (Alamofire, AppCenter, FMDB, PLCrashReporter, SnapKit, OCMock) | 3 (CodeEditSourceEditor, Sparkle, OracleNIO) | +| **Custom Frameworks** | 3 (SPMySQL, QueryKit, ShortcutRecorder) | 1 (TableProPluginKit) | +| **C Libraries** | Dynamic (.dylib): libmysqlclient, libssl, libcrypto | Static (.a): libmariadb, libpq, etc. via download script | +| **pbxproj Version** | Standard | objectVersion 77 (filesystem-synced groups) | + +### 6.2 Testing + +| Aspect | Sequel-Ace | TablePro | +| ------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------- | +| **Test Files** | 32 (~6,288 lines) | Multiple (XCTest) | +| **Languages** | Mixed ObjC + Swift | Swift only | +| **Mock Framework** | OCMock | None (protocol-based mocking) | +| **Test Areas** | AWS auth, string utils, JSON formatting, table filtering, sorting, DB operations, SSL validation | Redis key tree, query builders, model tests | + +### 6.3 CI/CD + +| Aspect | Sequel-Ace | TablePro | +| ------------------- | ----------------------------------------------- | ----------------------------------------------- | +| **CI Platform** | GitHub Actions (macOS 15, Xcode 16.2) | GitHub Actions | +| **Trigger** | Pull requests | v\* tags | +| **Automation** | Fastlane (version bump, changelog, PR creation) | Shell scripts (build-release.sh, create-dmg.sh) | +| **Release** | App Store via Fastlane | DMG/ZIP + Sparkle signatures | +| **Crash Reporting** | AppCenter + PLCrashReporter | None (Sparkle only) | + +### 6.4 Code Quality + +| Aspect | Sequel-Ace | TablePro | +| ------------------------- | ----------------------------------------- | ------------------------------------------------------- | +| **Linter** | SwiftLint (lenient: file_length disabled) | SwiftLint (strict: warn 1200, error 1800) + SwiftFormat | +| **Function Body Limit** | 120 lines | 160 warn / 250 error | +| **Cyclomatic Complexity** | 20 | 40 warn / 60 error | +| **Line Length** | Disabled | 120 (SwiftFormat), 180 warn / 300 error (SwiftLint) | +| **Force Unwrap** | Opt-in warning | Banned | + +--- + +## 7. Code Quality & Technical Debt + +### 7.1 Sequel-Ace Pain Points + +1. **Massive Files:** + - SPDatabaseDocument.m: ~6,665 lines (god object) + - SPConnectionController.m: ~180KB + - SPCustomQuery.m: ~173KB / 3,870 lines + - SPTableContent.m: ~198KB + - SPTextView.m: ~3,865 lines + - DBView.xib: 542KB (monolithic interface file) + +2. **Mixed Language Complexity:** + - Bridging header required (Sequel-Ace-Bridging-Header.h) + - ObjC/Swift interop overhead + - 177 header files to maintain + +3. **Tightly Coupled Components:** + - SPDatabaseDocument manages everything (connection, views, toolbar, state, undo) + - IBOutlet-based wiring between components + - NSNotificationCenter for cross-component communication (hard to trace) + +4. **Legacy Patterns:** + - Manual memory management patterns (even with ARC) + - XIB-based UI (harder to diff, merge conflicts) + - ThirdParty/ directory with vendored code (RegexKitLite, MGTemplateEngine, etc.) + +### 7.2 TablePro Advantages + +1. **Clean Separation:** Plugin system isolates database-specific code +2. **Modern Swift:** No ObjC interop complexity, protocol-oriented +3. **SwiftUI:** Declarative UI, code-only (easy to diff/review) +4. **Coordinator Pattern:** Split across extension files, manageable sizes +5. **Performance-Conscious:** NSString O(1) length, RowBuffer eviction, generation counters + +### 7.3 Sequel-Ace Advantages + +1. **Feature Maturity:** 20+ years of MySQL-specific features +2. **Admin Tools:** User management, process list, server variables +3. **Localization:** 18 languages vs TablePro's 2 +4. **Battle-Tested:** Large community, extensive edge case handling +5. **Free/Open-Source:** MIT license, community contributions + +--- + +## 8. Codebase Statistics + +| Metric | Sequel-Ace | TablePro | +| ------------------------ | --------------------------------------- | ----------------------------------------------------------- | +| **Objective-C .m files** | ~160 | 0 | +| **Objective-C .h files** | ~177 | 0 | +| **Swift files** | ~52 | Majority | +| **XIB/Storyboard files** | 31 | 0 | +| **C bridge modules** | 0 (dynamic libs) | 6 (CMariaDB, CLibPQ, CFreeTDS, CLibMongoc, CRedis, CDuckDB) | +| **Frameworks (custom)** | 3 (SPMySQL, QueryKit, ShortcutRecorder) | 1 (TableProPluginKit) | +| **SPM dependencies** | 6 | 3 | +| **Xcode targets** | 4 | 11 | +| **Test files** | 32 | Multiple | +| **Supported databases** | 2 (MySQL, MariaDB) | 11 | +| **Supported languages** | 18 | 2 (en, vi) | + +--- + +## 9. What TablePro Can Learn from Sequel-Ace + +### 9.1 Features Worth Considering + +1. **MySQL Admin Tools** — User management, process list, server variable inspector +2. **Database Copy/Rename** — Administrative operations for supported databases +3. **Advanced BLOB Editing** — Hex view, bit field editor, geometry visualization +4. **Bundle/Script System** — User-extensible commands (TextMate-style) +5. **CSV Import with Field Mapping** — Visual column mapping UI +6. **AWS IAM Authentication** — For RDS connections +7. **More Export Formats** — PDF, HTML, XML for reporting +8. **AppleScript Support** — Automation for power users + +### 9.2 Patterns to Avoid + +1. **God objects** — SPDatabaseDocument (6,665 lines) manages everything +2. **Monolithic XIBs** — DBView.xib at 542KB is unmaintainable +3. **Mixed language** — ObjC/Swift bridging adds complexity without clear benefit +4. **Vendored dependencies** — ThirdParty/ directory with aging libraries +5. **Dynamic library shipping** — Static linking (TablePro approach) is more reliable +6. **Disabled linting rules** — file_length, line_length disabled defeats the purpose + +### 9.3 Architecture Lessons + +- TablePro's plugin system is far superior for multi-database support +- SwiftUI declarative UI is more maintainable than XIB-based approach +- Coordinator pattern with extensions (TablePro) scales better than monolithic document (Sequel-Ace) +- Static library linking (TablePro) is more portable than dynamic library shipping (Sequel-Ace) +- Protocol-oriented testing (TablePro) is simpler than OCMock-based mocking (Sequel-Ace) + +--- + +## 10. Summary + +**Sequel-Ace** is a mature, MySQL-specialized database client with deep admin capabilities and 20+ years of feature development. Its strength is MySQL feature completeness and community-driven localization (18 languages). Its weakness is architectural — a monolithic Objective-C codebase with massive god objects, tightly coupled components, and no database extensibility. + +**TablePro** is a modern, multi-database client built with clean architecture principles. Its strength is the modular plugin system (11 databases), modern Swift/SwiftUI stack, and performance-conscious design. It trades MySQL admin depth for breadth of database support and a more maintainable codebase. + +The projects target different market segments: Sequel-Ace serves MySQL power users who need deep admin tools (free), while TablePro serves developers who work across multiple databases and value modern UX (commercial). From 3ae238aab6ba9d059abab8fe2d85aa21dc34e64c Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 20:42:28 +0700 Subject: [PATCH 8/9] chore: remove accidentally staged files --- .claude/worktrees/agent-a1a2c85e | 1 - .claude/worktrees/agent-a45fa581 | 1 - .claude/worktrees/agent-a47a2ae2 | 1 - .claude/worktrees/agent-a52204c8 | 1 - .claude/worktrees/agent-a9fb7d7e | 1 - .claude/worktrees/agent-ab285578 | 1 - .claude/worktrees/agent-afb913a6 | 1 - Sequel-Ace | 1 - docs/development/sequel-ace-learnings.md | 874 ----------------------- licenseapp | 1 - sequel-ace-vs-tablepro-analysis.md | 427 ----------- 11 files changed, 1310 deletions(-) delete mode 160000 .claude/worktrees/agent-a1a2c85e delete mode 160000 .claude/worktrees/agent-a45fa581 delete mode 160000 .claude/worktrees/agent-a47a2ae2 delete mode 160000 .claude/worktrees/agent-a52204c8 delete mode 160000 .claude/worktrees/agent-a9fb7d7e delete mode 160000 .claude/worktrees/agent-ab285578 delete mode 160000 .claude/worktrees/agent-afb913a6 delete mode 160000 Sequel-Ace delete mode 100644 docs/development/sequel-ace-learnings.md delete mode 160000 licenseapp delete mode 100644 sequel-ace-vs-tablepro-analysis.md diff --git a/.claude/worktrees/agent-a1a2c85e b/.claude/worktrees/agent-a1a2c85e deleted file mode 160000 index 48e706ed..00000000 --- a/.claude/worktrees/agent-a1a2c85e +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 48e706ed534dfa3d3784267ea94191da8c89cda6 diff --git a/.claude/worktrees/agent-a45fa581 b/.claude/worktrees/agent-a45fa581 deleted file mode 160000 index ebcded6d..00000000 --- a/.claude/worktrees/agent-a45fa581 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ebcded6d18ab66158eca4492cb5907bd88e6c4f8 diff --git a/.claude/worktrees/agent-a47a2ae2 b/.claude/worktrees/agent-a47a2ae2 deleted file mode 160000 index 6e018092..00000000 --- a/.claude/worktrees/agent-a47a2ae2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e018092e187f6b59782e1f18874b673912c4ed2 diff --git a/.claude/worktrees/agent-a52204c8 b/.claude/worktrees/agent-a52204c8 deleted file mode 160000 index 6e018092..00000000 --- a/.claude/worktrees/agent-a52204c8 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e018092e187f6b59782e1f18874b673912c4ed2 diff --git a/.claude/worktrees/agent-a9fb7d7e b/.claude/worktrees/agent-a9fb7d7e deleted file mode 160000 index 6e018092..00000000 --- a/.claude/worktrees/agent-a9fb7d7e +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e018092e187f6b59782e1f18874b673912c4ed2 diff --git a/.claude/worktrees/agent-ab285578 b/.claude/worktrees/agent-ab285578 deleted file mode 160000 index cb9a2721..00000000 --- a/.claude/worktrees/agent-ab285578 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cb9a2721ff9f83a2af724d0fb009f98792770083 diff --git a/.claude/worktrees/agent-afb913a6 b/.claude/worktrees/agent-afb913a6 deleted file mode 160000 index 48e706ed..00000000 --- a/.claude/worktrees/agent-afb913a6 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 48e706ed534dfa3d3784267ea94191da8c89cda6 diff --git a/Sequel-Ace b/Sequel-Ace deleted file mode 160000 index 175ae467..00000000 --- a/Sequel-Ace +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 175ae46749d5c1d72b094e3b4d085a9b0dae15e2 diff --git a/docs/development/sequel-ace-learnings.md b/docs/development/sequel-ace-learnings.md deleted file mode 100644 index 3f696c2c..00000000 --- a/docs/development/sequel-ace-learnings.md +++ /dev/null @@ -1,874 +0,0 @@ -# What TablePro Can Learn from Sequel-Ace - -> Analysis date: 2026-03-22 | Source: Deep codebase analysis of Sequel-Ace v5.2.0 - -This document captures actionable features, patterns, and architectural ideas from Sequel-Ace that could improve TablePro. Each item includes implementation notes, effort estimates, and priority ranking. - ---- - -## Table of Contents - -1. [Advanced Field Editor](#1-advanced-field-editor) -2. [Template-Driven Filter Operator System](#2-template-driven-filter-operator-system) -3. [CSV Import with Field Mapping](#3-csv-import-with-field-mapping) -4. [Database Admin Tools](#4-database-admin-tools) -5. [Streaming Result & Lazy Conversion](#5-streaming-result--lazy-conversion) -6. [Export Pipeline Improvements](#6-export-pipeline-improvements) -7. [Script/Command Extensibility](#7-scriptcommand-extensibility) -8. [Geometry Data Visualization](#8-geometry-data-visualization) -9. [SSH Tunnel Improvements](#9-ssh-tunnel-improvements) -10. [Design Patterns Worth Adopting](#10-design-patterns-worth-adopting) -11. [Priority Matrix](#11-priority-matrix) -12. [Implementation Tracking](#12-implementation-tracking) - ---- - -## 1. Advanced Field Editor - -**Status**: Not started -**Effort**: Medium | **Impact**: High -**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPFieldEditorController.h/m` - -### Current State (TablePro) - -TablePro has basic cell editing — inline text editing in the data grid. No dedicated modal editor for complex data types. - -### What Sequel-Ace Does - -A 5-mode tabbed field editor sheet: - -#### 1.1 Hex Editor - -- Formatted hex dump: `ADDRESS HEX_BYTES ASCII_REPRESENTATION` (16 bytes/line) -- Non-printable bytes shown as `.` -- Bidirectional: binary data → hex display, hex input → binary data -- Supports MySQL `X'...'` syntax, `0x...` prefix, plain hex -- Lazy-loaded on demand (only renders when user clicks hex tab) - -**Sequel-Ace reference**: `Source/Other/CategoryAdditions/SPDataAdditions.m` lines 331-544 - -**Implementation notes for TablePro**: -- SwiftUI view with `Canvas` or monospaced `Text` grid -- `Data` extension for hex formatting/parsing -- Editable hex input field with validation -- Could use existing `NSViewRepresentable` pattern for performance on large BLOBs - -#### 1.2 Bit Field Editor - -- 64 individual toggle buttons (one per bit) -- Synchronized representations: decimal, hexadecimal, octal text fields -- Bit operations: Set All, Clear All, Negate, Shift Left/Right, Rotate Left/Right -- NULL support with dedicated toggle - -**Implementation notes for TablePro**: -- SwiftUI `Grid` layout with `Toggle` buttons (no need for 64 IBOutlets) -- `@State var bits: UInt64` as single source of truth -- Computed properties for decimal/hex/octal display -- Toolbar with operation buttons - -#### 1.3 JSON Formatter - -- Custom tokenizer preserving float precision and key ordering -- `NSJSONSerialization` rounds floats and reorders keys — Sequel-Ace avoids this -- Configurable indentation (tabs or 1-32 spaces) -- Format/unformat toggle (pretty-print ↔ compact) - -**Sequel-Ace reference**: `Source/Other/Parsing/SPJSONFormatter.h/m` - -**Implementation notes for TablePro**: -- Could use tree-sitter JSON grammar (already have tree-sitter via CESS) -- Or a custom Swift tokenizer that preserves numeric precision -- Integrate with existing CodeEditSourceEditor for syntax highlighting - -#### 1.4 Image/BLOB Preview with QuickLook - -- Detects file type from BLOB data (images, PDFs, audio, video, Word docs) -- Temp file creation + `QLPreviewPanel` for instant preview -- Drag-and-drop image import -- Paste from clipboard -- User-extensible type list via preferences (`EditorQuickLookTypes.plist`) - -**Sequel-Ace reference**: `Resources/EditorQuickLookTypes.plist` - -**Built-in preview types**: -| Type | Extension | -|------|-----------| -| Image | icns | -| Sound | m4a, mp3, wav | -| Movie | mov | -| PDF | pdf | -| HTML | html | -| Word | doc, docx | -| RTF | rtf | - -**Implementation notes for TablePro**: -- Use `QLPreviewPanel` (AppKit) or `QuickLookPreview` (SwiftUI, macOS 13+) -- File type detection via `UTType` from first N bytes (magic bytes) -- Temp file in `NSTemporaryDirectory()` with alternating names to avoid cache - -#### 1.5 Geometry Visualization - -See [Section 8](#8-geometry-data-visualization) for dedicated coverage. - -### Proposed TablePro Architecture - -``` -FieldEditorView (SwiftUI Sheet) -├── Picker: [Text, Hex, Image, JSON, Bit] -├── TextEditorTab -│ └── CodeEditSourceEditor (reuse existing) -├── HexEditorTab -│ └── HexDumpView (Canvas-based, monospaced) -├── ImagePreviewTab -│ └── QLPreviewPanel / QuickLookPreview -├── JSONEditorTab -│ └── CodeEditSourceEditor with JSON grammar -└── BitEditorTab - └── BitFieldGrid (SwiftUI Grid + Toggle) -``` - -**Data flow**: Cell double-click → detect type → open appropriate tab → edit → validate → return to DataChangeManager. - ---- - -## 2. Template-Driven Filter Operator System - -**Status**: Not started -**Effort**: Medium | **Impact**: High -**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPRuleFilterController.h/m`, `Resources/Plists/ContentFilters.plist` - -### Current State (TablePro) - -TablePro has basic text-based filtering. No type-aware operators, no visual rule builder, no nested AND/OR logic. - -### What Sequel-Ace Does - -#### 2.1 Operator Definition via Plist/JSON - -Each filter operator is a template: - -```json -{ - "MenuLabel": "contains", - "NumberOfArguments": 1, - "Clause": "LIKE $BINARY '%${}%'", - "ConjunctionLabels": [], - "Tooltip": "Searches for values containing the given text" -} -``` - -**Template placeholders**: -- `${}` → user argument (escaped, quoted) -- `$CURRENT_FIELD` → backtick-quoted column name -- `$BINARY` → `BINARY` keyword (if case-sensitive) or empty string - -**Type-to-operator mapping**: - -| Column Type | Operators | -|------------|-----------| -| `string` | =, ≠, LIKE, NOT LIKE, contains, starts with, ends with, REGEXP, IN, BETWEEN, IS NULL, IS NOT NULL, is empty | -| `number` | =, ≠, >, <, ≥, ≤, IN, LIKE, BETWEEN, IS NULL, IS NOT NULL | -| `date` | =, ≠, is after, is before, ≥, ≤, BETWEEN, IS NULL, IS NOT NULL | -| `spatial` | MBRContains, MBRWithin, MBRDisjoint, MBREqual, MBRIntersects, MBROverlaps, MBRTouches, IS NULL, IS NOT NULL | - -#### 2.2 User-Extensible - -- Custom operators saved globally (UserDefaults) or per-document -- Layering: bundled defaults → global custom → document custom -- Users can add operators without code changes - -#### 2.3 Visual Rule Builder (NSRuleEditor) - -- Nested AND/OR groups with visual indentation -- Per-rule enable/disable checkbox (tri-state hierarchy) -- Column selector → type-aware operator dropdown → argument fields -- Flattening pass eliminates redundant nesting before SQL generation - -#### 2.4 Quick Filter Table - -- Grid: one column per DB column, type filter directly -- Auto-detects operators from input: `">= 100"` → `field >= '100'`, `"NULL"` → `IS NULL` -- Same-row = AND, multiple-rows = OR -- DISTINCT and NEGATE toggles -- Live search mode (filter on every keystroke) -- Configurable default operator per session - -#### 2.5 SQL Generation Pipeline - -``` -Rule Editor rows - → Serialize to intermediate dict (filterClass, column, operator, values) - → Flatten (merge redundant AND/OR groups) - → Recursive SQL generation with proper parenthesization - → SPTableFilterParser handles escaping and placeholder substitution -``` - -### Proposed TablePro Architecture - -``` -FilterOperators/ -├── FilterOperatorDefinition.swift // Codable struct matching template format -├── FilterOperatorRegistry.swift // Loads from JSON, keyed by DatabaseType + column type -├── Dialects/ -│ ├── mysql-filters.json -│ ├── postgresql-filters.json // ILIKE, ~, ~*, SIMILAR TO, etc. -│ ├── sqlite-filters.json // GLOB, LIKE (case-insensitive by default) -│ ├── mongodb-filters.json // $regex, $gt, $lt, $in, etc. -│ └── redis-filters.json // Pattern matching (MATCH) -├── FilterRuleBuilder.swift // Visual rule builder (SwiftUI) -├── FilterQuickGrid.swift // Column-per-column quick filter -├── FilterSQLGenerator.swift // Template → SQL with proper escaping -└── FilterPresetManager.swift // Save/load named filter presets -``` - -**Key design decisions**: -- JSON files per database dialect (not one giant plist) -- `DatabaseType` extension provides `filterDialect` property -- Plugin-extensible: plugins can bundle their own filter JSON -- Backward-compatible: existing text filter becomes a "quick filter" mode - ---- - -## 3. CSV Import with Field Mapping - -**Status**: Not started -**Effort**: Medium | **Impact**: High -**Sequel-Ace reference**: `Source/Controllers/DataImport/SPFieldMapperController.h/m`, `Source/Controllers/DataImport/SPDataImport.m` - -### Current State (TablePro) - -TablePro has CSV export (plugin) but no CSV import with field mapping UI. - -### What Sequel-Ace Does - -#### 3.1 Field Mapper UI - -Visual column mapping interface: - -| Source CSV Column | → | Target Table Column | Operator | -|---|---|---|---| -| `email_address` | → | `email` | Import | -| `full_name` | → | `name` | Import | -| (none) | → | `created_at` | Global Value: `NOW()` | -| `old_id` | → | `id` | Match (for UPDATE) | -| `legacy_field` | → | (skip) | Do Not Import | - -**Alignment modes**: -- **By Name**: auto-match source→target by column name similarity -- **By Index**: map column 1→1, 2→2, etc. -- **Custom**: manual drag-and-drop or dropdown selection - -#### 3.2 Import Methods - -| Method | SQL | Use Case | -|--------|-----|----------| -| INSERT | `INSERT INTO ... VALUES (...)` | New rows | -| REPLACE | `REPLACE INTO ... VALUES (...)` | Upsert by primary key | -| UPDATE | `UPDATE ... SET ... WHERE match_col = match_val` | Update existing rows | - -**Advanced options**: IGNORE, DELAYED, LOW_PRIORITY, HIGH_PRIORITY, ON DUPLICATE KEY UPDATE - -#### 3.3 Global Values - -- SQL expressions applied to all rows: `NOW()`, `CURDATE()`, `UUID()` -- Column references: `$1 + $2` (source column 1 + column 2) -- `$N` syntax triggers SQL mode (expression not quoted) - -#### 3.4 Data Type Handling - -- **Geometry fields**: WKT parsing for spatial types -- **Bit fields**: integer conversion -- **Nullable numerics**: NULL handling for empty strings -- **New table mode**: creates table from CSV structure with editable column types/names - -#### 3.5 Streaming Import - -- 1MB chunk-based file reading -- Background thread for processing -- `SPCSVParser` for row/cell parsing -- Progress bar shows bytes processed vs total -- Error handling: Ask/Ignore/Abort on per-row errors - -### Proposed TablePro Architecture - -``` -ImportService/ -├── CSVImportController.swift // Main import coordinator -├── FieldMapperView.swift // SwiftUI field mapping UI -├── FieldMapping.swift // Mapping model (source index → target column) -├── ImportMethod.swift // INSERT/REPLACE/UPDATE enum -├── GlobalValueExpression.swift // SQL expression for unmapped columns -├── CSVChunkReader.swift // Streaming 1MB chunk reader -└── ImportProgressTracker.swift // Progress reporting -``` - -**Plugin integration**: Import could be a new plugin capability — `PluginDatabaseDriver` gains an optional `importRows(_:into:mapping:method:)` method with default implementation generating standard SQL. - ---- - -## 4. Database Admin Tools - -**Status**: Not started -**Effort**: Low-Medium | **Impact**: Medium - -### 4.1 Process List Viewer - -**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPProcessListController.h/m` (801 lines) - -**Features**: -- `SHOW [FULL] PROCESSLIST` with configurable auto-refresh (1s/5s/10s/30s/custom) -- Kill Query / Kill Connection (TiDB-aware: `KILL TIDB QUERY id`) -- Real-time filtering across all columns (Id, User, Host, Db, Command, Time, State, Info) -- Save process list to file -- Toggle Process ID column, toggle full process list mode - -**Implementation notes for TablePro**: -- Simple SwiftUI `Table` + `Timer.publish` for auto-refresh -- Add to `PluginDatabaseDriver` protocol: `func getProcessList() async throws -> [[String: String]]` -- Database-specific: MySQL (`SHOW PROCESSLIST`), PostgreSQL (`pg_stat_activity`), Redis (`CLIENT LIST`) -- Could be a floating panel or a tab in the main view - -**Estimated effort**: 2-3 days (UI + protocol method + MySQL/PostgreSQL implementations) - -### 4.2 Server Variables Inspector - -**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPServerVariablesController.h/m` (~350 lines) - -**Features**: -- `SHOW VARIABLES` with real-time search -- Copy name/value/both -- Save as `.cnf` file -- Read-only display - -**Implementation notes for TablePro**: -- Even simpler than Process List — just a searchable table -- Add to protocol: `func getServerVariables() async throws -> [(name: String, value: String)]` -- Database-specific: MySQL (`SHOW VARIABLES`), PostgreSQL (`SHOW ALL`), Redis (`CONFIG GET *`) - -**Estimated effort**: 1-2 days - -### 4.3 User/Role Manager - -**Sequel-Ace reference**: `Source/Controllers/SubviewControllers/SPUserManager.h/m` (>1000 lines) - -**Features**: -- Tree view: user → user@host children -- 4 tabs: General, Global Privileges, Resources, Schema Privileges -- Grant/Revoke SQL generation from checkbox matrix -- MySQL 5.7.6+ vs pre-5.7.6 password handling -- MariaDB-specific privilege mapping - -**Implementation notes for TablePro**: -- Complex, database-specific — lower priority -- Could be generalized: MySQL users/grants, PostgreSQL roles/privileges, Redis ACLs -- Significant effort per database type -- Consider: is this better as a dedicated admin tool or part of a database client? - -**Estimated effort**: 1-2 weeks (per database type) - -### 4.4 Database Copy/Rename - -**Sequel-Ace reference**: `Source/Other/DatabaseActions/SPDatabaseCopy.h/m`, `SPTableCopy.h/m` - -**Features**: -- Clone database: `SHOW CREATE TABLE` + `INSERT INTO ... SELECT` for each table -- FK-aware: disables `foreign_key_checks` during copy -- Table move via `ALTER TABLE ... RENAME` -- Preserves encoding/collation - -**Implementation notes for TablePro**: -- Add to protocol: `func copyDatabase(from:to:withContent:)`, `func renameDatabase(from:to:)` -- MySQL: straightforward with SHOW CREATE TABLE -- PostgreSQL: `CREATE DATABASE ... TEMPLATE source_db` (simpler) -- Could add to right-click context menu on database sidebar - -**Estimated effort**: 3-5 days - ---- - -## 5. Streaming Result & Lazy Conversion - -**Status**: Not started -**Effort**: High | **Impact**: Medium -**Sequel-Ace reference**: `Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.h/m`, `Source/Other/CategoryAdditions/SPDataStorage.h/m` - -### What Sequel-Ace Does - -#### 5.1 Three-Tier Streaming - -| Tier | Memory | Access | Use Case | -|------|--------|--------|----------| -| On-demand (`SPMySQLStreamingResult`) | O(1) | Sequential only | Simple queries | -| Buffered (`SPMySQLFastStreamingResult`) | O(k) | Sequential, freed after read | Large exports | -| Full cache (`SPMySQLStreamingResultStore`) | O(n) | Random O(1) | Data grid browsing | - -#### 5.2 Custom Malloc Zone - -```c -dataStorage = malloc_create_zone(64 * 1024, 0); // 64KB dedicated heap -// ... store all row data in this zone ... -malloc_destroy_zone(dataStorage); // Instant cleanup on reload -``` - -- All row data in isolated heap → zone-destroy on reload = instant cleanup (no per-object deallocation) -- Capacity doubling: 100 → 200 → 400 → ... rows - -#### 5.3 Variable-Size Row Metadata - -Dynamically chooses metadata width based on row data size: -- Row < 255 bytes → 1 byte per field offset (UCHAR) -- Row < 65535 bytes → 2 bytes per field offset (USHORT) -- Row ≥ 65535 bytes → 8 bytes per field offset (ULONG) - -Saves ~50% metadata overhead for typical small rows. - -#### 5.4 Lazy String Conversion - -``` -Raw MySQL row (char** pointers) - → stored as raw bytes + field lengths in result store - → NSString/NSData conversion ONLY when cell is accessed by UI -``` - -Avoids allocating String objects for cells never scrolled into view. - -#### 5.5 Sparse Edit Overlay (SPDataStorage) - -``` -cellDataAtRow:column: - if editedRows[rowIndex] exists: - return editedRows[rowIndex][columnIndex] // edited copy - else: - return streamingStore[rowIndex][columnIndex] // original data -``` - -- `NSPointerArray editedRows` — only stores rows that user has edited -- Copy-on-edit: first edit copies row from streaming store -- Unloaded columns return `SPNotLoaded` sentinel (for lazy BLOB loading) - -#### 5.6 Cached Method Pointers - -```c -static inline id SPMySQLResultStoreGetRow(SPMySQLStreamingResultStore* self, NSUInteger rowIndex) { - typedef id (*SPMSRSRowFetchMethodPtr)(...); - static SPMSRSRowFetchMethodPtr SPMSRSRowFetch; - if (!SPMSRSRowFetch) SPMSRSRowFetch = (SPMSRSRowFetchMethodPtr)[...]; - return SPMSRSRowFetch(...); -} -``` - -Bypasses Objective-C message dispatch in tight loops (data grid scrolling). - -### Applicability to TablePro - -TablePro's `RowBuffer` already handles some of this, but could benefit from: - -1. **Lazy string conversion** — store raw `Data` from database drivers, convert to `String` only when `DataGridView` requests the cell value. Biggest win for large result sets where user only scrolls through a fraction. - -2. **Malloc zone isolation** — for the RowBuffer backing store. `malloc_create_zone` + `malloc_destroy_zone` on tab switch/reload is faster than deallocating thousands of individual arrays. - -3. **Variable-width metadata** — if building a custom compact row format for RowBuffer. - -4. **Sparse edit tracking** — instead of copying entire row arrays on edit, only store the diff. TablePro's `DataChangeManager` already tracks changes but the underlying row data could be more memory-efficient. - -**Trade-off**: These are C-level optimizations that add complexity. Only worth it if profiling shows memory/performance issues with large result sets (100K+ rows). - ---- - -## 6. Export Pipeline Improvements - -**Status**: Not started -**Effort**: Medium | **Impact**: Medium -**Sequel-Ace reference**: `Source/Controllers/DataExport/SPExportController.h/m`, `Source/Controllers/DataExport/Exporters/` - -### What Sequel-Ace Does Better - -#### 6.1 NSOperation-Based Concurrent Export - -``` -SPExporter : NSOperation -├── SPCSVExporter -├── SPSQLExporter -├── SPXMLExporter -├── SPDotExporter -├── SPPDFExporter -└── SPHTMLExporter - -NSOperationQueue handles concurrent multi-table exports -``` - -Each exporter runs as an independent operation — multiple tables export simultaneously. - -#### 6.2 Streaming Export - -- Uses `SPMySQLFastStreamingResult` — never buffers full table in memory -- Row-by-row write to output file -- Progress tracked by rows processed vs total - -#### 6.3 Transparent Compression - -- `SPFileHandle` wraps file I/O with gzip/bzip2 support -- `setCompressionFormat:` before writing — compression happens at write time -- No separate compression step needed - -#### 6.4 Template-Based Filenames - -Tokens for multi-file export: -- `{database}` → current database name -- `{table}` → current table name -- `{date}` → export date -- `{time}` → export time -- `{host}` → connection host - -#### 6.5 Additional Export Formats - -Formats TablePro doesn't have: -- **XML** — structured data with schema information -- **Dot/GraphViz** — database relationship diagrams (ER diagrams) -- **PDF** — formatted table data for printing/sharing -- **HTML** — styled table data for web viewing - -### Proposed TablePro Improvements - -1. **Concurrent export** — use Swift structured concurrency (`TaskGroup`) for multi-table export -2. **Streaming** — add `exportRows(streaming:)` to plugin protocol, iterate without buffering -3. **Compression** — add gzip option to export UI (use `Foundation.Data.compress`) -4. **Filename templates** — for multi-table/multi-file exports -5. **Dot/ER diagram export** — generate relationship graphs from foreign key metadata - ---- - -## 7. Script/Command Extensibility - -**Status**: Not started -**Effort**: Medium | **Impact**: Medium -**Sequel-Ace reference**: `Source/Controllers/Other/SPBundleManager.h/m`, `Source/Other/Utility/SABundleRunner.h/m` - -### What Sequel-Ace Does - -#### 7.1 Bundle System - -Scripts stored as `.sequelbundle` directories: - -``` -MyCommand.sequelbundle/ -├── info.plist // metadata: name, scope, trigger, I/O config -└── command.sh // executable script -``` - -#### 7.2 Execution Context - -Scripts receive context via environment variables: -- `$SP_DATABASE_NAME` — current database -- `$SP_SELECTED_TABLE` — active table -- `$SP_QUERY_FILE` — path to temp file with current query/selection -- `$SP_QUERY_RESULT_FILE` — path to query result data -- `$SP_CURRENT_EDITED_COLUMN_NAME` — column being edited - -Input delivered via stdin redirect from temp file. - -#### 7.3 Output Disposition via Exit Code - -| Exit Code | Action | -|-----------|--------| -| 200 | No action | -| 201 | Replace selection | -| 202 | Replace all content | -| 203 | Insert as text | -| 205 | Show as HTML in floating window | -| 207 | Show as text tooltip | -| 208 | Show as HTML tooltip | - -#### 7.4 Scopes - -Scripts bound to execution contexts: -- **QueryEditor** — runs with editor selection/content -- **DataTable** — runs with table row data -- **InputField** — runs with field editor content -- **General** — runs with no specific context - -#### 7.5 HTML Output Window - -- WebKit-based floating window for rich script output -- Zoom, navigation, save, print support -- Scripts can generate charts, reports, formatted data - -### Proposed TablePro Architecture - -``` -CustomCommands/ -├── CommandDefinition.swift // name, scope, trigger, I/O config -├── CommandRunner.swift // NSTask/Process execution -├── CommandEnvironment.swift // Environment variable injection -├── CommandOutputHandler.swift // Exit code → action dispatch -├── CommandManagerView.swift // UI for managing commands -└── HTMLOutputPanel.swift // WKWebView floating window -``` - -**Modern improvements over Sequel-Ace**: -- Use `Process` (Swift) instead of `NSTask` (ObjC) -- JSON-based command definitions instead of plist -- Structured concurrency for async execution -- SwiftUI settings pane for command management -- Keyboard shortcut assignment via `KeyboardShortcut` - ---- - -## 8. Geometry Data Visualization - -**Status**: Not started -**Effort**: Low | **Impact**: Low (niche but differentiating) -**Sequel-Ace reference**: `Source/Views/SPGeometryDataView.h/m` - -### What Sequel-Ace Does - -Renders MySQL GEOMETRY types as visual diagrams: - -**Supported types**: POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, GEOMETRYCOLLECTION - -**Rendering**: -- Auto-scaling to fit target dimension (default 400px) -- 10px margin border -- Color scheme: Points (red fill + gray stroke), Lines (black), Polygons (alternating cyan/lime/red with 10% alpha fill) -- NSBezierPath-based drawing -- PDF export via `-pdfData` - -**Input**: WKT-parsed coordinate dictionary with `type`, `coordinates`, `bbox` keys. - -### Proposed TablePro Implementation - -```swift -struct GeometryView: View { - let geometry: GeometryData // parsed WKT - - var body: some View { - Canvas { context, size in - let transform = calculateTransform(bbox: geometry.bbox, targetSize: size) - switch geometry.type { - case .point: drawPoints(context, geometry.coordinates, transform) - case .lineString: drawLineString(context, geometry.coordinates, transform) - case .polygon: drawPolygon(context, geometry.coordinates, transform) - // ... etc - } - } - } -} -``` - -**Integration**: Show in field editor's Image tab when column type is geometry. Also useful as a cell renderer (thumbnail in data grid). - -**Database support**: MySQL (WKT/WKB), PostgreSQL/PostGIS (ST_AsText, ST_AsBinary), SQLite/SpatiaLite. - ---- - -## 9. SSH Tunnel Improvements - -**Status**: Not started -**Effort**: Low-Medium | **Impact**: Low-Medium -**Sequel-Ace reference**: `Source/Other/SSHTunnel/SPSSHTunnel.h/m`, `Source/Other/SSHTunnel/SequelAceTunnelAssistant.m` - -### What Sequel-Ace Does - -#### 9.1 Separate Tunnel Assistant Process - -- `SequelAceTunnelAssistant` is a standalone helper executable -- Acts as `SSH_ASKPASS` for interactive password/passphrase prompts -- Communicates with main app via `NSConnection` RPC (deprecated → use XPC) -- Checks Keychain first (silent auth), then prompts UI if needed - -#### 9.2 Connection Muxing - -- `ControlMaster=auto` with hashed control path -- Reuses SSH connections across multiple database connections to same host -- Disabled by default due to stability issues - -#### 9.3 Keepalive - -- `TCPKeepAlive=yes` -- `ServerAliveInterval` (configurable) -- `ServerAliveCountMax=3` - -#### 9.4 Port Allocation - -- Random local port via `getRandomPort()` -- Fallback port for host failover - -### Applicability to TablePro - -- **XPC Service** for SSH tunnel (modern replacement for NSConnection RPC) -- **Connection muxing** for users connecting to multiple databases on same host -- **Keepalive configuration** exposed in connection settings - ---- - -## 10. Design Patterns Worth Adopting - -### 10.1 Sparse Edit Overlay - -**Pattern**: Only copy/store rows that the user has edited. Unedited rows read directly from the underlying result store. - -**Sequel-Ace**: `NSPointerArray editedRows` — sparse array, only non-nil at edited indices. - -**TablePro application**: Could optimize `DataChangeManager` to avoid duplicating entire row arrays for single-cell edits. - -### 10.2 Template-Based SQL Generation - -**Pattern**: Define SQL fragments as templates with placeholders, interpolate at runtime. - -**Sequel-Ace**: `ContentFilters.plist` with `$CURRENT_FIELD`, `$BINARY`, `${}` placeholders. - -**TablePro application**: Filter operators, query builders, statement generators. Makes SQL generation database-agnostic and user-extensible. - -### 10.3 Tri-State Checkbox Hierarchy - -**Pattern**: Parent checkbox reflects aggregate state of children (all checked, all unchecked, mixed). - -**Sequel-Ace**: Filter rule enable/disable, with parent OR/AND groups showing mixed state. - -**TablePro application**: Filter preset management, multi-select operations, column visibility toggles. - -### 10.4 NSOperation Export Pipeline - -**Pattern**: Each export format is an `NSOperation` subclass. Queue handles concurrency and cancellation. - -**Sequel-Ace**: `SPExporter` base class → `SPCSVExporter`, `SPSQLExporter`, etc. - -**TablePro application**: Use Swift `Operation` subclasses or structured concurrency `TaskGroup` for concurrent multi-table export. - -### 10.5 Lazy Cell Conversion - -**Pattern**: Store raw bytes from database. Convert to String/Number only when UI requests the cell value. - -**Sequel-Ace**: C char arrays stored in malloc zone → NSString created on `cellDataAtRow:column:` call. - -**TablePro application**: Store `Data` in RowBuffer, convert to display type in `DataGridView` cell provider. Saves memory for cells never scrolled into view. - -### 10.6 Custom Malloc Zone - -**Pattern**: Allocate all result data in a dedicated malloc zone. Destroy zone on result reload for instant cleanup. - -**Sequel-Ace**: `malloc_create_zone(64 * 1024, 0)` per result store. - -**TablePro application**: Consider for RowBuffer backing store if profiling shows deallocation overhead for large result sets. - -### 10.7 Exit-Code-Driven Output Dispatch - -**Pattern**: Script exit code determines what happens with the output (replace selection, show as HTML, insert as text, etc.). - -**Sequel-Ace**: Exit codes 200-208 mapped to specific UI actions. - -**TablePro application**: For custom command/script extensibility system. - ---- - -## 11. Priority Matrix - -| # | Feature | Effort | Impact | Priority | Dependencies | -|---|---------|--------|--------|----------|-------------| -| 1 | Template-driven filter operators | Medium (1-2 weeks) | High | **P0** | None | -| 2 | Advanced field editor (hex/JSON/bit/QL) | Medium (1-2 weeks) | High | **P0** | None | -| 3 | CSV import with field mapping | Medium (1-2 weeks) | High | **P1** | Import plugin protocol | -| 4 | Process list viewer | Low (2-3 days) | Medium | **P1** | Driver protocol addition | -| 5 | Server variables inspector | Low (1-2 days) | Medium | **P2** | Driver protocol addition | -| 6 | Export pipeline (concurrent/streaming) | Medium (1 week) | Medium | **P2** | None | -| 7 | Database copy/rename | Low-Medium (3-5 days) | Medium | **P2** | Driver protocol addition | -| 8 | Script/command extensibility | Medium (1-2 weeks) | Medium | **P3** | None | -| 9 | Geometry visualization | Low (2-3 days) | Low | **P3** | WKT parser | -| 10 | Streaming/lazy conversion | High (2-3 weeks) | Medium | **P3** | RowBuffer refactor | -| 11 | SSH tunnel improvements | Low-Medium (3-5 days) | Low | **P4** | XPC service | -| 12 | User/role manager | High (1-2 weeks per DB) | Low-Medium | **P4** | Per-database implementation | - -**Priority key**: P0 = Next sprint, P1 = This quarter, P2 = Next quarter, P3 = Backlog, P4 = Nice-to-have - ---- - -## 12. Implementation Tracking - -### P0 — Next Sprint - -- [ ] **Template-driven filter operators** - - [ ] Define `FilterOperatorDefinition` Codable struct - - [ ] Create `mysql-filters.json`, `postgresql-filters.json`, `sqlite-filters.json` - - [ ] Implement `FilterOperatorRegistry` (loads JSON, keyed by DatabaseType + column type) - - [ ] Implement `FilterSQLGenerator` (template interpolation with escaping) - - [ ] Build `FilterRuleBuilderView` (SwiftUI, nested AND/OR groups) - - [ ] Build `FilterQuickGridView` (column-per-column fast filter) - - [ ] Integrate with existing filtering in `MainContentCoordinator+Filtering` - - [ ] Add filter preset save/load - - [ ] Add per-rule enable/disable checkboxes - - [ ] Plugin support: plugins can bundle their own filter JSON - -- [ ] **Advanced field editor** - - [ ] Design `FieldEditorView` (SwiftUI sheet with tab picker) - - [ ] Implement `HexEditorTab` (hex dump view + editable hex input) - - [ ] Implement `JSONEditorTab` (CodeEditSourceEditor with JSON grammar) - - [ ] Implement `BitFieldTab` (SwiftUI Grid with toggles + decimal/hex/octal sync) - - [ ] Implement `ImagePreviewTab` (QLPreviewPanel integration) - - [ ] Add file type detection from BLOB data (UTType magic bytes) - - [ ] Wire up to DataChangeManager for edit flow - - [ ] Double-click cell → detect type → open appropriate tab - -### P1 — This Quarter - -- [ ] **CSV import with field mapping** - - [ ] Design `FieldMapperView` (SwiftUI) - - [ ] Implement `CSVChunkReader` (streaming 1MB chunks) - - [ ] Implement field alignment modes (by name, by index, custom) - - [ ] Implement import methods (INSERT, REPLACE, UPDATE) - - [ ] Add global value expressions (`NOW()`, `$1 + $2`) - - [ ] Add progress tracking and error handling (ask/ignore/abort) - - [ ] Add to `PluginDatabaseDriver` protocol: `importRows` method - -- [ ] **Process list viewer** - - [ ] Add `getProcessList()` to `PluginDatabaseDriver` protocol - - [ ] Implement for MySQL (`SHOW PROCESSLIST`) - - [ ] Implement for PostgreSQL (`SELECT * FROM pg_stat_activity`) - - [ ] Implement for Redis (`CLIENT LIST`) - - [ ] Build `ProcessListView` (SwiftUI Table + auto-refresh timer) - - [ ] Add kill query/connection support - - [ ] Add filtering and save-to-file - -### P2 — Next Quarter - -- [ ] **Server variables inspector** - - [ ] Add `getServerVariables()` to `PluginDatabaseDriver` protocol - - [ ] Implement for MySQL, PostgreSQL, Redis - - [ ] Build `ServerVariablesView` (searchable SwiftUI Table) - -- [ ] **Export pipeline improvements** - - [ ] Add streaming export support to plugin protocol - - [ ] Implement concurrent multi-table export (TaskGroup) - - [ ] Add gzip compression option - - [ ] Add filename template system for multi-file export - -- [ ] **Database copy/rename** - - [ ] Add protocol methods - - [ ] Implement for MySQL, PostgreSQL - - [ ] Add to sidebar context menu - -### P3 — Backlog - -- [ ] **Script/command extensibility** - - [ ] Design command definition format (JSON) - - [ ] Implement `CommandRunner` (Process-based execution) - - [ ] Implement environment variable injection - - [ ] Implement exit-code-driven output dispatch - - [ ] Build command manager UI - - [ ] Add HTML output panel (WKWebView) - -- [ ] **Geometry visualization** - - [ ] Implement WKT parser - - [ ] Build `GeometryView` (SwiftUI Canvas) - - [ ] Integrate with field editor Image tab - - [ ] Support MySQL, PostGIS, SpatiaLite - -- [ ] **Streaming/lazy conversion** - - [ ] Profile current RowBuffer performance with 100K+ rows - - [ ] Prototype lazy string conversion (store Data, convert on access) - - [ ] Evaluate malloc zone isolation for RowBuffer - - [ ] Implement sparse edit overlay if profiling justifies - -### P4 — Nice-to-Have - -- [ ] **SSH tunnel improvements** (XPC service, connection muxing) -- [ ] **User/role manager** (MySQL grants, PostgreSQL roles, Redis ACLs) diff --git a/licenseapp b/licenseapp deleted file mode 160000 index 6a5d06b9..00000000 --- a/licenseapp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6a5d06b9f1c4c11e51f748065345311c589f2178 diff --git a/sequel-ace-vs-tablepro-analysis.md b/sequel-ace-vs-tablepro-analysis.md deleted file mode 100644 index db42e795..00000000 --- a/sequel-ace-vs-tablepro-analysis.md +++ /dev/null @@ -1,427 +0,0 @@ -# Sequel-Ace vs TablePro — Deep Comparative Analysis - -> Generated: 2026-03-22 | Sequel-Ace v5.2.0 | TablePro (current main) - ---- - -## 1. Project Overview - -| Aspect | Sequel-Ace | TablePro | -| ---------------- | ------------------------------------------ | -------------------------------------------------- | -| **Type** | macOS database client (MySQL/MariaDB only) | macOS database client (multi-database) | -| **Origin** | Fork of Sequel Pro (~20+ years lineage) | Built from scratch | -| **Language** | ~75% Objective-C, ~25% Swift | 100% Swift | -| **UI Framework** | AppKit + Interface Builder (XIB/NIB) | SwiftUI + AppKit interop | -| **Min macOS** | 12.0 (Monterey) | 14.0 (Sonoma) | -| **Architecture** | Universal Binary (arm64 + x86_64) | Universal Binary (arm64 + x86_64) | -| **Distribution** | Mac App Store + Homebrew (free) | Direct download + Sparkle auto-update (commercial) | -| **License** | MIT (open-source) | Proprietary (commercial) | - ---- - -## 2. Database Support - -| Database | Sequel-Ace | TablePro | -| ---------- | ------------------------------ | -------------------------------- | -| MySQL | Yes (native SPMySQL.framework) | Yes (plugin, CMariaDB) | -| MariaDB | Yes (via MySQL driver) | Yes (plugin, CMariaDB) | -| PostgreSQL | No | Yes (plugin, CLibPQ) | -| Redshift | No | Yes (via PostgreSQL plugin) | -| SQLite | No\* | Yes (plugin, Foundation sqlite3) | -| Redis | No | Yes (plugin, CRedis) | -| MongoDB | No | Yes (plugin, CLibMongoc) | -| ClickHouse | No | Yes (plugin, URLSession HTTP) | -| SQL Server | No | Yes (plugin, CFreeTDS) | -| Oracle | No | Yes (plugin, OracleNIO SPM) | -| DuckDB | No | Yes (plugin, CDuckDB) | - -\*Sequel-Ace uses FMDB/SQLite internally for query history storage, not as a user-facing database driver. - -**Key Difference:** Sequel-Ace is deeply specialized for MySQL/MariaDB. TablePro supports 11 database types through a modular plugin architecture. - ---- - -## 3. Architecture Comparison - -### 3.1 Application Architecture - -| Aspect | Sequel-Ace | TablePro | -| ------------------------ | --------------------------------------------- | -------------------------------------------------------- | -| **Pattern** | Classic MVC (Cocoa) | MVVM + Coordinator | -| **Document Model** | SPDatabaseDocument (monolithic, ~6,665 lines) | MainContentCoordinator (split across 7+ extension files) | -| **Window Model** | NSDocument-based, custom SPWindow | Native macOS window tabs (tabbingIdentifier) | -| **State Management** | NSNotificationCenter + delegates | Combine/ObservableObject + SwiftUI bindings | -| **Dependency Injection** | IBOutlets + manual wiring | Protocol-based + environment objects | - -### 3.2 Driver/Plugin Architecture - -**Sequel-Ace:** - -- Monolithic driver: SPMySQL.framework (custom Objective-C framework wrapping libmysqlclient) -- No plugin system for database drivers -- Bundle system exists but for user scripts/commands only (TextMate-style) -- QueryKit.framework for SQL query building (another custom framework) -- C libraries shipped as dynamic libraries (libmysqlclient.24.dylib, libssl.3.dylib, libcrypto.3.dylib) - -**TablePro:** - -- `.tableplugin` bundles loaded at runtime by PluginManager -- TableProPluginKit shared framework defines `PluginDatabaseDriver` protocol -- PluginDriverAdapter bridges plugin drivers to app's `DatabaseDriver` protocol -- C bridges per plugin (CMariaDB, CLibPQ, CFreeTDS, CLibMongoc, CRedis, CDuckDB) -- Static libraries (.a files) downloaded from GitHub Releases -- 11 Xcode targets (app + tests + PluginKit + 8 plugins) - -### 3.3 Query Editor - -**Sequel-Ace:** - -- `SPTextView` — Custom NSTextView subclass (~3,865 lines) -- Hand-built SQL syntax highlighting via NSTextStorage -- Custom autocomplete (SPNarrowDownCompletion) -- Bracket highlighting (SPBracketHighlighter) -- Code snippets with mirroring -- SQL tokenizer (SPSQLTokenizer, flex-based lexer SPEditorTokens.l) - -**TablePro:** - -- CodeEditSourceEditor (SPM, tree-sitter based) -- SQLEditorCoordinator bridges all features -- Tree-sitter syntax highlighting (handled by CESS) -- CompletionEngine with SQLCompletionAdapter -- SQLContextAnalyzer for context-aware completion -- Vim key interceptor, inline AI suggestions - -### 3.4 Data Grid - -**Sequel-Ace:** - -- `SPCopyTable` → `SPTableView` → NSTableView (Objective-C subclass chain) -- SPDataStorage wraps SPMySQLStreamingResultStore -- Inline cell editing with SPFieldEditorController -- Pagination via ContentPaginationViewController -- SPRuleFilterController for visual query builder - -**TablePro:** - -- DataGridView (NSTableView wrapped in SwiftUI) -- Identity-based update guard to prevent redundant reloads -- RowBuffer (class) avoids CoW on large arrays -- Generation counter pattern prevents out-of-order result flashes -- RowBuffer eviction keeps only 2 most recently-executed tabs in memory - ---- - -## 4. Feature Comparison - -### 4.1 Connection Management - -| Feature | Sequel-Ace | TablePro | -| ----------------------- | ------------------------------------------- | ------------------------------------------------------------ | -| Standard TCP/IP | Yes | Yes | -| Unix Socket | Yes | N/A (multi-db) | -| SSH Tunneling | Yes (full, with dedicated tunnel assistant) | Yes | -| SSL/TLS | Yes (certificate config) | Yes | -| AWS IAM Auth | Yes (with MFA token) | No | -| Keychain Storage | Yes | Yes (ConnectionStorage) | -| Color-coded Connections | Yes (7 colors) | Yes | -| Favorites/Groups | Yes (tree-based, SPFavoriteNode) | Yes | -| Connection Pooling | Yes | Yes (DatabaseManager) | -| Health Monitoring | Yes (keep-alive ping) | Yes (ConnectionHealthMonitor, 30s ping, exponential backoff) | -| Auto-reconnect | Yes (retry logic) | Yes (exponential backoff) | - -### 4.2 Query Features - -| Feature | Sequel-Ace | TablePro | -| ---------------------- | --------------------------------- | --------------------------------------------- | -| SQL Editor | Yes (SPTextView, ~3.8K lines) | Yes (CodeEditSourceEditor, tree-sitter) | -| Syntax Highlighting | Yes (NSTextStorage-based) | Yes (tree-sitter) | -| Autocomplete | Yes (SPNarrowDownCompletion) | Yes (CompletionEngine + SQLCompletionAdapter) | -| Bracket Matching | Yes (SPBracketHighlighter) | Yes (CESS built-in) | -| Query Favorites | Yes (SPQueryFavoriteManager) | Yes | -| Query History | Yes (SQLiteHistoryManager, FTS) | Yes (QueryHistoryStorage, SQLite FTS5) | -| Multi-query Execution | Yes (range detection) | Yes | -| MySQL Help Integration | Yes (showMySQLHelpForCurrentWord) | No | -| Code Snippets | Yes (with mirroring) | No | -| Vim Mode | No | Yes (VimKeyInterceptor) | -| AI Suggestions | No | Yes (InlineSuggestionManager) | -| Multi-cursor | No | Yes (CodeEditSourceEditor) | - -### 4.3 Data Viewing & Editing - -| Feature | Sequel-Ace | TablePro | -| ------------------------ | ---------------------------------------- | ---------------------------------- | -| Table Data Grid | Yes (SPCopyTable/NSTableView) | Yes (DataGridView/NSTableView) | -| Cell Editing | Yes (inline + sheet-based) | Yes (inline) | -| BLOB Handling | Yes (hex view, image preview, QuickLook) | Yes | -| JSON Formatting | Yes (SPJSONFormatter) | Yes | -| Geometry Data View | Yes (SPGeometryDataView) | No | -| Bit Field Editor | Yes (visual bit toggles) | No | -| Row Add/Duplicate/Delete | Yes | Yes | -| Pagination | Yes (ContentPaginationView) | Yes | -| Filtering | Yes (rule-based SPRuleFilterController) | Yes | -| Copy as CSV/SQL/Tab | Yes (SPCopyTable built-in) | Yes | -| Change Tracking | Manual | Yes (DataChangeManager, undo/redo) | - -### 4.4 Schema Management - -| Feature | Sequel-Ace | TablePro | -| ---------------------- | ------------------------------------------- | -------- | -| View Table Structure | Yes (SPTableStructure) | Yes | -| Edit Columns | Yes | Yes | -| Create Table | Yes | Yes | -| View CREATE Syntax | Yes | Yes | -| Triggers | Yes (SPTableTriggers) | Yes | -| Relations/Foreign Keys | Yes (SPTableRelations) | Yes | -| Indexes | Yes (SPIndexesController) | Yes | -| User Management | Yes (SPUserManager with Core Data) | No | -| Server Variables | Yes (SPServerVariablesController) | No | -| Process List | Yes (SPProcessListController, kill queries) | No | - -### 4.5 Export/Import - -| Feature | Sequel-Ace | TablePro | -| ----------- | --------------------------- | ---------------------------- | -| CSV Export | Yes | Yes (plugin) | -| JSON Export | Yes | Yes (plugin) | -| SQL Export | Yes (51KB implementation) | Yes (plugin) | -| XML Export | Yes | No | -| PDF Export | Yes | No | -| HTML Export | Yes | No | -| Dot Export | Yes (graph visualization) | No | -| XLSX Export | No | Yes (plugin) | -| CSV Import | Yes (with field mapping UI) | No (separate plugin planned) | -| SQL Import | No | Yes (plugin) | -| MQL Import | No | Yes (plugin, MongoDB) | - -### 4.6 Administrative Features - -| Feature | Sequel-Ace | TablePro | -| ----------------- | --------------------------- | -------- | -| User Management | Yes (full GRANT management) | No | -| Process List | Yes (with kill capability) | No | -| Server Variables | Yes (view/filter) | No | -| Database Copy | Yes (SPDatabaseCopy) | No | -| Database Rename | Yes (SPDatabaseRename) | No | -| Table Duplication | Yes (with data option) | No | -| Console/Query Log | Yes (SPConsoleMessage) | Yes | - -### 4.7 Unique to Each - -**Sequel-Ace Only:** - -- MySQL-specific admin tools (user management, process list, server variables) -- Bundle/script system (TextMate-style extensibility) -- AWS IAM/RDS authentication with MFA -- Geometry data visualization -- Bit field visual editor -- AppleScript support -- PDF/HTML/XML/Dot export -- CSV import with field mapping UI -- Database copy/rename operations -- 18-language localization - -**TablePro Only:** - -- Multi-database support (11 database types) -- Plugin architecture for extensible drivers -- SwiftUI-based modern UI -- Vim mode in editor -- AI-powered inline suggestions -- Multi-cursor editing (via CodeEditSourceEditor) -- Tree-sitter syntax highlighting -- Redis key tree navigation -- MongoDB query builder -- XLSX export -- Sparkle auto-update -- Tab persistence with snapshot/restore -- RowBuffer memory optimization (eviction policy) - ---- - -## 5. UI & Design - -### 5.1 UI Technology - -| Aspect | Sequel-Ace | TablePro | -| --------------------- | ---------------------------------------------------------- | ------------------------------------------------- | -| **Primary Framework** | AppKit + Interface Builder | SwiftUI + AppKit interop | -| **Interface Files** | 30 XIBs + 1 Storyboard | SwiftUI views (code-only) | -| **SwiftUI Usage** | None (pure AppKit) | Primary UI framework | -| **Custom Controls** | SPSplitView, SPWindow, SPTableView, SPTextView | DataGridView (NSTableView wrapper), SQLEditorView | -| **Theming** | NSAppearance (light/dark/system) + color assets (17 sets) | SQLEditorTheme + TableProEditorTheme adapter | -| **Editor Themes** | Customizable via preference pane (export/import/duplicate) | Built-in theme system | - -### 5.2 Window Architecture - -**Sequel-Ace:** - -- Single main window (MainWindow.xib, 1200x630 default) -- Horizontal SPSplitView: left panel (table navigator) + right panel (NSTabView with 7 tabs) -- Tabs: Structure, Content, Relations, Triggers, Custom Query, Indexes, Extended Info -- NSToolbar for navigation -- Preference window: toolbar-based 6-pane switcher - -**TablePro:** - -- Native macOS window tabs (each tab = separate NSWindow in tab group) -- MainContentView (SwiftUI) with sidebar + content area -- MainContentCoordinator split across 7+ extension files -- EditorTabBar (pure SwiftUI) for query tabs within each connection - -### 5.3 Localization - -| Aspect | Sequel-Ace | TablePro | -| ----------------- | -------------------------------------------------------------------------------- | ------------------------------------ | -| **Languages** | 18 (en, ar, cs, de, eo, es, fr, it, ja, pt, pt-BR, ru, tr, vi, zh-Hans, zh-Hant) | 2 (en, vi via Localizable.xcstrings) | -| **Format** | .strings files (~3,530 strings) | Localizable.xcstrings (Xcode 15+) | -| **Documentation** | N/A (app-only) | Mintlify docs (en + vi) | - ---- - -## 6. Build System & CI/CD - -### 6.1 Build Configuration - -| Aspect | Sequel-Ace | TablePro | -| --------------------- | ---------------------------------------------------------------- | -------------------------------------------------------- | -| **Project Type** | .xcodeproj (3 sub-projects) | .xcodeproj (11 targets) | -| **Targets** | 4 (app, tests, xibPostprocessor, tunnelAssistant) | 11 (app, tests, PluginKit, 8 plugins) | -| **SPM Dependencies** | 6 (Alamofire, AppCenter, FMDB, PLCrashReporter, SnapKit, OCMock) | 3 (CodeEditSourceEditor, Sparkle, OracleNIO) | -| **Custom Frameworks** | 3 (SPMySQL, QueryKit, ShortcutRecorder) | 1 (TableProPluginKit) | -| **C Libraries** | Dynamic (.dylib): libmysqlclient, libssl, libcrypto | Static (.a): libmariadb, libpq, etc. via download script | -| **pbxproj Version** | Standard | objectVersion 77 (filesystem-synced groups) | - -### 6.2 Testing - -| Aspect | Sequel-Ace | TablePro | -| ------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------- | -| **Test Files** | 32 (~6,288 lines) | Multiple (XCTest) | -| **Languages** | Mixed ObjC + Swift | Swift only | -| **Mock Framework** | OCMock | None (protocol-based mocking) | -| **Test Areas** | AWS auth, string utils, JSON formatting, table filtering, sorting, DB operations, SSL validation | Redis key tree, query builders, model tests | - -### 6.3 CI/CD - -| Aspect | Sequel-Ace | TablePro | -| ------------------- | ----------------------------------------------- | ----------------------------------------------- | -| **CI Platform** | GitHub Actions (macOS 15, Xcode 16.2) | GitHub Actions | -| **Trigger** | Pull requests | v\* tags | -| **Automation** | Fastlane (version bump, changelog, PR creation) | Shell scripts (build-release.sh, create-dmg.sh) | -| **Release** | App Store via Fastlane | DMG/ZIP + Sparkle signatures | -| **Crash Reporting** | AppCenter + PLCrashReporter | None (Sparkle only) | - -### 6.4 Code Quality - -| Aspect | Sequel-Ace | TablePro | -| ------------------------- | ----------------------------------------- | ------------------------------------------------------- | -| **Linter** | SwiftLint (lenient: file_length disabled) | SwiftLint (strict: warn 1200, error 1800) + SwiftFormat | -| **Function Body Limit** | 120 lines | 160 warn / 250 error | -| **Cyclomatic Complexity** | 20 | 40 warn / 60 error | -| **Line Length** | Disabled | 120 (SwiftFormat), 180 warn / 300 error (SwiftLint) | -| **Force Unwrap** | Opt-in warning | Banned | - ---- - -## 7. Code Quality & Technical Debt - -### 7.1 Sequel-Ace Pain Points - -1. **Massive Files:** - - SPDatabaseDocument.m: ~6,665 lines (god object) - - SPConnectionController.m: ~180KB - - SPCustomQuery.m: ~173KB / 3,870 lines - - SPTableContent.m: ~198KB - - SPTextView.m: ~3,865 lines - - DBView.xib: 542KB (monolithic interface file) - -2. **Mixed Language Complexity:** - - Bridging header required (Sequel-Ace-Bridging-Header.h) - - ObjC/Swift interop overhead - - 177 header files to maintain - -3. **Tightly Coupled Components:** - - SPDatabaseDocument manages everything (connection, views, toolbar, state, undo) - - IBOutlet-based wiring between components - - NSNotificationCenter for cross-component communication (hard to trace) - -4. **Legacy Patterns:** - - Manual memory management patterns (even with ARC) - - XIB-based UI (harder to diff, merge conflicts) - - ThirdParty/ directory with vendored code (RegexKitLite, MGTemplateEngine, etc.) - -### 7.2 TablePro Advantages - -1. **Clean Separation:** Plugin system isolates database-specific code -2. **Modern Swift:** No ObjC interop complexity, protocol-oriented -3. **SwiftUI:** Declarative UI, code-only (easy to diff/review) -4. **Coordinator Pattern:** Split across extension files, manageable sizes -5. **Performance-Conscious:** NSString O(1) length, RowBuffer eviction, generation counters - -### 7.3 Sequel-Ace Advantages - -1. **Feature Maturity:** 20+ years of MySQL-specific features -2. **Admin Tools:** User management, process list, server variables -3. **Localization:** 18 languages vs TablePro's 2 -4. **Battle-Tested:** Large community, extensive edge case handling -5. **Free/Open-Source:** MIT license, community contributions - ---- - -## 8. Codebase Statistics - -| Metric | Sequel-Ace | TablePro | -| ------------------------ | --------------------------------------- | ----------------------------------------------------------- | -| **Objective-C .m files** | ~160 | 0 | -| **Objective-C .h files** | ~177 | 0 | -| **Swift files** | ~52 | Majority | -| **XIB/Storyboard files** | 31 | 0 | -| **C bridge modules** | 0 (dynamic libs) | 6 (CMariaDB, CLibPQ, CFreeTDS, CLibMongoc, CRedis, CDuckDB) | -| **Frameworks (custom)** | 3 (SPMySQL, QueryKit, ShortcutRecorder) | 1 (TableProPluginKit) | -| **SPM dependencies** | 6 | 3 | -| **Xcode targets** | 4 | 11 | -| **Test files** | 32 | Multiple | -| **Supported databases** | 2 (MySQL, MariaDB) | 11 | -| **Supported languages** | 18 | 2 (en, vi) | - ---- - -## 9. What TablePro Can Learn from Sequel-Ace - -### 9.1 Features Worth Considering - -1. **MySQL Admin Tools** — User management, process list, server variable inspector -2. **Database Copy/Rename** — Administrative operations for supported databases -3. **Advanced BLOB Editing** — Hex view, bit field editor, geometry visualization -4. **Bundle/Script System** — User-extensible commands (TextMate-style) -5. **CSV Import with Field Mapping** — Visual column mapping UI -6. **AWS IAM Authentication** — For RDS connections -7. **More Export Formats** — PDF, HTML, XML for reporting -8. **AppleScript Support** — Automation for power users - -### 9.2 Patterns to Avoid - -1. **God objects** — SPDatabaseDocument (6,665 lines) manages everything -2. **Monolithic XIBs** — DBView.xib at 542KB is unmaintainable -3. **Mixed language** — ObjC/Swift bridging adds complexity without clear benefit -4. **Vendored dependencies** — ThirdParty/ directory with aging libraries -5. **Dynamic library shipping** — Static linking (TablePro approach) is more reliable -6. **Disabled linting rules** — file_length, line_length disabled defeats the purpose - -### 9.3 Architecture Lessons - -- TablePro's plugin system is far superior for multi-database support -- SwiftUI declarative UI is more maintainable than XIB-based approach -- Coordinator pattern with extensions (TablePro) scales better than monolithic document (Sequel-Ace) -- Static library linking (TablePro) is more portable than dynamic library shipping (Sequel-Ace) -- Protocol-oriented testing (TablePro) is simpler than OCMock-based mocking (Sequel-Ace) - ---- - -## 10. Summary - -**Sequel-Ace** is a mature, MySQL-specialized database client with deep admin capabilities and 20+ years of feature development. Its strength is MySQL feature completeness and community-driven localization (18 languages). Its weakness is architectural — a monolithic Objective-C codebase with massive god objects, tightly coupled components, and no database extensibility. - -**TablePro** is a modern, multi-database client built with clean architecture principles. Its strength is the modular plugin system (11 databases), modern Swift/SwiftUI stack, and performance-conscious design. It trades MySQL admin depth for breadth of database support and a more maintainable codebase. - -The projects target different market segments: Sequel-Ace serves MySQL power users who need deep admin tools (free), while TablePro serves developers who work across multiple databases and value modern UX (commercial). From 052723fec720e79f2ddeade50148016607e42e94 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 20:45:17 +0700 Subject: [PATCH 9/9] fix: remove stale main tag from CHANGELOG and document unused theme color --- CHANGELOG.md | 2 +- TablePro/Theme/ThemeDefinition.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9040eeb..e420bdc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Large document safety caps for syntax highlighting (skip >5MB, throttle >50KB) -## [0.23.2] - 2026-03-24 main +## [0.23.2] - 2026-03-24 ### Fixed diff --git a/TablePro/Theme/ThemeDefinition.swift b/TablePro/Theme/ThemeDefinition.swift index f7ff507a..3f586afa 100644 --- a/TablePro/Theme/ThemeDefinition.swift +++ b/TablePro/Theme/ThemeDefinition.swift @@ -177,6 +177,7 @@ internal struct EditorThemeColors: Codable, Equatable, Sendable { var selection: String var lineNumber: String var invisibles: String + /// Reserved for future current-statement background highlight in the query editor. var currentStatementHighlight: String var syntax: SyntaxColors