Skip to content

Commit c4c6642

Browse files
committed
fix: iOS data search — crash, CAST, BLOB filter, ILIKE, accessibility
- Fix stale rows[index] crash in context menu/swipe closures (use captured row value) - Add CAST for MSSQL (NVARCHAR(MAX)) and ClickHouse (toString), default CAST(AS TEXT) - Filter BLOB/BYTEA/BINARY columns from search (produce garbage results) - Cancel previous search task before starting new one - Use ILIKE for PostgreSQL case-insensitive search - Add VoiceOver accessibility to RowCard and toolbar buttons - Keep bottom toolbar visible during active search/filter with empty results
1 parent c65e313 commit c4c6642

2 files changed

Lines changed: 37 additions & 13 deletions

File tree

TableProMobile/TableProMobile/Helpers/SQLBuilder.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,15 @@ enum SQLBuilder {
231231
castExpr = "CAST(\(quotedCol) AS CHAR)"
232232
case .postgresql, .redshift:
233233
castExpr = "CAST(\(quotedCol) AS TEXT)"
234+
case .mssql:
235+
castExpr = "CAST(\(quotedCol) AS NVARCHAR(MAX))"
236+
case .clickhouse:
237+
castExpr = "toString(\(quotedCol))"
234238
default:
235-
castExpr = quotedCol
239+
castExpr = "CAST(\(quotedCol) AS TEXT)"
236240
}
237-
return "\(castExpr) LIKE '%\(pattern)%'\(likeEscape)"
241+
let likeOp = (type == .postgresql || type == .redshift) ? "ILIKE" : "LIKE"
242+
return "\(castExpr) \(likeOp) '%\(pattern)%'\(likeEscape)"
238243
}
239244

240245
return "(\(conditions.joined(separator: " OR ")))"

TableProMobile/TableProMobile/Views/DataBrowserView.swift

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ struct DataBrowserView: View {
3232
@State private var goToPageInput = ""
3333
@State private var searchText = ""
3434
@State private var activeSearchText = ""
35+
@State private var searchTask: Task<Void, Never>?
3536
@State private var filters: [TableFilter] = []
3637
@State private var filterLogicMode: FilterLogicMode = .and
3738
@State private var showFilterSheet = false
@@ -100,7 +101,7 @@ struct DataBrowserView: View {
100101
var body: some View {
101102
searchableContent
102103
.toolbar { topToolbar }
103-
.toolbar(rows.isEmpty ? .hidden : .visible, for: .bottomBar)
104+
.toolbar(rows.isEmpty && !hasActiveSearch && !hasActiveFilters ? .hidden : .visible, for: .bottomBar)
104105
.toolbar { paginationToolbar }
105106
.task { await loadData(isInitial: true) }
106107
.sheet(isPresented: $showInsertSheet) { insertSheet }
@@ -250,7 +251,7 @@ struct DataBrowserView: View {
250251
ForEach(ExportFormat.allCases) { format in
251252
Button(format.rawValue) {
252253
let text = ClipboardExporter.exportRow(
253-
columns: columns, row: rows[index],
254+
columns: columns, row: row,
254255
format: format, tableName: table.name
255256
)
256257
ClipboardExporter.copyToClipboard(text)
@@ -260,17 +261,17 @@ struct DataBrowserView: View {
260261
if !foreignKeys.isEmpty {
261262
let rowFKs = foreignKeys.filter { fk in
262263
guard let colIndex = columns.firstIndex(where: { $0.name == fk.column }),
263-
colIndex < rows[index].count,
264-
rows[index][colIndex] != nil else { return false }
264+
colIndex < row.count,
265+
row[colIndex] != nil else { return false }
265266
return true
266267
}
267268
if !rowFKs.isEmpty {
268269
Divider()
269270
ForEach(rowFKs, id: \.name) { fk in
270271
Button {
271272
if let colIndex = columns.firstIndex(where: { $0.name == fk.column }),
272-
colIndex < rows[index].count,
273-
let value = rows[index][colIndex] {
273+
colIndex < row.count,
274+
let value = row[colIndex] {
274275
fkPreviewItem = FKPreviewItem(fk: fk, value: value)
275276
}
276277
} label: {
@@ -283,7 +284,7 @@ struct DataBrowserView: View {
283284
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
284285
if !isView && hasPrimaryKeys && !connection.safeModeLevel.blocksWrites {
285286
Button(role: .destructive) {
286-
deleteTarget = primaryKeyValues(for: rows[index])
287+
deleteTarget = primaryKeyValues(for: row)
287288
showDeleteConfirmation = true
288289
} label: {
289290
Label("Delete", systemImage: "trash")
@@ -320,6 +321,7 @@ struct DataBrowserView: View {
320321
}
321322
} label: {
322323
Image(systemName: "square.and.arrow.up")
324+
.accessibilityLabel(Text("Export"))
323325
}
324326
.disabled(rows.isEmpty)
325327
}
@@ -344,6 +346,7 @@ struct DataBrowserView: View {
344346
Image(systemName: sortState.isSorting
345347
? "arrow.up.arrow.down.circle.fill"
346348
: "arrow.up.arrow.down.circle")
349+
.accessibilityLabel(Text("Sort"))
347350
}
348351
.disabled(columns.isEmpty)
349352
}
@@ -352,6 +355,7 @@ struct DataBrowserView: View {
352355
Image(systemName: hasActiveFilters
353356
? "line.3.horizontal.decrease.circle.fill"
354357
: "line.3.horizontal.decrease.circle")
358+
.accessibilityLabel(Text("Filter"))
355359
}
356360
.badge(activeFilterCount)
357361
}
@@ -360,12 +364,14 @@ struct DataBrowserView: View {
360364
StructureView(table: table, session: session, databaseType: connection.type)
361365
} label: {
362366
Image(systemName: "info.circle")
367+
.accessibilityLabel(Text("Table Structure"))
363368
}
364369
}
365370
if !isView && !connection.safeModeLevel.blocksWrites {
366371
ToolbarItem(placement: .primaryAction) {
367372
Button { showInsertSheet = true } label: {
368373
Image(systemName: "plus")
374+
.accessibilityLabel(Text("Insert Row"))
369375
}
370376
}
371377
}
@@ -452,9 +458,14 @@ struct DataBrowserView: View {
452458
do {
453459
let query: String
454460
if hasActiveSearch {
461+
let searchableColumns = columns.filter { col in
462+
let upper = col.typeName.uppercased()
463+
return !upper.contains("BLOB") && !upper.contains("BYTEA") && !upper.contains("BINARY")
464+
&& !upper.contains("VARBINARY") && !upper.contains("IMAGE")
465+
}
455466
query = SQLBuilder.buildSearchSelect(
456467
table: table.name, type: connection.type,
457-
searchText: activeSearchText, searchColumns: columns,
468+
searchText: activeSearchText, searchColumns: searchableColumns,
458469
filters: filters, logicMode: filterLogicMode,
459470
sortState: sortState,
460471
limit: pagination.pageSize, offset: pagination.currentOffset
@@ -511,9 +522,14 @@ struct DataBrowserView: View {
511522
do {
512523
let countQuery: String
513524
if hasActiveSearch {
525+
let searchableColumns = columns.filter { col in
526+
let upper = col.typeName.uppercased()
527+
return !upper.contains("BLOB") && !upper.contains("BYTEA") && !upper.contains("BINARY")
528+
&& !upper.contains("VARBINARY") && !upper.contains("IMAGE")
529+
}
514530
countQuery = SQLBuilder.buildSearchCount(
515531
table: table.name, type: connection.type,
516-
searchText: activeSearchText, searchColumns: columns,
532+
searchText: activeSearchText, searchColumns: searchableColumns,
517533
filters: filters, logicMode: filterLogicMode
518534
)
519535
} else if hasActiveFilters {
@@ -609,15 +625,17 @@ struct DataBrowserView: View {
609625
guard hasActiveSearch, !columns.isEmpty else { return }
610626
pagination.currentPage = 0
611627
pagination.totalRows = nil
612-
Task { await loadData() }
628+
searchTask?.cancel()
629+
searchTask = Task { await loadData() }
613630
}
614631

615632
private func clearSearch() {
616633
searchText = ""
617634
activeSearchText = ""
618635
pagination.currentPage = 0
619636
pagination.totalRows = nil
620-
Task { await loadData() }
637+
searchTask?.cancel()
638+
searchTask = Task { await loadData() }
621639
}
622640

623641
private func applyFilters() {
@@ -841,5 +859,6 @@ private struct RowCard: View {
841859
}
842860
}
843861
.padding(.vertical, 2)
862+
.accessibilityElement(children: .combine)
844863
}
845864
}

0 commit comments

Comments
 (0)