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
10 changes: 3 additions & 7 deletions Plugins/BigQueryDriverPlugin/BigQueryAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ internal final class ServiceAccountAuthProvider: @unchecked Sendable, BigQueryAu

self.clientEmail = email
self.privateKeyPEM = key
self.projectId = overrideProjectId?.isEmpty == false ? overrideProjectId! : saProjectId
self.projectId = overrideProjectId.flatMap { $0.isEmpty ? nil : $0 } ?? saProjectId

if self.projectId.isEmpty {
throw BigQueryError.authFailed("No project ID found in service account JSON or connection settings")
Expand Down Expand Up @@ -349,9 +349,7 @@ internal final class ADCAuthProvider: @unchecked Sendable, BigQueryAuthProvider
}

let quotaProject = json["quota_project_id"] as? String ?? ""
self.projectId = overrideProjectId?.isEmpty == false
? overrideProjectId!
: quotaProject
self.projectId = overrideProjectId.flatMap { $0.isEmpty ? nil : $0 } ?? quotaProject

if self.projectId.isEmpty {
throw BigQueryError.authFailed(
Expand All @@ -374,9 +372,7 @@ internal final class ADCAuthProvider: @unchecked Sendable, BigQueryAuthProvider
}

// Resolve source credentials
let resolvedProjectId = overrideProjectId?.isEmpty == false
? overrideProjectId!
: (json["quota_project_id"] as? String ?? "")
let resolvedProjectId = overrideProjectId.flatMap { $0.isEmpty ? nil : $0 } ?? (json["quota_project_id"] as? String ?? "")

if resolvedProjectId.isEmpty {
throw BigQueryError.authFailed(
Expand Down
8 changes: 2 additions & 6 deletions Plugins/BigQueryDriverPlugin/BigQueryQueryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,7 @@ internal struct BigQueryQueryBuilder {

// Search
if let searchText = params.searchText, !searchText.isEmpty {
let searchCols = params.searchColumns?.isEmpty == false
? params.searchColumns!
: columns
let searchCols = params.searchColumns.flatMap { $0.isEmpty ? nil : $0 } ?? columns
let escapedSearch = searchText.replacingOccurrences(of: "'", with: "''")
let searchClauses = searchCols.map { col in
"CAST(\(quoteIdentifier(col)) AS STRING) LIKE '%\(escapedSearch)%'"
Expand Down Expand Up @@ -236,9 +234,7 @@ internal struct BigQueryQueryBuilder {
}

if let searchText = params.searchText, !searchText.isEmpty {
let searchCols = params.searchColumns?.isEmpty == false
? params.searchColumns!
: columns
let searchCols = params.searchColumns.flatMap { $0.isEmpty ? nil : $0 } ?? columns
let escapedSearch = searchText.replacingOccurrences(of: "'", with: "''")
let searchClauses = searchCols.map { col in
"CAST(\(quoteIdentifier(col)) AS STRING) LIKE '%\(escapedSearch)%'"
Expand Down
4 changes: 1 addition & 3 deletions Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {

init(config: DriverConnectionConfig) {
self.config = config
self._currentSchema = config.additionalFields["mssqlSchema"]?.isEmpty == false
? config.additionalFields["mssqlSchema"]!
: "dbo"
self._currentSchema = config.additionalFields["mssqlSchema"].flatMap { $0.isEmpty ? nil : $0 } ?? "dbo"
}

private var escapedSchema: String {
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)
}

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
Loading