Skip to content

Commit 1a933f4

Browse files
authored
fix: Redis hash/list/set/zset/stream views drop non-UTF8 binary values (#447)
* fix: Redis hash/list/set/zset/stream views drop non-UTF8 binary values * fix: eliminate all remaining stringArrayValue usages in Redis plugin * fix: address PR review — string preview, force unwraps in tests
1 parent 96bcd12 commit 1a933f4

3 files changed

Lines changed: 560 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Multi-select connections in Welcome window (Cmd+Click, Shift+Click) with bulk delete (⌘⌫), Move to Group, and multi-connect
13+
- Drag-and-drop connections between groups, reorder within groups, and reorder groups
1214
- ClickHouse, MSSQL, Redis, XLSX Export, MQL Export, and SQL Import now ship as built-in plugins
1315
- Large document safety caps for syntax highlighting (skip >5MB, throttle >50KB)
1416
- Lazy-load full values for LONGTEXT/MEDIUMTEXT/CLOB columns in the detail pane sidebar
1517

1618
### Fixed
1719

1820
- Detail pane showing truncated values for LONGTEXT/MEDIUMTEXT/CLOB columns, preventing correct editing
21+
- Redis hash/list/set/zset/stream views showing empty or misaligned rows when values contained binary, null, or integer types
1922

2023
## [0.23.2] - 2026-03-24
2124

Plugins/RedisDriverPlugin/RedisPluginDriver.swift

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ final class RedisPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
109109

110110
case .keys(let pattern):
111111
let result = try await conn.executeCommand(["KEYS", pattern])
112-
return result.stringArrayValue?.count ?? 0
112+
return result.arrayValue?.count ?? 0
113113

114114
case .dbsize:
115115
let result = try await conn.executeCommand(["DBSIZE"])
@@ -200,7 +200,7 @@ final class RedisPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
200200
// Get total database count from CONFIG GET databases
201201
let configResult = try await conn.executeCommand(["CONFIG", "GET", "databases"])
202202
var maxDatabases = 16
203-
if let array = configResult.stringArrayValue, array.count >= 2, let count = Int(array[1]) {
203+
if let array = configResult.arrayValue, array.count >= 2, let count = Int(redisReplyToString(array[1])) {
204204
maxDatabases = count
205205
}
206206

@@ -310,7 +310,7 @@ final class RedisPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
310310
}
311311
let result = try await conn.executeCommand(["CONFIG", "GET", "databases"])
312312
var maxDatabases = 16
313-
if let array = result.stringArrayValue, array.count >= 2, let count = Int(array[1]) {
313+
if let array = result.arrayValue, array.count >= 2, let count = Int(redisReplyToString(array[1])) {
314314
maxDatabases = count
315315
}
316316
return (0 ..< maxDatabases).map { "db\($0)" }
@@ -627,9 +627,10 @@ private extension RedisPluginDriver {
627627

628628
case .keys(let pattern):
629629
let result = try await conn.executeCommand(["KEYS", pattern])
630-
guard let keys = result.stringArrayValue else {
630+
guard let items = result.arrayValue else {
631631
return buildEmptyKeyResult(startTime: startTime)
632632
}
633+
let keys = items.map { redisReplyToString($0) }
633634
let capped = Array(keys.prefix(PluginRowLimits.defaultMax))
634635
let keysTruncated = keys.count > PluginRowLimits.defaultMax
635636
return try await buildKeyBrowseResult(
@@ -1234,15 +1235,15 @@ private extension RedisPluginDriver {
12341235
func formatPreviewReply(_ reply: RedisReply, type: String) -> String? {
12351236
switch type.lowercased() {
12361237
case "string":
1237-
return truncatePreview(reply.stringValue)
1238+
return truncatePreview(redisReplyToString(reply))
12381239

12391240
case "hash":
1240-
let array: [String]
1241+
let array: [RedisReply]
12411242
if case .array(let scanResult) = reply,
12421243
scanResult.count == 2,
1243-
let items = scanResult[1].stringArrayValue {
1244+
let items = scanResult[1].arrayValue {
12441245
array = items
1245-
} else if let items = reply.stringArrayValue, !items.isEmpty {
1246+
} else if let items = reply.arrayValue, !items.isEmpty {
12461247
array = items
12471248
} else {
12481249
return "{}"
@@ -1251,39 +1252,41 @@ private extension RedisPluginDriver {
12511252
var pairs: [String] = []
12521253
var idx = 0
12531254
while idx + 1 < array.count {
1255+
let field = redisReplyToString(array[idx])
1256+
let value = redisReplyToString(array[idx + 1])
12541257
pairs.append(
1255-
"\"\(escapeJsonString(array[idx]))\":\"\(escapeJsonString(array[idx + 1]))\""
1258+
"\"\(escapeJsonString(field))\":\"\(escapeJsonString(value))\""
12561259
)
12571260
idx += 2
12581261
}
12591262
return truncatePreview("{\(pairs.joined(separator: ","))}")
12601263

12611264
case "list":
1262-
guard let items = reply.stringArrayValue else { return "[]" }
1263-
let quoted = items.map { "\"\(escapeJsonString($0))\"" }
1265+
guard let items = reply.arrayValue else { return "[]" }
1266+
let quoted = items.map { "\"\(escapeJsonString(redisReplyToString($0)))\"" }
12641267
return truncatePreview("[\(quoted.joined(separator: ", "))]")
12651268

12661269
case "set":
1267-
let members: [String]
1270+
let members: [RedisReply]
12681271
if case .array(let scanResult) = reply,
12691272
scanResult.count == 2,
1270-
let items = scanResult[1].stringArrayValue {
1273+
let items = scanResult[1].arrayValue {
12711274
members = items
1272-
} else if let items = reply.stringArrayValue {
1275+
} else if let items = reply.arrayValue {
12731276
members = items
12741277
} else {
12751278
return "[]"
12761279
}
1277-
let quoted = members.map { "\"\(escapeJsonString($0))\"" }
1280+
let quoted = members.map { "\"\(escapeJsonString(redisReplyToString($0)))\"" }
12781281
return truncatePreview("[\(quoted.joined(separator: ", "))]")
12791282

12801283
case "zset":
12811284
// Parse WITHSCORES result: alternating member, score pairs
1282-
guard let items = reply.stringArrayValue, !items.isEmpty else { return "[]" }
1285+
guard let items = reply.arrayValue, !items.isEmpty else { return "[]" }
12831286
var pairs: [String] = []
12841287
var i = 0
12851288
while i + 1 < items.count {
1286-
pairs.append("\(items[i]):\(items[i + 1])")
1289+
pairs.append("\(redisReplyToString(items[i])):\(redisReplyToString(items[i + 1]))")
12871290
i += 2
12881291
}
12891292
return truncatePreview(pairs.joined(separator: ", "))
@@ -1296,14 +1299,14 @@ private extension RedisPluginDriver {
12961299
var entryStrings: [String] = []
12971300
for entry in entries {
12981301
guard let parts = entry.arrayValue, parts.count >= 2,
1299-
let entryId = parts[0].stringValue,
1300-
let fields = parts[1].stringArrayValue else {
1302+
let fields = parts[1].arrayValue else {
13011303
continue
13021304
}
1305+
let entryId = redisReplyToString(parts[0])
13031306
var fieldPairs: [String] = []
13041307
var j = 0
13051308
while j + 1 < fields.count {
1306-
fieldPairs.append("\(fields[j])=\(fields[j + 1])")
1309+
fieldPairs.append("\(redisReplyToString(fields[j]))=\(redisReplyToString(fields[j + 1]))")
13071310
j += 2
13081311
}
13091312
entryStrings.append("\(entryId): \(fieldPairs.joined(separator: ", "))")
@@ -1435,7 +1438,7 @@ private extension RedisPluginDriver {
14351438
}
14361439

14371440
func buildHashResult(_ result: RedisReply, startTime: Date) -> PluginQueryResult {
1438-
guard let array = result.stringArrayValue, !array.isEmpty else {
1441+
guard let items = result.arrayValue, !items.isEmpty else {
14391442
return PluginQueryResult(
14401443
columns: ["Field", "Value"],
14411444
columnTypeNames: ["String", "String"],
@@ -1447,8 +1450,8 @@ private extension RedisPluginDriver {
14471450

14481451
var rows: [[String?]] = []
14491452
var i = 0
1450-
while i + 1 < array.count {
1451-
rows.append([array[i], array[i + 1]])
1453+
while i + 1 < items.count {
1454+
rows.append([redisReplyToString(items[i]), redisReplyToString(items[i + 1])])
14521455
i += 2
14531456
}
14541457

@@ -1462,7 +1465,7 @@ private extension RedisPluginDriver {
14621465
}
14631466

14641467
func buildListResult(_ result: RedisReply, startOffset: Int = 0, startTime: Date) -> PluginQueryResult {
1465-
guard let array = result.stringArrayValue else {
1468+
guard let items = result.arrayValue else {
14661469
return PluginQueryResult(
14671470
columns: ["Index", "Value"],
14681471
columnTypeNames: ["Int64", "String"],
@@ -1472,8 +1475,8 @@ private extension RedisPluginDriver {
14721475
)
14731476
}
14741477

1475-
let rows = array.enumerated().map { index, value -> [String?] in
1476-
[String(startOffset + index), value]
1478+
let rows = items.enumerated().map { index, item -> [String?] in
1479+
[String(startOffset + index), redisReplyToString(item)]
14771480
}
14781481

14791482
return PluginQueryResult(
@@ -1486,7 +1489,7 @@ private extension RedisPluginDriver {
14861489
}
14871490

14881491
func buildSetResult(_ result: RedisReply, startTime: Date) -> PluginQueryResult {
1489-
guard let array = result.stringArrayValue else {
1492+
guard let items = result.arrayValue else {
14901493
return PluginQueryResult(
14911494
columns: ["Member"],
14921495
columnTypeNames: ["String"],
@@ -1496,7 +1499,7 @@ private extension RedisPluginDriver {
14961499
)
14971500
}
14981501

1499-
let rows = array.map { [$0] as [String?] }
1502+
let rows = items.map { [redisReplyToString($0)] as [String?] }
15001503

15011504
return PluginQueryResult(
15021505
columns: ["Member"],
@@ -1508,7 +1511,7 @@ private extension RedisPluginDriver {
15081511
}
15091512

15101513
func buildSortedSetResult(_ result: RedisReply, withScores: Bool, startTime: Date) -> PluginQueryResult {
1511-
guard let array = result.stringArrayValue else {
1514+
guard let items = result.arrayValue else {
15121515
return PluginQueryResult(
15131516
columns: withScores ? ["Member", "Score"] : ["Member"],
15141517
columnTypeNames: withScores ? ["String", "Double"] : ["String"],
@@ -1521,8 +1524,8 @@ private extension RedisPluginDriver {
15211524
if withScores {
15221525
var rows: [[String?]] = []
15231526
var i = 0
1524-
while i + 1 < array.count {
1525-
rows.append([array[i], array[i + 1]])
1527+
while i + 1 < items.count {
1528+
rows.append([redisReplyToString(items[i]), redisReplyToString(items[i + 1])])
15261529
i += 2
15271530
}
15281531
return PluginQueryResult(
@@ -1533,7 +1536,7 @@ private extension RedisPluginDriver {
15331536
executionTime: Date().timeIntervalSince(startTime)
15341537
)
15351538
} else {
1536-
let rows = array.map { [$0] as [String?] }
1539+
let rows = items.map { [redisReplyToString($0)] as [String?] }
15371540
return PluginQueryResult(
15381541
columns: ["Member"],
15391542
columnTypeNames: ["String"],
@@ -1558,15 +1561,15 @@ private extension RedisPluginDriver {
15581561
var rows: [[String?]] = []
15591562
for entry in entries {
15601563
guard let entryParts = entry.arrayValue, entryParts.count >= 2,
1561-
let entryId = entryParts[0].stringValue,
1562-
let fields = entryParts[1].stringArrayValue else {
1564+
let fields = entryParts[1].arrayValue else {
15631565
continue
15641566
}
1567+
let entryId = redisReplyToString(entryParts[0])
15651568

15661569
var fieldPairs: [String] = []
15671570
var i = 0
15681571
while i + 1 < fields.count {
1569-
fieldPairs.append("\(fields[i])=\(fields[i + 1])")
1572+
fieldPairs.append("\(redisReplyToString(fields[i]))=\(redisReplyToString(fields[i + 1]))")
15701573
i += 2
15711574
}
15721575
rows.append([entryId, fieldPairs.joined(separator: ", ")])
@@ -1582,7 +1585,7 @@ private extension RedisPluginDriver {
15821585
}
15831586

15841587
func buildConfigResult(_ result: RedisReply, startTime: Date) -> PluginQueryResult {
1585-
guard let array = result.stringArrayValue, !array.isEmpty else {
1588+
guard let items = result.arrayValue, !items.isEmpty else {
15861589
return PluginQueryResult(
15871590
columns: ["Parameter", "Value"],
15881591
columnTypeNames: ["String", "String"],
@@ -1594,8 +1597,8 @@ private extension RedisPluginDriver {
15941597

15951598
var rows: [[String?]] = []
15961599
var i = 0
1597-
while i + 1 < array.count {
1598-
rows.append([array[i], array[i + 1]])
1600+
while i + 1 < items.count {
1601+
rows.append([redisReplyToString(items[i]), redisReplyToString(items[i + 1])])
15991602
i += 2
16001603
}
16011604

0 commit comments

Comments
 (0)