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
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ final class CloudflareD1PluginDriver: PluginDatabaseDriver, @unchecked Sendable

guard let match = databases.first(where: { $0.name == databaseName }) else {
throw CloudflareD1Error(
message: String(localized: "Database '\(databaseName)' not found in account")
message: String(format: String(localized: "Database '%@' not found in account"), databaseName)
)
}
databaseId = match.uuid
Expand Down Expand Up @@ -603,7 +603,7 @@ final class CloudflareD1PluginDriver: PluginDatabaseDriver, @unchecked Sendable

guard let resolvedUuid = uuid else {
throw CloudflareD1Error(
message: String(localized: "Database '\(database)' not found")
message: String(format: String(localized: "Database '%@' not found"), database)
)
}

Expand Down
2 changes: 1 addition & 1 deletion Plugins/CloudflareD1DriverPlugin/D1HttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ final class D1HttpClient: @unchecked Sendable {
Self.logger.warning("D1 rate limited. Retry-After: \(retryAfter ?? "not specified")")
if let seconds = retryAfter {
throw D1HttpError(
message: String(localized: "Rate limited by Cloudflare. Retry after \(seconds) seconds.")
message: String(format: String(localized: "Rate limited by Cloudflare. Retry after %@ seconds."), seconds)
)
} else {
throw D1HttpError(
Expand Down
8 changes: 4 additions & 4 deletions Plugins/DynamoDBDriverPlugin/DynamoDBConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,15 @@ internal enum DynamoDBError: Error, LocalizedError {
case .notConnected:
return String(localized: "Not connected to DynamoDB")
case .connectionFailed(let detail):
return String(localized: "Connection failed: \(detail)")
return String(format: String(localized: "Connection failed: %@"), detail)
case .serverError(let detail):
return String(localized: "DynamoDB error: \(detail)")
return String(format: String(localized: "DynamoDB error: %@"), detail)
case .authFailed(let detail):
return String(localized: "Authentication failed: \(detail)")
return String(format: String(localized: "Authentication failed: %@"), detail)
case .requestCancelled:
return String(localized: "Request was cancelled")
case .invalidResponse(let detail):
return String(localized: "Invalid response: \(detail)")
return String(format: String(localized: "Invalid response: %@"), detail)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions Plugins/EtcdDriverPlugin/EtcdCommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ extension EtcdParseError: PluginDriverError {
var pluginErrorMessage: String {
switch self {
case .emptySyntax: return String(localized: "Empty etcd command")
case .unknownCommand(let cmd): return String(localized: "Unknown command: \(cmd)")
case .missingArgument(let msg): return String(localized: "Missing argument: \(msg)")
case .invalidArgument(let msg): return String(localized: "Invalid argument: \(msg)")
case .unknownCommand(let cmd): return String(format: String(localized: "Unknown command: %@"), cmd)
case .missingArgument(let msg): return String(format: String(localized: "Missing argument: %@"), msg)
case .invalidArgument(let msg): return String(format: String(localized: "Invalid argument: %@"), msg)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions Plugins/EtcdDriverPlugin/EtcdHttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ internal enum EtcdError: Error, LocalizedError {
case .notConnected:
return String(localized: "Not connected to etcd")
case .connectionFailed(let detail):
return String(localized: "Connection failed: \(detail)")
return String(format: String(localized: "Connection failed: %@"), detail)
case .serverError(let detail):
return String(localized: "Server error: \(detail)")
return String(format: String(localized: "Server error: %@"), detail)
case .authFailed(let detail):
return String(localized: "Authentication failed: \(detail)")
return String(format: String(localized: "Authentication failed: %@"), detail)
case .requestCancelled:
return String(localized: "Request was cancelled")
}
Expand Down
4 changes: 2 additions & 2 deletions Plugins/RedisDriverPlugin/RedisCommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ extension RedisParseError: PluginDriverError {
var pluginErrorMessage: String {
switch self {
case .emptySyntax: return String(localized: "Empty Redis command")
case .invalidArgument(let msg): return String(localized: "Invalid argument: \(msg)")
case .missingArgument(let msg): return String(localized: "Missing argument: \(msg)")
case .invalidArgument(let msg): return String(format: String(localized: "Invalid argument: %@"), msg)
case .missingArgument(let msg): return String(format: String(localized: "Missing argument: %@"), msg)
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions Plugins/TableProPluginKit/MongoShellParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ public enum MongoShellParseError: Error, LocalizedError {
public var errorDescription: String? {
switch self {
case .invalidSyntax(let msg):
return String(localized: "Invalid MongoDB syntax: \(msg)")
return String(format: String(localized: "Invalid MongoDB syntax: %@"), msg)
case .unsupportedMethod(let method):
return String(localized: "Unsupported MongoDB method: \(method)")
return String(format: String(localized: "Unsupported MongoDB method: %@"), method)
case .invalidJson(let msg):
return String(localized: "Invalid JSON: \(msg)")
return String(format: String(localized: "Invalid JSON: %@"), msg)
case .missingArgument(let msg):
return String(localized: "Missing argument: \(msg)")
return String(format: String(localized: "Missing argument: %@"), msg)
}
}
}
Expand Down
6 changes: 0 additions & 6 deletions TablePro/Core/ChangeTracking/SQLStatementGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,6 @@ struct SQLStatementGenerator {
return ParameterizedStatement(sql: sql, parameters: parameters)
}

/// Marker type for SQL function literals that cannot be parameterized
private struct SQLFunctionLiteral {
let value: String
init(_ value: String) { self.value = value }
}

// MARK: - UPDATE Generation

func generateUpdateSQL(for change: RowChange) -> ParameterizedStatement? {
Expand Down
9 changes: 2 additions & 7 deletions TablePro/Core/Database/DatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,16 +230,11 @@ extension DatabaseDriver {
var queryBuildingPluginDriver: (any PluginDatabaseDriver)? { nil }

func quoteIdentifier(_ name: String) -> String {
let q = "\""
let escaped = name.replacingOccurrences(of: q, with: q + q)
return "\(q)\(escaped)\(q)"
SQLEscaping.quoteIdentifier(name)
}

func escapeStringLiteral(_ value: String) -> String {
var result = value
result = result.replacingOccurrences(of: "'", with: "''")
result = result.replacingOccurrences(of: "\0", with: "")
return result
SQLEscaping.escapeStringLiteral(value)
}

func createViewTemplate() -> String? { nil }
Expand Down
7 changes: 7 additions & 0 deletions TablePro/Core/Database/SQLEscaping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ enum SQLEscaping {
return result
}

/// Quote a SQL identifier using ANSI double-quote rules, doubling any embedded quote.
static func quoteIdentifier(_ identifier: String) -> String {
let quote = "\""
let escaped = identifier.replacingOccurrences(of: quote, with: quote + quote)
return "\(quote)\(escaped)\(quote)"
}

/// Known SQL temporal function expressions that should not be quoted/parameterized.
/// Canonical source — used by SQLStatementGenerator and sidebar save logic.
static let temporalFunctionExpressions: Set<String> = [
Expand Down
6 changes: 0 additions & 6 deletions TablePro/Core/MCP/MCPAuditLogStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ actor MCPAuditLogStorage {
private var dbPath: String?
private let testDatabaseSuffix: String?

enum TimeRange: Equatable {
case lastHours(Int)
case lastDays(Int)
case all
}

init() {
self.testDatabaseSuffix = nil
setupDatabase()
Expand Down
47 changes: 0 additions & 47 deletions TablePro/Core/MCP/TokenPermissionFilter.swift

This file was deleted.

4 changes: 2 additions & 2 deletions TablePro/Core/Plugins/QueryResultExportDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ final class QueryResultExportDataSource: PluginExportDataSource, @unchecked Send
if let driver {
return driver.quoteIdentifier(identifier)
}
return "\"\(identifier.replacingOccurrences(of: "\"", with: "\"\""))\""
return SQLEscaping.quoteIdentifier(identifier)
}

func escapeStringLiteral(_ value: String) -> String {
if let driver {
return driver.escapeStringLiteral(value)
}
return value.replacingOccurrences(of: "'", with: "''")
return SQLEscaping.escapeStringLiteral(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve NUL bytes in query-result SQL exports

In non-streaming query-results export, ExportDialog constructs ExportService(databaseType:), leaving this data source without a driver, and the SQL exporter writes text cells through dataSource.escapeStringLiteral before generating INSERT values. Reusing SQLEscaping.escapeStringLiteral on this nil-driver path strips embedded \0 characters, so a text result containing a NUL byte is silently changed in the exported SQL; the previous fallback only doubled quotes and preserved the value. Please keep result-set export escaping lossless here or route through a driver-specific literal encoder.

Useful? React with 👍 / 👎.

}

func fetchTableDDL(table: String, databaseName: String) async throws -> String {
Expand Down
12 changes: 0 additions & 12 deletions TablePro/Models/Connection/ConnectionExport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,6 @@
import Foundation
import UniformTypeIdentifiers

// MARK: - Sheet Binding Wrappers

struct IdentifiableURL: Identifiable {
let id = UUID()
let url: URL
}

struct IdentifiableConnections: Identifiable {
let id = UUID()
let connections: [DatabaseConnection]
}

// MARK: - UTType

extension UTType {
Expand Down
40 changes: 0 additions & 40 deletions TablePro/Models/Connection/ConnectionToolbarState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,6 @@ import Observation
import SwiftUI
import TableProPluginKit

// MARK: - Connection Environment

/// Represents the connection environment type for visual badges
enum ConnectionEnvironment: String, CaseIterable {
case local = "LOCAL"
case ssh = "SSH"
case production = "PROD"
case staging = "STAGING"

/// SF Symbol for this environment type
var iconName: String {
switch self {
case .local: return "house.fill"
case .ssh: return "lock.fill"
case .production: return "exclamationmark.triangle.fill"
case .staging: return "testtube.2"
}
}

/// Badge background color
var backgroundColor: Color {
switch self {
case .local: return .gray.opacity(0.3)
case .ssh: return .orange.opacity(0.3)
case .production: return .red.opacity(0.3)
case .staging: return .blue.opacity(0.3)
}
}

/// Badge foreground color
var foregroundColor: Color {
switch self {
case .local: return .secondary
case .ssh: return .orange
case .production: return .red
case .staging: return .blue
}
}
}

// MARK: - Connection State

/// Represents the current state of the database connection
Expand Down
17 changes: 17 additions & 0 deletions TableProTests/Core/Database/SQLEscapingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,23 @@ struct SQLEscapingTests {
#expect(result == "\\''")
}

// MARK: - quoteIdentifier Tests (ANSI SQL)

@Test("Plain identifier wrapped in double quotes")
func testQuoteIdentifierPlain() {
#expect(SQLEscaping.quoteIdentifier("users") == "\"users\"")
}

@Test("Embedded double quote is doubled")
func testQuoteIdentifierEmbeddedQuote() {
#expect(SQLEscaping.quoteIdentifier("we\"ird") == "\"we\"\"ird\"")
}

@Test("Empty identifier yields empty quotes")
func testQuoteIdentifierEmpty() {
#expect(SQLEscaping.quoteIdentifier("") == "\"\"")
}

// MARK: - escapeLikeWildcards Tests

@Test("LIKE plain string unchanged")
Expand Down
8 changes: 8 additions & 0 deletions TableProTests/Core/MongoDB/MongoShellParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1072,4 +1072,12 @@ struct MongoShellParserTests {
Issue.record("Expected .deleteOne operation")
}
}

@Test("error descriptions substitute the interpolated value")
func testErrorDescriptionsFormatArgument() {
#expect(MongoShellParseError.invalidSyntax("bad{").errorDescription == "Invalid MongoDB syntax: bad{")
#expect(MongoShellParseError.unsupportedMethod("foo").errorDescription == "Unsupported MongoDB method: foo")
#expect(MongoShellParseError.invalidJson("oops").errorDescription == "Invalid JSON: oops")
#expect(MongoShellParseError.missingArgument("id").errorDescription == "Missing argument: id")
}
}
Loading