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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Startup commands — run custom SQL after connecting (e.g., SET time_zone) in Connection > Advanced tab
- Plugin system architecture — all 8 database drivers (MySQL, PostgreSQL, SQLite, ClickHouse, MSSQL, MongoDB, Redis, Oracle) extracted into `.tableplugin` bundles loaded at runtime
- Settings > Plugins tab for plugin management — list installed plugins, enable/disable, install from file, uninstall user plugins, view plugin details
- Plugin marketplace — browse, search, and install plugins from the GitHub-hosted registry with SHA-256 checksum verification, ETag caching, and offline fallback
Expand Down
49 changes: 49 additions & 0 deletions TablePro/Core/Database/DatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ final class DatabaseManager {
try await driver.applyQueryTimeout(timeoutSeconds)
}

// Run startup commands before schema init
await executeStartupCommands(
connection.startupCommands, on: driver, connectionName: connection.name
)

// Initialize schema for drivers that support schema switching
if let schemaDriver = driver as? SchemaSwitchable {
activeSessions[connection.id]?.currentSchema = schemaDriver.currentSchema
Expand Down Expand Up @@ -190,6 +195,9 @@ final class DatabaseManager {
if metaTimeout > 0 {
try? await metaDriver.applyQueryTimeout(metaTimeout)
}
await self.executeStartupCommands(
connection.startupCommands, on: metaDriver, connectionName: connection.name
)
if let savedSchema = self.activeSessions[metaConnectionId]?.currentSchema,
let schemaMetaDriver = metaDriver as? SchemaSwitchable {
try? await schemaMetaDriver.switchSchema(to: savedSchema)
Expand Down Expand Up @@ -547,6 +555,10 @@ final class DatabaseManager {
try await driver.applyQueryTimeout(timeoutSeconds)
}

await executeStartupCommands(
session.connection.startupCommands, on: driver, connectionName: session.connection.name
)

if let savedSchema = session.currentSchema,
let schemaDriver = driver as? SchemaSwitchable {
try? await schemaDriver.switchSchema(to: savedSchema)
Expand Down Expand Up @@ -617,6 +629,10 @@ final class DatabaseManager {
try await driver.applyQueryTimeout(timeoutSeconds)
}

await executeStartupCommands(
session.connection.startupCommands, on: driver, connectionName: session.connection.name
)

if let savedSchema = activeSessions[sessionId]?.currentSchema,
let schemaDriver = driver as? SchemaSwitchable {
try? await schemaDriver.switchSchema(to: savedSchema)
Expand All @@ -639,6 +655,8 @@ final class DatabaseManager {
let metaConnection = effectiveConnection
let metaConnectionId = sessionId
let metaTimeout = AppSettingsManager.shared.general.queryTimeoutSeconds
let startupCmds = session.connection.startupCommands
let connName = session.connection.name
Task { [weak self] in
guard let self else { return }
do {
Expand All @@ -647,6 +665,9 @@ final class DatabaseManager {
if metaTimeout > 0 {
try? await metaDriver.applyQueryTimeout(metaTimeout)
}
await self.executeStartupCommands(
startupCmds, on: metaDriver, connectionName: connName
)
if let savedSchema = self.activeSessions[metaConnectionId]?.currentSchema,
let schemaMetaDriver = metaDriver as? SchemaSwitchable {
try? await schemaMetaDriver.switchSchema(to: savedSchema)
Expand Down Expand Up @@ -720,6 +741,34 @@ final class DatabaseManager {
}
}

// MARK: - Startup Commands

nonisolated private func executeStartupCommands(
_ commands: String?, on driver: DatabaseDriver, connectionName: String
) async {
guard let commands, !commands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return
}

let statements = commands
.components(separatedBy: CharacterSet(charactersIn: ";\n"))
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }

for statement in statements {
do {
_ = try await driver.execute(query: statement)
Self.logger.info(
"Startup command succeeded for '\(connectionName)': \(statement)"
)
} catch {
Self.logger.warning(
"Startup command failed for '\(connectionName)': \(statement) — \(error.localizedDescription)"
)
}
}
}

// MARK: - Schema Changes

/// Execute schema changes (ALTER TABLE, CREATE INDEX, etc.) in a transaction
Expand Down
21 changes: 19 additions & 2 deletions TablePro/Core/Storage/ConnectionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,18 @@ final class ConnectionStorage {
username: connection.username,
type: connection.type,
sshConfig: connection.sshConfig,
sslConfig: connection.sslConfig,
color: connection.color,
tagId: connection.tagId,
groupId: connection.groupId
groupId: connection.groupId,
isReadOnly: connection.isReadOnly,
aiPolicy: connection.aiPolicy,
mongoReadPreference: connection.mongoReadPreference,
mongoWriteConcern: connection.mongoWriteConcern,
redisDatabase: connection.redisDatabase,
mssqlSchema: connection.mssqlSchema,
oracleServiceName: connection.oracleServiceName,
startupCommands: connection.startupCommands
)

// Save the duplicate connection
Expand Down Expand Up @@ -365,6 +374,9 @@ private struct StoredConnection: Codable {
// Oracle service name
let oracleServiceName: String?

// Startup commands
let startupCommands: String?

init(from connection: DatabaseConnection) {
self.id = connection.id
self.name = connection.name
Expand Down Expand Up @@ -406,6 +418,9 @@ private struct StoredConnection: Codable {

// Oracle service name
self.oracleServiceName = connection.oracleServiceName

// Startup commands
self.startupCommands = connection.startupCommands
}

// Custom decoder to handle migration from old format
Expand Down Expand Up @@ -445,6 +460,7 @@ private struct StoredConnection: Codable {
aiPolicy = try container.decodeIfPresent(String.self, forKey: .aiPolicy)
mssqlSchema = try container.decodeIfPresent(String.self, forKey: .mssqlSchema)
oracleServiceName = try container.decodeIfPresent(String.self, forKey: .oracleServiceName)
startupCommands = try container.decodeIfPresent(String.self, forKey: .startupCommands)
}

func toConnection() -> DatabaseConnection {
Expand Down Expand Up @@ -487,7 +503,8 @@ private struct StoredConnection: Codable {
isReadOnly: isReadOnly,
aiPolicy: parsedAIPolicy,
mssqlSchema: mssqlSchema,
oracleServiceName: oracleServiceName
oracleServiceName: oracleServiceName,
startupCommands: startupCommands
)
}
}
5 changes: 4 additions & 1 deletion TablePro/Models/Connection/DatabaseConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ struct DatabaseConnection: Identifiable, Hashable {
var redisDatabase: Int?
var mssqlSchema: String?
var oracleServiceName: String?
var startupCommands: String?

init(
id: UUID = UUID(),
Expand All @@ -428,7 +429,8 @@ struct DatabaseConnection: Identifiable, Hashable {
mongoWriteConcern: String? = nil,
redisDatabase: Int? = nil,
mssqlSchema: String? = nil,
oracleServiceName: String? = nil
oracleServiceName: String? = nil,
startupCommands: String? = nil
) {
self.id = id
self.name = name
Expand All @@ -449,6 +451,7 @@ struct DatabaseConnection: Identifiable, Hashable {
self.redisDatabase = redisDatabase
self.mssqlSchema = mssqlSchema
self.oracleServiceName = oracleServiceName
self.startupCommands = startupCommands
}

/// Returns the display color (custom color or database type color)
Expand Down
6 changes: 6 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -8586,6 +8586,9 @@
}
}
}
},
"SQL commands to run after connecting, e.g. SET time_zone = 'Asia/Ho_Chi_Minh'. One per line or separated by semicolons." : {

},
"SQL Dialect" : {

Expand Down Expand Up @@ -8781,6 +8784,9 @@
}
}
}
},
"Startup Commands" : {

},
"statement" : {
"localizations" : {
Expand Down
Loading