Skip to content

Commit c01361f

Browse files
authored
feat(ios): DuckDB support for files and in-memory databases (#1537)
* feat(ios): add DuckDB support for files and in-memory databases (#1526) * fix(ios): release prior session on reconnect and drop stale DuckDB bookmarks (#1526) * fix(ios): link DuckDB core_functions and disable runtime extension autoload (#1526) * fix(ios): force-load DuckDB so built-in extensions register at launch (#1526) * fix(ios): add DuckDB header search path to the test target (#1526) * Revert "fix(ios): add DuckDB header search path to the test target (#1526)" This reverts commit 2429a26. * Revert "fix(ios): force-load DuckDB so built-in extensions register at launch (#1526)" This reverts commit a46174b. * fix(ios): register DuckDB linked extensions in core via the static-link defines (#1526) * fix(ios): drop DuckDB dummy extension loader so linked extensions register (#1526) * feat(ios): cancel running DuckDB queries and keep multi-statement results renderable (#1526) * feat(ios): stream DuckDB results by chunk with native vector decoding (#1526) * fix(ios): resolve DuckDB streaming quoteIdentifier redeclaration (#1526) * fix(ios): disambiguate DuckDB streaming AsyncThrowingStream initializer (#1526) * fix(ios): restore DuckDBActor interrupt for stream cancellation (#1526) * fix(ios): decode DuckDB execute() results via chunk API to avoid deprecated value-API crash (#1526) * refactor(ios): unify DuckDB cast/decode logic and cast any non-natively-decoded type (#1526) --------- Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent 003f837 commit c01361f

24 files changed

Lines changed: 1717 additions & 33 deletions

.github/workflows/ios-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ jobs:
5757
uses: actions/cache@v4
5858
with:
5959
path: Libs
60-
# Include the FreeTDS stub header in the cache key so iOS xcframework refreshes
61-
# whenever the C bridge surface (e.g. new symbol declarations) changes.
62-
key: ${{ runner.os }}-libs-${{ hashFiles('Libs/checksums.sha256', 'Plugins/MSSQLDriverPlugin/CFreeTDS/include/sybdb.h') }}
60+
# Include C bridge stub headers in the cache key so the iOS xcframework set
61+
# refreshes whenever a bridge surface (e.g. a new driver's headers) changes.
62+
key: ${{ runner.os }}-libs-${{ hashFiles('Libs/checksums.sha256', 'Plugins/MSSQLDriverPlugin/CFreeTDS/include/sybdb.h', 'TableProMobile/TableProMobile/CBridges/CDuckDB/CDuckDB.h') }}
6363

6464
- name: Download static libraries
6565
env:

CHANGELOG.md

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

1010
### Added
1111

12+
- iOS: open DuckDB database files and in-memory DuckDB databases, matching the Mac app. (#1526)
1213
- Save the current query as a favorite from a star button in the SQL editor toolbar.
1314
- Field names and types in the row Details panel can now be selected and copied.
1415

Packages/TableProCore/Sources/TableProDatabase/ConnectionManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public final class ConnectionManager: @unchecked Sendable {
2020
}
2121

2222
public func connect(_ connection: DatabaseConnection) async throws -> ConnectionSession {
23+
await disconnect(connection.id)
2324
let password = try secureStore.retrieve(forKey: Self.passwordKey(for: connection.id))
2425

2526
var effectiveHost = connection.host

Packages/TableProCore/Tests/TableProDatabaseTests/ConnectionManagerTests.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ import Foundation
88
private final class MockDatabaseDriver: DatabaseDriver, @unchecked Sendable {
99
var isConnected = false
1010
var shouldFailConnect = false
11+
private(set) var disconnectCount = 0
1112

1213
func connect() async throws {
1314
if shouldFailConnect { throw NSError(domain: "test", code: 1) }
1415
isConnected = true
1516
}
1617

17-
func disconnect() async throws { isConnected = false }
18+
func disconnect() async throws {
19+
isConnected = false
20+
disconnectCount += 1
21+
}
1822
func ping() async throws -> Bool { isConnected }
1923

2024
func execute(query: String) async throws -> QueryResult {
@@ -111,6 +115,30 @@ struct ConnectionManagerTests {
111115
#expect(session == nil)
112116
}
113117

118+
@Test("Reconnecting for the same id tears down the previous session")
119+
func reconnectDisconnectsPrevious() async throws {
120+
let factory = MockDriverFactory()
121+
let store = MockSecureStore()
122+
let manager = ConnectionManager(driverFactory: factory, secureStore: store)
123+
124+
let connection = DatabaseConnection(
125+
name: "Test",
126+
type: DatabaseType(rawValue: "mock")
127+
)
128+
129+
let first = MockDatabaseDriver()
130+
factory.drivers["mock"] = first
131+
_ = try await manager.connect(connection)
132+
133+
let second = MockDatabaseDriver()
134+
factory.drivers["mock"] = second
135+
_ = try await manager.connect(connection)
136+
137+
#expect(first.disconnectCount == 1)
138+
#expect(second.isConnected)
139+
#expect(manager.session(for: connection.id)?.driver === second)
140+
}
141+
114142
@Test("Connect with unknown type throws driverNotFound")
115143
func connectUnknownType() async throws {
116144
let factory = MockDriverFactory()

TableProMobile/TableProMobile.xcodeproj/project.pbxproj

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
5AB9F3EF2F7C1D03001F3337 /* TableProQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 5AB9F3EE2F7C1D03001F3337 /* TableProQuery */; };
2727
5AD1F1B62FB4455700296783 /* TableProMSSQLCore in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD1F1B52FB4455700296783 /* TableProMSSQLCore */; };
2828
5AD1F1B82FB4456900296783 /* FreeTDS.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AD1F1B72FB4456900296783 /* FreeTDS.xcframework */; };
29+
5AD0CCDB2F0000000000F002 /* DuckDB.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AD0CCDB2F0000000000F001 /* DuckDB.xcframework */; };
2930
5AD1F2002FB5500000000002 /* FreeTDSConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD1F2002FB5500000000001 /* FreeTDSConnection.swift */; };
3031
/* End PBXBuildFile section */
3132

@@ -529,6 +530,7 @@
529530
5AB9F3D92F7C1C12001F3337 /* TableProMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TableProMobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
530531
5AC8A8F82FAFC99F005DE2A3 /* TableProMobileTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TableProMobileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
531532
5AD1F1B72FB4456900296783 /* FreeTDS.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = FreeTDS.xcframework; path = ../Libs/ios/FreeTDS.xcframework; sourceTree = "<group>"; };
533+
5AD0CCDB2F0000000000F001 /* DuckDB.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = DuckDB.xcframework; path = ../Libs/ios/DuckDB.xcframework; sourceTree = "<group>"; };
532534
5AD1F2002FB5500000000001 /* FreeTDSConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FreeTDSConnection.swift; path = ../Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift; sourceTree = "<group>"; };
533535
/* End PBXFileReference section */
534536

@@ -609,6 +611,7 @@
609611
5AA313442F7EA5B4008EBA97 /* OpenSSL-SSL.xcframework in Frameworks */,
610612
5AB9F3ED2F7C1D03001F3337 /* TableProPluginKit in Frameworks */,
611613
5AD1F1B82FB4456900296783 /* FreeTDS.xcframework in Frameworks */,
614+
5AD0CCDB2F0000000000F002 /* DuckDB.xcframework in Frameworks */,
612615
5AD1F1B62FB4455700296783 /* TableProMSSQLCore in Frameworks */,
613616
5A87EEED2F7F893000D028D0 /* TableProSync in Frameworks */,
614617
5AB9F3EB2F7C1D03001F3337 /* TableProModels in Frameworks */,
@@ -1634,6 +1637,7 @@
16341637
isa = PBXGroup;
16351638
children = (
16361639
5AD1F1B72FB4456900296783 /* FreeTDS.xcframework */,
1640+
5AD0CCDB2F0000000000F001 /* DuckDB.xcframework */,
16371641
5A87EEEB2F7F891F00D028D0 /* TableProSync */,
16381642
5A87EEE42F7F88F200D028D0 /* TablePro */,
16391643
5AA313532F7EC188008EBA97 /* LibSSH2.xcframework */,
@@ -2085,14 +2089,15 @@
20852089
OTHER_LDFLAGS = (
20862090
"-lz",
20872091
"-liconv",
2092+
"-lc++",
20882093
);
20892094
PRODUCT_BUNDLE_IDENTIFIER = com.TablePro.TableProMobile;
20902095
PRODUCT_NAME = "$(TARGET_NAME)";
20912096
STRING_CATALOG_GENERATE_SYMBOLS = YES;
20922097
SWIFT_APPROACHABLE_CONCURRENCY = YES;
20932098
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
20942099
SWIFT_EMIT_LOC_STRINGS = YES;
2095-
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS";
2100+
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS $(SRCROOT)/TableProMobile/CBridges/CDuckDB";
20962101
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
20972102
SWIFT_VERSION = 5.0;
20982103
TARGETED_DEVICE_FAMILY = "1,2";
@@ -2127,14 +2132,15 @@
21272132
OTHER_LDFLAGS = (
21282133
"-lz",
21292134
"-liconv",
2135+
"-lc++",
21302136
);
21312137
PRODUCT_BUNDLE_IDENTIFIER = com.TablePro.TableProMobile;
21322138
PRODUCT_NAME = "$(TARGET_NAME)";
21332139
STRING_CATALOG_GENERATE_SYMBOLS = YES;
21342140
SWIFT_APPROACHABLE_CONCURRENCY = YES;
21352141
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
21362142
SWIFT_EMIT_LOC_STRINGS = YES;
2137-
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS";
2143+
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS $(SRCROOT)/TableProMobile/CBridges/CDuckDB";
21382144
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
21392145
SWIFT_VERSION = 5.0;
21402146
TARGETED_DEVICE_FAMILY = "1,2";
@@ -2156,7 +2162,7 @@
21562162
STRING_CATALOG_GENERATE_SYMBOLS = NO;
21572163
SWIFT_APPROACHABLE_CONCURRENCY = YES;
21582164
SWIFT_EMIT_LOC_STRINGS = NO;
2159-
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS";
2165+
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS $(SRCROOT)/TableProMobile/CBridges/CDuckDB";
21602166
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
21612167
SWIFT_VERSION = 5.0;
21622168
TARGETED_DEVICE_FAMILY = "1,2";
@@ -2179,7 +2185,7 @@
21792185
STRING_CATALOG_GENERATE_SYMBOLS = NO;
21802186
SWIFT_APPROACHABLE_CONCURRENCY = YES;
21812187
SWIFT_EMIT_LOC_STRINGS = NO;
2182-
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS";
2188+
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TableProMobile/CBridges/CMariaDB $(SRCROOT)/TableProMobile/CBridges/CLibPQ $(SRCROOT)/TableProMobile/CBridges/CRedis $(SRCROOT)/TableProMobile/CBridges/CLibSSH2 $(SRCROOT)/TableProMobile/CBridges/CFreeTDS $(SRCROOT)/TableProMobile/CBridges/CDuckDB";
21832189
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
21842190
SWIFT_VERSION = 5.0;
21852191
TARGETED_DEVICE_FAMILY = "1,2";

TableProMobile/TableProMobile/AppState.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ final class AppState {
189189
try? secureStore.delete(forKey: "com.TablePro.sshpassword.\(connection.id.uuidString)")
190190
try? secureStore.delete(forKey: "com.TablePro.keypassphrase.\(connection.id.uuidString)")
191191
try? secureStore.delete(forKey: "com.TablePro.sshkeydata.\(connection.id.uuidString)")
192+
FileBookmarkStore().delete(for: connection.id)
192193
clearPerConnectionPreferences(for: connection.id)
193194
persist(connections: updated)
194195
updateWidgetData()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#ifndef CDuckDB_h
2+
#define CDuckDB_h
3+
4+
#include <duckdb.h>
5+
6+
#endif
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module CDuckDB [system] {
2+
header "CDuckDB.h"
3+
export *
4+
}

TableProMobile/TableProMobile/Coordinators/ConnectionCoordinator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ final class ConnectionCoordinator {
5555

5656
var supportsSchemas: Bool {
5757
connection.type == .postgresql || connection.type == .redshift ||
58-
connection.type == .mssql
58+
connection.type == .mssql || connection.type == .duckdb
5959
}
6060

6161
init(connection: DatabaseConnection, appState: AppState) {

0 commit comments

Comments
 (0)