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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Auto-fit column width: double-click column divider or right-click → "Size to Fit"
- Collapsible results panel (`Cmd+Opt+R`), multiple result tabs for multi-statement queries, result pinning
- Inline error banner for query errors
- JSON syntax highlighting and brace matching in Details sidebar and JSON editor popover
- Database-aware SQL functions in field menu (MySQL, PostgreSQL, SQLite, SQL Server, ClickHouse)

### Changed

- Replace GCD dispatch patterns with Swift structured concurrency
- Refactor Details sidebar into modular field editor architecture with extracted editor components

### Fixed

Expand Down
58 changes: 58 additions & 0 deletions TablePro/Core/Services/Query/SQLFunctionProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// SQLFunctionProvider.swift
// TablePro

internal enum SQLFunctionProvider {
internal struct SQLFunction {
let label: String
let expression: String
}

static func functions(for databaseType: DatabaseType) -> [SQLFunction] {
if databaseType == .mysql || databaseType == .mariadb {
return [
SQLFunction(label: "NOW()", expression: "NOW()"),
SQLFunction(label: "CURRENT_TIMESTAMP()", expression: "CURRENT_TIMESTAMP()"),
SQLFunction(label: "CURDATE()", expression: "CURDATE()"),
SQLFunction(label: "CURTIME()", expression: "CURTIME()"),
SQLFunction(label: "UTC_TIMESTAMP()", expression: "UTC_TIMESTAMP()"),
SQLFunction(label: "UUID()", expression: "UUID()")
]
} else if databaseType == .postgresql || databaseType == .redshift {
return [
SQLFunction(label: "now()", expression: "now()"),
SQLFunction(label: "CURRENT_TIMESTAMP", expression: "CURRENT_TIMESTAMP"),
SQLFunction(label: "CURRENT_DATE", expression: "CURRENT_DATE"),
SQLFunction(label: "CURRENT_TIME", expression: "CURRENT_TIME"),
SQLFunction(label: "gen_random_uuid()", expression: "gen_random_uuid()")
]
} else if databaseType == .sqlite || databaseType == .duckdb || databaseType == .cloudflareD1 {
return [
SQLFunction(label: "datetime('now')", expression: "datetime('now')"),
SQLFunction(label: "date('now')", expression: "date('now')"),
SQLFunction(label: "time('now')", expression: "time('now')"),
SQLFunction(label: "datetime('now','localtime')", expression: "datetime('now','localtime')")
]
} else if databaseType == .mssql {
return [
SQLFunction(label: "GETDATE()", expression: "GETDATE()"),
SQLFunction(label: "GETUTCDATE()", expression: "GETUTCDATE()"),
SQLFunction(label: "SYSDATETIME()", expression: "SYSDATETIME()"),
SQLFunction(label: "NEWID()", expression: "NEWID()")
]
} else if databaseType == .clickhouse {
return [
SQLFunction(label: "now()", expression: "now()"),
SQLFunction(label: "today()", expression: "today()"),
SQLFunction(label: "yesterday()", expression: "yesterday()"),
SQLFunction(label: "generateUUIDv4()", expression: "generateUUIDv4()")
]
} else {
return [
SQLFunction(label: "CURRENT_TIMESTAMP", expression: "CURRENT_TIMESTAMP"),
SQLFunction(label: "CURRENT_DATE", expression: "CURRENT_DATE"),
SQLFunction(label: "CURRENT_TIME", expression: "CURRENT_TIME")
]
}
}
}
9 changes: 9 additions & 0 deletions TablePro/Extensions/String+JSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
import Foundation

extension String {
/// Returns true if this string looks like a JSON object or array (starts with `{`/`[` and parses successfully).
/// Only checks objects and arrays to avoid false positives with bare primitives like `"hello"`, `123`, `true`.
var looksLikeJson: Bool {
let trimmed = unicodeScalars.first
guard trimmed == "{" || trimmed == "[" else { return false }
guard let data = data(using: .utf8) else { return false }
return (try? JSONSerialization.jsonObject(with: data)) != nil
}

/// Returns a pretty-printed version of this string if it contains valid JSON, or nil otherwise.
func prettyPrintedAsJson() -> String? {
guard let data = data(using: .utf8),
Expand Down
17 changes: 12 additions & 5 deletions TablePro/Models/UI/MultiRowEditState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import Foundation
import Observation

/// Represents the edit state for a single field across multiple rows
struct FieldEditState {
struct FieldEditState: Identifiable {
var id = UUID()
let columnIndex: Int
let columnName: String
let columnTypeEnum: ColumnType
Expand Down Expand Up @@ -101,8 +102,8 @@ final class MultiRowEditState {
}

// Check if all values are the same
let uniqueValues = Set(values.map { $0 ?? "__NULL__" })
let hasMultipleValues = uniqueValues.count > 1
let allSame = values.dropFirst().allSatisfy { $0 == values.first }
let hasMultipleValues = !allSame

let originalValue: String?
if hasMultipleValues {
Expand All @@ -113,6 +114,7 @@ final class MultiRowEditState {
}

// Preserve pending edits if data hasn't changed
var preservedId: UUID?
var pendingValue: String?
var isPendingNull = false
var isPendingDefault = false
Expand All @@ -126,6 +128,7 @@ final class MultiRowEditState {
let oldField = fields[colIndex]
// Preserve pending edits when original data matches
if oldField.originalValue == originalValue && oldField.hasMultipleValues == hasMultipleValues {
preservedId = oldField.id
pendingValue = oldField.pendingValue
isPendingNull = oldField.isPendingNull
isPendingDefault = oldField.isPendingDefault
Expand All @@ -143,7 +146,7 @@ final class MultiRowEditState {
pendingValue = originalValue ?? ""
}

newFields.append(FieldEditState(
var newField = FieldEditState(
columnIndex: colIndex,
columnName: columnName,
columnTypeEnum: columnTypeEnum,
Expand All @@ -155,7 +158,11 @@ final class MultiRowEditState {
isPendingDefault: isPendingDefault,
isTruncated: preservedIsTruncated,
isLoadingFullValue: preservedIsLoadingFullValue
))
)
if let preservedId {
newField.id = preservedId
}
newFields.append(newField)
}

self.fields = newFields
Expand Down
Loading
Loading