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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Open in Agent Mode** on a connection in the welcome window.
- **Outside MCP Servers** in Settings > Integrations, letting a session call tools on an MCP server you run.
- Per-connection allowlist for an outside MCP server, with its token in the Keychain and neither synced.
- Welcome sheet on iPhone and iPad, asking once about iCloud sync and usage data.
- Sample database on iPhone and iPad, opened from the empty connection list or the **More** menu.
- What's New on iPhone and iPad after an update, and under **Settings > About**.
- Tips in the iOS connection list for swiping to favorite, touch and hold, and tag search.
- Acknowledgements and a privacy policy link under **Settings > About** on iPhone and iPad.
- Privacy manifest for the iOS app.

### Changed

- 537 driver and import/export strings are now translatable, having only ever shown in English.
- Every plugin bundle compiled under the same concurrency settings as the app that loads it.
- Release C optimization and link-time optimization scoped to the app, not to its Swift package dependencies.
- Assistant conversations belong to one connection, and outlive the window that opened them.
- iCloud sync and usage data on iPhone and iPad stay off until you turn them on.
- Group rows in the iOS connection list take swipe actions and show even when no connection is saved.

### Removed

- **Refresh from iCloud**, **Sync Now** and the toolbar sync button on iPhone and iPad.
- **Manage Groups**, the **Clear** button on **Recent**, and the **More** menu's tag filter on iPhone and iPad.

### Fixed

Expand Down Expand Up @@ -82,13 +95,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Rows saved, added or deleted on iPhone and iPad refused by a MySQL, MariaDB, PostgreSQL or Redshift server that starts sessions read-only.
- Copying objects into a connection that already has a transaction open committing it.
- Replace-copy into a remote libSQL target failing at `BEGIN`.
- iOS edits uploaded to iCloud after iCloud Sync was turned off.
- iOS text left in English for Korean, Vietnamese, Simplified Chinese and Traditional Chinese.
- iOS connections overwritten by the next edit after the saved library failed to load.
- A `.tablepro` file or link closing a half-filled form on iPhone and iPad.
- Links to a table opening only its connection on iPhone and iPad.
- Face ID symbol on the unlock button of Touch ID and Optic ID devices.
- Empty icon tiles for Snowflake, Beancount, SurrealDB and Kafka connections on iPhone and iPad.
- iOS edit mode left on after deleting every connection.
- Two rename fields when renaming a favorite on iPhone and iPad.
- Connection names cut off at large text sizes on iPhone and iPad.
- `Cmd+W` closing the whole connection instead of the current tab until something in the window was clicked.

### Security

- Code inside a plugin bundle, and its resource envelope, were not verified before the bundle was loaded.
- The system log carried query text, schema and table names, file paths and driver error messages, which can hold row values.
- A chat tool registered at runtime could take the name of a tool TablePro ships.
- An open connection, a sheet and the app switcher preview left usable or visible behind the iOS app lock.
- **Require Face ID** turned off on iPhone and iPad without authenticating.

## [0.75.0] - 2026-09-18

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public struct DatabaseConnection: Identifiable, Hashable, Sendable {
public var tagIds: [UUID]
public var sortOrder: Int
public var isFavorite: Bool
public var isSample: Bool

public var participatesInSync: Bool {
!isSample
}

public var tagId: UUID? {
get { tagIds.first }
Expand All @@ -55,7 +60,8 @@ public struct DatabaseConnection: Identifiable, Hashable, Sendable {
groupId: UUID? = nil,
tagIds: [UUID] = [],
sortOrder: Int = 0,
isFavorite: Bool = false
isFavorite: Bool = false,
isSample: Bool = false
) {
self.id = id
self.name = name
Expand All @@ -77,13 +83,14 @@ public struct DatabaseConnection: Identifiable, Hashable, Sendable {
self.tagIds = tagIds
self.sortOrder = sortOrder
self.isFavorite = isFavorite
self.isSample = isSample
}

private enum CodingKeys: String, CodingKey {
case id, name, type, host, port, username, database, color, colorTag
case isReadOnly, safeModeLevel, queryTimeoutSeconds, additionalFields
case sshEnabled, sshConfiguration, sslEnabled, sslConfiguration
case groupId, tagId, tagIds, sortOrder, isFavorite
case groupId, tagId, tagIds, sortOrder, isFavorite, isSample
}
}

Expand Down Expand Up @@ -125,6 +132,7 @@ extension DatabaseConnection: Codable {
}
sortOrder = try container.decodeIfPresent(Int.self, forKey: .sortOrder) ?? 0
isFavorite = try container.decodeIfPresent(Bool.self, forKey: .isFavorite) ?? false
isSample = try container.decodeIfPresent(Bool.self, forKey: .isSample) ?? false
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -151,5 +159,8 @@ extension DatabaseConnection: Codable {
}
try container.encode(sortOrder, forKey: .sortOrder)
try container.encode(isFavorite, forKey: .isFavorite)
if isSample {
try container.encode(isSample, forKey: .isSample)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,6 @@ public final class SyncRecordCache {
}
}

public func removeAll() {
migration.withLock { $0 = true }
legacyDefaults?.removeObject(forKey: legacyStorageKey)
try? FileManager.default.removeItem(at: directory)
}

// MARK: - Migration

/// Moves a cache written by an older build out of `UserDefaults` on first use, then clears the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
import Testing

@testable import TableProModels

@Suite("DatabaseConnection sample flag")
struct DatabaseConnectionSampleTests {
private static let storedWithoutFlag = """
{"id":"6F9619FF-8B86-D011-B42D-00C04FC964FF","name":"Prod","type":"PostgreSQL","host":"db.local",\
"port":5432,"username":"app","database":"prod"}
"""

@Test("A connection stored before the flag existed decodes as not a sample")
func legacyRecordIsNotSample() throws {
let connection = try JSONDecoder().decode(
DatabaseConnection.self,
from: Data(Self.storedWithoutFlag.utf8)
)

#expect(connection.isSample == false)
#expect(connection.participatesInSync)
}

@Test("A sample round-trips and stays out of sync")
func sampleRoundTrips() throws {
let sample = DatabaseConnection(name: "Sample", type: .sqlite, database: "Chinook.sqlite", isSample: true)

let decoded = try JSONDecoder().decode(DatabaseConnection.self, from: JSONEncoder().encode(sample))

#expect(decoded.isSample)
#expect(decoded.participatesInSync == false)
}

@Test("An ordinary connection writes no sample key")
func ordinaryConnectionOmitsKey() throws {
let connection = DatabaseConnection(name: "Prod", type: .mysql)

let object = try JSONSerialization.jsonObject(with: JSONEncoder().encode(connection)) as? [String: Any]

#expect(object?["isSample"] == nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ struct ThirdPartyLicenseInventory {

/// Components with a confirmed licence, which is what the acknowledgements list renders.
var attributed: [ThirdPartyComponent] {
components.filter { !$0.isUnverified }
components.filter { $0.ships(on: .macos) && !$0.isUnverified }
}

/// Components whose licence could not be confirmed from a primary source. Surfaced rather
/// than dropped, so an unresolved obligation stays visible instead of looking discharged.
var unresolved: [ThirdPartyComponent] {
components.filter(\.isUnverified)
components.filter { $0.ships(on: .macos) && $0.isUnverified }
}

init(rootURL: URL) throws {
Expand Down
30 changes: 30 additions & 0 deletions TablePro/Models/ThirdParty/ThirdPartyComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ struct ThirdPartyComponent: Codable, Identifiable, Hashable {
}
}

enum Platform: String, Codable, Hashable {
case macos
case ios
}

let id: String
let name: String
let version: String
Expand All @@ -51,15 +56,40 @@ struct ThirdPartyComponent: Codable, Identifiable, Hashable {
let source: String
let patched: Bool
let notes: String?
let platforms: [Platform]

var versionSource: VersionSource { VersionSource(rawValue: source) }

func ships(on platform: Platform) -> Bool {
platforms.contains(platform)
}

/// A component whose licence has not been confirmed from a primary source. It is listed
/// as unresolved rather than being given a guessed licence, because a wrong SPDX line is
/// a licence violation stated in the app's own voice.
var isUnverified: Bool { spdx == ThirdPartyComponent.unverifiedSPDX }

static let unverifiedSPDX = "UNVERIFIED"

static let defaultPlatforms: [Platform] = [.macos]
}

extension ThirdPartyComponent {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
version = try container.decode(String.self, forKey: .version)
spdx = try container.decode(String.self, forKey: .spdx)
copyrights = try container.decode([String].self, forKey: .copyrights)
homepageURL = try container.decode(String.self, forKey: .homepageURL)
licenseTextURL = try container.decode(String.self, forKey: .licenseTextURL)
textFile = try container.decodeIfPresent(String.self, forKey: .textFile)
source = try container.decode(String.self, forKey: .source)
patched = try container.decode(Bool.self, forKey: .patched)
notes = try container.decodeIfPresent(String.self, forKey: .notes)
platforms = try container.decodeIfPresent([Platform].self, forKey: .platforms) ?? Self.defaultPlatforms
}
}

private extension String {
Expand Down
Loading
Loading