From b0291dc3efe14185404b71ed375b1c35053a0c11 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 12 Apr 2026 00:44:51 +0700 Subject: [PATCH 1/7] chore(ios): add missing translations for Turkish, Vietnamese, and Chinese --- .../TableProMobile/Localizable.xcstrings | 531 ++++++++++++++++++ 1 file changed, 531 insertions(+) diff --git a/TableProMobile/TableProMobile/Localizable.xcstrings b/TableProMobile/TableProMobile/Localizable.xcstrings index bbeaaffd..4e70d0dd 100644 --- a/TableProMobile/TableProMobile/Localizable.xcstrings +++ b/TableProMobile/TableProMobile/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, "%@" : { "localizations" : { "tr" : { @@ -311,6 +314,50 @@ } } }, + "All data in \"%@\" will be permanently deleted." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "\"%@\" içindeki tüm veriler kalıcı olarak silinecektir." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toàn bộ dữ liệu trong \"%@\" sẽ bị xóa vĩnh viễn." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "\"%@\"中的所有数据将被永久删除。" + } + } + } + }, + "All filter conditions will be removed." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm filtre koşulları kaldırılacaktır." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tất cả điều kiện lọc sẽ bị xóa." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有筛选条件将被移除。" + } + } + } + }, "An unknown error occurred." : { "extractionState" : "stale", "localizations" : { @@ -378,6 +425,28 @@ } } }, + "Are you sure you want to delete this connection? Saved credentials will be permanently removed." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu bağlantıyı silmek istediğinizden emin misiniz? Kayıtlı kimlik bilgileri kalıcı olarak kaldırılacaktır." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có chắc muốn xóa kết nối này? Thông tin đăng nhập đã lưu sẽ bị xóa vĩnh viễn." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定要删除此连接吗?已保存的凭据将被永久移除。" + } + } + } + }, "Are you sure you want to delete this row? This action cannot be undone." : { "localizations" : { "tr" : { @@ -819,6 +888,28 @@ } } }, + "Clear Query" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sorguyu Temizle" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa truy vấn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除查询" + } + } + } + }, "Color" : { "localizations" : { "tr" : { @@ -1040,6 +1131,28 @@ } } }, + "Connections in this group will be moved to ungrouped." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu gruptaki bağlantılar grupsuz bölümüne taşınacaktır." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Các kết nối trong nhóm này sẽ được chuyển sang mục chưa phân nhóm." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此分组中的连接将被移至未分组。" + } + } + } + }, "Continue" : { "localizations" : { "tr" : { @@ -1084,6 +1197,28 @@ } } }, + "Copy Name" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adı Kopyala" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép tên" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "拷贝名称" + } + } + } + }, "Copy Results" : { "localizations" : { "tr" : { @@ -1128,6 +1263,28 @@ } } }, + "Copy to Clipboard" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Panoya Kopyala" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép vào bộ nhớ tạm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "拷贝到剪贴板" + } + } + } + }, "Copy Value" : { "localizations" : { "tr" : { @@ -1414,6 +1571,50 @@ } } }, + "Delete Connection" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantıyı Sil" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa kết nối" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除连接" + } + } + } + }, + "Delete Group" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grubu Sil" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa nhóm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除分组" + } + } + } + }, "Delete Row" : { "localizations" : { "tr" : { @@ -1480,6 +1681,50 @@ } } }, + "Drop" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaldır" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa bảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除" + } + } + } + }, + "Drop Table" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabloyu Kaldır" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa bảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除表" + } + } + } + }, "Duplicate" : { "localizations" : { "tr" : { @@ -1766,6 +2011,28 @@ } } }, + "Export" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dışa Aktar" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出" + } + } + } + }, "Failed to load more rows" : { "extractionState" : "stale", "localizations" : { @@ -1878,6 +2145,28 @@ } } }, + "Filter" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtre" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bộ lọc" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选" + } + } + } + }, "Filter by Tag" : { "localizations" : { "tr" : { @@ -3446,6 +3735,28 @@ } } }, + "Query text and results will be cleared." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sorgu metni ve sonuçları temizlenecektir." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nội dung truy vấn và kết quả sẽ bị xóa." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询文本和结果将被清除。" + } + } + } + }, "read-only" : { "localizations" : { "tr" : { @@ -3468,6 +3779,28 @@ } } }, + "Reconnecting..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeniden bağlanıyor..." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang kết nối lại..." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在重新连接..." + } + } + } + }, "Reload" : { "localizations" : { "tr" : { @@ -3688,6 +4021,28 @@ } } }, + "Search all columns" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm sütunlarda ara" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm kiếm tất cả cột" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索所有列" + } + } + } + }, "Search tables" : { "localizations" : { "tr" : { @@ -3865,6 +4220,72 @@ } } }, + "Share" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paylaş" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chia sẻ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "共享" + } + } + } + }, + "Share Results" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sonuçları Paylaş" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chia sẻ kết quả" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "共享结果" + } + } + } + }, + "Share Row" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satırı Paylaş" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chia sẻ dòng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "共享行" + } + } + } + }, "Skip" : { "localizations" : { "tr" : { @@ -3909,6 +4330,28 @@ } } }, + "Sort" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sırala" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sắp xếp" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "排序" + } + } + } + }, "Sort By" : { "localizations" : { "tr" : { @@ -4196,6 +4639,28 @@ } } }, + "Table Structure" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tablo Yapısı" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cấu trúc bảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表结构" + } + } + } + }, "Tables" : { "localizations" : { "tr" : { @@ -4394,6 +4859,28 @@ } } }, + "The table \"%@\" and all its data will be permanently deleted." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "\"%@\" tablosu ve tüm verileri kalıcı olarak silinecektir." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng \"%@\" và toàn bộ dữ liệu của nó sẽ bị xóa vĩnh viễn." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表\"%@\"及其所有数据将被永久删除。" + } + } + } + }, "The table or column does not exist." : { "localizations" : { "tr" : { @@ -4571,6 +5058,50 @@ } } }, + "Truncate" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabloyu Boşalt" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa dữ liệu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空" + } + } + } + }, + "Truncate Table" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabloyu Boşalt" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa dữ liệu bảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空表" + } + } + } + }, "Ungrouped" : { "localizations" : { "tr" : { From 51b45ca2a7d16287ba5b2a528e867df634bc55db Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 12 Apr 2026 00:50:05 +0700 Subject: [PATCH 2/7] fix: show database-specific options in Create Database dialog --- CHANGELOG.md | 1 + .../MySQLDriverPlugin/MySQLPluginDriver.swift | 4 +- .../PostgreSQLPluginDriver.swift | 3 +- .../RedshiftPluginDriver.swift | 3 +- .../Models/Schema/CreateDatabaseOptions.swift | 70 +++++++++++++++ .../CreateDatabaseSheet.swift | 86 ++++++++++--------- .../DatabaseSwitcherSheet.swift | 6 +- 7 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 TablePro/Models/Schema/CreateDatabaseOptions.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 93aed948..b13166d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Create Database dialog showing MySQL charset/collation options for all database types; now shows database-specific options (encoding/LC_COLLATE for PostgreSQL, hidden for Redis/etcd) - SSH Tunnel not working with `~/.ssh/config` profiles (#672): added `Include` directive support, SSH token expansion (`%d`, `%h`, `%u`, `%r`), multi-word `Host` filtering, and detailed handshake error messages ## [0.30.1] - 2026-04-10 diff --git a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift index 8826fb8e..8a2e3d53 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift @@ -560,7 +560,9 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { func createDatabase(name: String, charset: String, collation: String?) async throws { let escapedName = name.replacingOccurrences(of: "`", with: "``") - let validCharsets = ["utf8mb4", "utf8", "latin1", "ascii"] + let validCharsets = ["utf8mb4", "utf8mb3", "utf8", "latin1", "ascii", + "binary", "utf16", "utf32", "cp1251", "big5", + "euckr", "gb2312", "gbk", "sjis"] guard validCharsets.contains(charset) else { throw MariaDBPluginError(code: 0, message: "Invalid character set: \(charset)", sqlState: nil) } diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift index fc29e40b..264f459a 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift @@ -765,7 +765,8 @@ final class PostgreSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { func createDatabase(name: String, charset: String, collation: String?) async throws { let escapedName = name.replacingOccurrences(of: "\"", with: "\"\"") - let validCharsets = ["UTF8", "LATIN1", "SQL_ASCII"] + let validCharsets = ["UTF8", "LATIN1", "SQL_ASCII", "WIN1252", "EUC_JP", + "EUC_KR", "ISO_8859_5", "KOI8R", "SJIS", "BIG5", "GBK"] let normalizedCharset = charset.uppercased() guard validCharsets.contains(normalizedCharset) else { throw LibPQPluginError(message: "Invalid encoding: \(charset)", sqlState: nil, detail: nil) diff --git a/Plugins/PostgreSQLDriverPlugin/RedshiftPluginDriver.swift b/Plugins/PostgreSQLDriverPlugin/RedshiftPluginDriver.swift index ff23d618..41f79f74 100644 --- a/Plugins/PostgreSQLDriverPlugin/RedshiftPluginDriver.swift +++ b/Plugins/PostgreSQLDriverPlugin/RedshiftPluginDriver.swift @@ -621,7 +621,8 @@ final class RedshiftPluginDriver: PluginDatabaseDriver, @unchecked Sendable { func createDatabase(name: String, charset: String, collation: String?) async throws { let escapedName = name.replacingOccurrences(of: "\"", with: "\"\"") - let validCharsets = ["UTF8", "LATIN1", "SQL_ASCII"] + let validCharsets = ["UTF8", "LATIN1", "SQL_ASCII", "WIN1252", "EUC_JP", + "EUC_KR", "ISO_8859_5", "KOI8R", "SJIS", "BIG5", "GBK"] let normalizedCharset = charset.uppercased() guard validCharsets.contains(normalizedCharset) else { throw LibPQPluginError(message: "Invalid encoding: \(charset)", sqlState: nil, detail: nil) diff --git a/TablePro/Models/Schema/CreateDatabaseOptions.swift b/TablePro/Models/Schema/CreateDatabaseOptions.swift new file mode 100644 index 00000000..1d9ff1f2 --- /dev/null +++ b/TablePro/Models/Schema/CreateDatabaseOptions.swift @@ -0,0 +1,70 @@ +// +// CreateDatabaseOptions.swift +// TablePro +// +// Database-type-specific options for CREATE DATABASE dialog. +// + +import Foundation + +struct CreateDatabaseOptions { + struct Config { + let charsetLabel: String + let collationLabel: String + let defaultCharset: String + let defaultCollation: String + let charsets: [String] + let collations: [String: [String]] + let showOptions: Bool + } + + static func config(for type: DatabaseType) -> Config { + if type == .mysql || type == .mariadb { + return Config( + charsetLabel: "Character Set", + collationLabel: "Collation", + defaultCharset: "utf8mb4", + defaultCollation: "utf8mb4_unicode_ci", + charsets: CreateTableOptions.charsets, + collations: CreateTableOptions.collations, + showOptions: true + ) + } else if type == .postgresql || type == .redshift { + return Config( + charsetLabel: "Encoding", + collationLabel: "LC_COLLATE", + defaultCharset: "UTF8", + defaultCollation: "en_US.UTF-8", + charsets: postgresqlEncodings, + collations: postgresqlLocales, + showOptions: true + ) + } else { + return Config( + charsetLabel: "", + collationLabel: "", + defaultCharset: "", + defaultCollation: "", + charsets: [], + collations: [:], + showOptions: false + ) + } + } + + private static let postgresqlEncodings = [ + "UTF8", "LATIN1", "SQL_ASCII", "WIN1252", "EUC_JP", + "EUC_KR", "ISO_8859_5", "KOI8R", "SJIS", "BIG5", "GBK" + ] + + // PostgreSQL LC_COLLATE is OS-locale based, not encoding-dependent + private static let localeOptions = ["en_US.UTF-8", "C", "POSIX", "C.UTF-8"] + + private static let postgresqlLocales: [String: [String]] = { + var result: [String: [String]] = [:] + for enc in postgresqlEncodings { + result[enc] = localeOptions + } + return result + }() +} diff --git a/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift b/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift index cdd4c14a..7c79ad9f 100644 --- a/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift +++ b/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift @@ -10,27 +10,25 @@ import SwiftUI struct CreateDatabaseSheet: View { @Environment(\.dismiss) private var dismiss + let databaseType: DatabaseType let onCreate: (String, String, String?) async throws -> Void @State private var databaseName = "" - @State private var charset = "utf8mb4" - @State private var collation = "utf8mb4_unicode_ci" + @State private var charset: String + @State private var collation: String @State private var isCreating = false @State private var errorMessage: String? - private let charsets = [ - "utf8mb4", - "utf8", - "latin1", - "ascii" - ] + private let config: CreateDatabaseOptions.Config - private let collations: [String: [String]] = [ - "utf8mb4": ["utf8mb4_unicode_ci", "utf8mb4_general_ci", "utf8mb4_bin"], - "utf8": ["utf8_unicode_ci", "utf8_general_ci", "utf8_bin"], - "latin1": ["latin1_swedish_ci", "latin1_general_ci", "latin1_bin"], - "ascii": ["ascii_general_ci", "ascii_bin"] - ] + init(databaseType: DatabaseType, onCreate: @escaping (String, String, String?) async throws -> Void) { + self.databaseType = databaseType + self.onCreate = onCreate + let cfg = CreateDatabaseOptions.config(for: databaseType) + self.config = cfg + self._charset = State(initialValue: cfg.defaultCharset) + self._collation = State(initialValue: cfg.defaultCollation) + } var body: some View { VStack(spacing: 0) { @@ -54,36 +52,38 @@ struct CreateDatabaseSheet: View { .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) } - // Charset - VStack(alignment: .leading, spacing: 6) { - Text("Character Set") - .font(.system(size: ThemeEngine.shared.activeTheme.typography.small, weight: .medium)) - .foregroundStyle(.secondary) - - Picker("", selection: $charset) { - ForEach(charsets, id: \.self) { cs in - Text(cs).tag(cs) + if config.showOptions { + // Charset / Encoding + VStack(alignment: .leading, spacing: 6) { + Text(config.charsetLabel) + .font(.system(size: ThemeEngine.shared.activeTheme.typography.small, weight: .medium)) + .foregroundStyle(.secondary) + + Picker("", selection: $charset) { + ForEach(config.charsets, id: \.self) { cs in + Text(cs).tag(cs) + } } + .labelsHidden() + .pickerStyle(.menu) + .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) } - .labelsHidden() - .pickerStyle(.menu) - .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) - } - // Collation - VStack(alignment: .leading, spacing: 6) { - Text("Collation") - .font(.system(size: ThemeEngine.shared.activeTheme.typography.small, weight: .medium)) - .foregroundStyle(.secondary) + // Collation / LC_COLLATE + VStack(alignment: .leading, spacing: 6) { + Text(config.collationLabel) + .font(.system(size: ThemeEngine.shared.activeTheme.typography.small, weight: .medium)) + .foregroundStyle(.secondary) - Picker("", selection: $collation) { - ForEach(collations[charset] ?? [], id: \.self) { col in - Text(col).tag(col) + Picker("", selection: $collation) { + ForEach(config.collations[charset] ?? [], id: \.self) { col in + Text(col).tag(col) + } } + .labelsHidden() + .pickerStyle(.menu) + .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) } - .labelsHidden() - .pickerStyle(.menu) - .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) } // Error message @@ -116,14 +116,12 @@ struct CreateDatabaseSheet: View { } .frame(width: 380) .onExitCommand { - // Prevent dismissing the sheet via ESC while a database is being created if !isCreating { dismiss() } } .onChange(of: charset) { _, newCharset in - // Update collation when charset changes - if let firstCollation = collations[newCharset]?.first { + if let firstCollation = config.collations[newCharset]?.first { collation = firstCollation } } @@ -137,7 +135,11 @@ struct CreateDatabaseSheet: View { Task { do { - try await onCreate(databaseName, charset, collation) + if config.showOptions { + try await onCreate(databaseName, charset, collation) + } else { + try await onCreate(databaseName, "", nil) + } await MainActor.run { dismiss() } diff --git a/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift b/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift index 89f4b15d..1624e66a 100644 --- a/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift +++ b/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift @@ -116,7 +116,7 @@ struct DatabaseSwitcherSheet: View { .defaultFocus($focus, .search) .task { await viewModel.fetchDatabases() } .sheet(isPresented: $showCreateDialog) { - CreateDatabaseSheet { name, charset, collation in + CreateDatabaseSheet(databaseType: databaseType) { name, charset, collation in try await viewModel.createDatabase( name: name, charset: charset, collation: collation) await viewModel.refreshDatabases() @@ -174,7 +174,9 @@ struct DatabaseSwitcherSheet: View { .help(String(localized: "Refresh database list")) // Create (only for non-SQLite) - if databaseType != .sqlite && !isSchemaMode { + if databaseType != .sqlite && databaseType != .redis + && databaseType != .etcd && !isSchemaMode + { Button(action: { showCreateDialog = true }) { Image(systemName: "plus") .frame(width: 24, height: 24) From dbdb2a255217368b3a04e2ef2c1e31e34bfc1c47 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 12 Apr 2026 00:55:51 +0700 Subject: [PATCH 3/7] fix: sidebar loading spinner stuck when database has no tables --- TablePro/Views/Sidebar/SidebarView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TablePro/Views/Sidebar/SidebarView.swift b/TablePro/Views/Sidebar/SidebarView.swift index 52cacce8..876e768e 100644 --- a/TablePro/Views/Sidebar/SidebarView.swift +++ b/TablePro/Views/Sidebar/SidebarView.swift @@ -159,7 +159,7 @@ struct SidebarView: View { } private var hasActiveConnection: Bool { - viewModel.isLoading || DatabaseManager.shared.driver(for: connectionId) != nil + viewModel.isLoading } private var loadingState: some View { From f7b59419e697ddb2fec80e5be7361d592311ff97 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 12 Apr 2026 01:02:37 +0700 Subject: [PATCH 4/7] fix: stale tables showing briefly after database switch --- .../Main/Extensions/MainContentCoordinator+Navigation.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index 7fac271b..a8ee1028 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -431,7 +431,8 @@ extension MainContentCoordinator { } AppSettingsStorage.shared.saveLastDatabase(database, for: connectionId) await loadSchema() - reloadSidebar() + await schemaProvider.invalidateTables() + sidebarViewModel?.forceLoadTables() } catch { // Restore toolbar to previous database on failure toolbarState.databaseName = previousDatabase From eaf7f75433d89d778f15ceb57f3bbf7b216f8fe4 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 12 Apr 2026 01:06:50 +0700 Subject: [PATCH 5/7] fix: prevent stale tables flash during database switch --- TablePro/Views/Main/MainContentCommandActions.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index 8c6f179a..11cef1bb 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -711,7 +711,11 @@ final class MainContentCommandActions { if let driver = DatabaseManager.shared.driver(for: self.connection.id) { coordinator?.toolbarState.databaseVersion = driver.serverVersion } - coordinator?.reloadSidebar() + // Skip sidebar reload during database switch — switchDatabase() handles it + // after schema invalidation to avoid flashing stale tables. + if coordinator?.isSwitchingDatabase != true { + coordinator?.reloadSidebar() + } coordinator?.initRedisKeyTreeIfNeeded() } } From ecda97685a70df2ee7c3ad6c318a471244091503 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 12 Apr 2026 01:09:55 +0700 Subject: [PATCH 6/7] fix: skip auto-reload from stale cache during database switch --- TablePro/Views/Sidebar/SidebarView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TablePro/Views/Sidebar/SidebarView.swift b/TablePro/Views/Sidebar/SidebarView.swift index 876e768e..d3eecdab 100644 --- a/TablePro/Views/Sidebar/SidebarView.swift +++ b/TablePro/Views/Sidebar/SidebarView.swift @@ -117,7 +117,9 @@ struct SidebarView: View { } .onChange(of: tables) { _, newTables in let hasSession = DatabaseManager.shared.activeSessions[connectionId] != nil - if newTables.isEmpty && hasSession && !viewModel.isLoading { + if newTables.isEmpty && hasSession && !viewModel.isLoading + && coordinator?.isSwitchingDatabase != true + { viewModel.loadTables() } } From 4ade5d1430e18fbcf36a8c733b8c3fd892c20fc7 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 12 Apr 2026 01:27:37 +0700 Subject: [PATCH 7/7] fix(ios): prevent crash in SQL syntax highlighter with stale range The highlight method is called asynchronously from the text storage delegate. By the time it executes, the text may have changed, making the editedRange invalid (location beyond string length). Add bounds check and clamp the range before calling lineRangeForRange. Fixes crash: -[NSString lineRangeForRange:] out-of-bounds exception --- .../Views/Components/SQLSyntaxHighlighter.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/TableProMobile/TableProMobile/Views/Components/SQLSyntaxHighlighter.swift b/TableProMobile/TableProMobile/Views/Components/SQLSyntaxHighlighter.swift index b5dd2417..1e62855e 100644 --- a/TableProMobile/TableProMobile/Views/Components/SQLSyntaxHighlighter.swift +++ b/TableProMobile/TableProMobile/Views/Components/SQLSyntaxHighlighter.swift @@ -64,16 +64,22 @@ enum SQLSyntaxHighlighter { static func highlight(_ textStorage: NSTextStorage, in editedRange: NSRange) { let fullLength = textStorage.length guard fullLength > 0 else { return } + guard editedRange.location < fullLength else { return } let cappedLength = min(fullLength, maxHighlightLength) let nsString = textStorage.string as NSString + let safeEditedRange = NSRange( + location: editedRange.location, + length: min(editedRange.length, fullLength - editedRange.location) + ) + let highlightRange: NSRange - if editedRange.location == 0 && editedRange.length >= cappedLength { + if safeEditedRange.location == 0 && safeEditedRange.length >= cappedLength { highlightRange = NSRange(location: 0, length: cappedLength) } else { - let lineStart = nsString.lineRange(for: NSRange(location: editedRange.location, length: 0)).location - let editEnd = min(NSMaxRange(editedRange), cappedLength) + let lineStart = nsString.lineRange(for: NSRange(location: safeEditedRange.location, length: 0)).location + let editEnd = min(NSMaxRange(safeEditedRange), cappedLength) let lineEnd = NSMaxRange(nsString.lineRange(for: NSRange(location: max(editEnd - 1, 0), length: 0))) highlightRange = NSRange(location: lineStart, length: min(lineEnd - lineStart, cappedLength - lineStart)) }