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
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

When exporting non-streaming query results, ExportDialog creates ExportService(databaseType:), so QueryResultExportDataSource has no driver and SQL export text cells flow through this fallback before SQLExportPlugin writes the INSERT values. Switching the fallback to SQLEscaping.escapeStringLiteral now strips embedded \0 bytes, whereas the previous implementation only doubled quotes, so a text value such as "a\0b" is exported as 'ab' and a round-trip silently loses data.

Useful? React with 👍 / 👎.

}

func fetchTableDDL(table: String, databaseName: String) async throws -> String {
Expand Down
9 changes: 0 additions & 9 deletions TablePro/Core/Services/Query/RowParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ protocol RowDataParser {
/// Matches the format produced by RowOperationsManager.copySelectedRowsToClipboard()
struct TSVRowParser: RowDataParser {
func parse(_ text: String, schema: TableSchema) -> Result<[ParsedRow], RowParseError> {
// Check for empty input
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return .failure(.emptyClipboard)
}

// Split into lines
let lines = text.components(separatedBy: .newlines)
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
Expand All @@ -42,18 +40,14 @@ struct TSVRowParser: RowDataParser {
for (index, line) in lines.enumerated() {
let lineNumber = index + 1

// Parse TSV line
let rawValues = line.components(separatedBy: "\t")
var values = rawValues.map { normalizeValue($0) }

// Handle column count mismatch
if values.count < schema.columnCount {
// Pad with NULL for missing columns
while values.count < schema.columnCount {
values.append(nil)
}
} else if values.count > schema.columnCount {
// Truncate extra columns
values = Array(values.prefix(schema.columnCount))
}

Expand All @@ -76,7 +70,6 @@ struct TSVRowParser: RowDataParser {
private func normalizeValue(_ rawValue: String) -> String? {
let trimmed = rawValue.trimmingCharacters(in: .whitespaces)

// Empty string or "NULL" (case-insensitive) → nil
if trimmed.isEmpty || trimmed.uppercased() == "NULL" {
return nil
}
Expand Down Expand Up @@ -107,7 +100,6 @@ struct CSVRowParser: RowDataParser {
return .failure(.noValidRows)
}

// Detect header row: if first row's values match column names, skip it
let startIndex = isHeaderRow(records[0], schema: schema) ? 1 : 0
guard startIndex < records.count else {
return .failure(.noValidRows)
Expand All @@ -119,7 +111,6 @@ struct CSVRowParser: RowDataParser {
let lineNumber = recordIndex + 1
var values = records[recordIndex].map { normalizeValue($0) }

// Handle column count mismatch
if values.count < schema.columnCount {
while values.count < schema.columnCount {
values.append(nil)
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