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 @@ -9,13 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

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

### Fixed

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

## [0.23.2] - 2026-03-24

Expand Down
79 changes: 41 additions & 38 deletions Plugins/RedisDriverPlugin/RedisPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ final class RedisPluginDriver: PluginDatabaseDriver, @unchecked Sendable {

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

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

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

case .keys(let pattern):
let result = try await conn.executeCommand(["KEYS", pattern])
guard let keys = result.stringArrayValue else {
guard let items = result.arrayValue else {
return buildEmptyKeyResult(startTime: startTime)
}
let keys = items.map { redisReplyToString($0) }
let capped = Array(keys.prefix(PluginRowLimits.defaultMax))
let keysTruncated = keys.count > PluginRowLimits.defaultMax
return try await buildKeyBrowseResult(
Expand Down Expand Up @@ -1234,15 +1235,15 @@ private extension RedisPluginDriver {
func formatPreviewReply(_ reply: RedisReply, type: String) -> String? {
switch type.lowercased() {
case "string":
return truncatePreview(reply.stringValue)
return truncatePreview(redisReplyToString(reply))

case "hash":
let array: [String]
let array: [RedisReply]
if case .array(let scanResult) = reply,
scanResult.count == 2,
let items = scanResult[1].stringArrayValue {
let items = scanResult[1].arrayValue {
array = items
} else if let items = reply.stringArrayValue, !items.isEmpty {
} else if let items = reply.arrayValue, !items.isEmpty {
array = items
} else {
return "{}"
Expand All @@ -1251,39 +1252,41 @@ private extension RedisPluginDriver {
var pairs: [String] = []
var idx = 0
while idx + 1 < array.count {
let field = redisReplyToString(array[idx])
let value = redisReplyToString(array[idx + 1])
pairs.append(
"\"\(escapeJsonString(array[idx]))\":\"\(escapeJsonString(array[idx + 1]))\""
"\"\(escapeJsonString(field))\":\"\(escapeJsonString(value))\""
)
idx += 2
}
return truncatePreview("{\(pairs.joined(separator: ","))}")

case "list":
guard let items = reply.stringArrayValue else { return "[]" }
let quoted = items.map { "\"\(escapeJsonString($0))\"" }
guard let items = reply.arrayValue else { return "[]" }
let quoted = items.map { "\"\(escapeJsonString(redisReplyToString($0)))\"" }
return truncatePreview("[\(quoted.joined(separator: ", "))]")

case "set":
let members: [String]
let members: [RedisReply]
if case .array(let scanResult) = reply,
scanResult.count == 2,
let items = scanResult[1].stringArrayValue {
let items = scanResult[1].arrayValue {
members = items
} else if let items = reply.stringArrayValue {
} else if let items = reply.arrayValue {
members = items
} else {
return "[]"
}
let quoted = members.map { "\"\(escapeJsonString($0))\"" }
let quoted = members.map { "\"\(escapeJsonString(redisReplyToString($0)))\"" }
return truncatePreview("[\(quoted.joined(separator: ", "))]")

case "zset":
// Parse WITHSCORES result: alternating member, score pairs
guard let items = reply.stringArrayValue, !items.isEmpty else { return "[]" }
guard let items = reply.arrayValue, !items.isEmpty else { return "[]" }
var pairs: [String] = []
var i = 0
while i + 1 < items.count {
pairs.append("\(items[i]):\(items[i + 1])")
pairs.append("\(redisReplyToString(items[i])):\(redisReplyToString(items[i + 1]))")
i += 2
}
return truncatePreview(pairs.joined(separator: ", "))
Expand All @@ -1296,14 +1299,14 @@ private extension RedisPluginDriver {
var entryStrings: [String] = []
for entry in entries {
guard let parts = entry.arrayValue, parts.count >= 2,
let entryId = parts[0].stringValue,
let fields = parts[1].stringArrayValue else {
let fields = parts[1].arrayValue else {
continue
}
let entryId = redisReplyToString(parts[0])
var fieldPairs: [String] = []
var j = 0
while j + 1 < fields.count {
fieldPairs.append("\(fields[j])=\(fields[j + 1])")
fieldPairs.append("\(redisReplyToString(fields[j]))=\(redisReplyToString(fields[j + 1]))")
j += 2
}
entryStrings.append("\(entryId): \(fieldPairs.joined(separator: ", "))")
Expand Down Expand Up @@ -1435,7 +1438,7 @@ private extension RedisPluginDriver {
}

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

var rows: [[String?]] = []
var i = 0
while i + 1 < array.count {
rows.append([array[i], array[i + 1]])
while i + 1 < items.count {
rows.append([redisReplyToString(items[i]), redisReplyToString(items[i + 1])])
i += 2
}

Expand All @@ -1462,7 +1465,7 @@ private extension RedisPluginDriver {
}

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

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

return PluginQueryResult(
Expand All @@ -1486,7 +1489,7 @@ private extension RedisPluginDriver {
}

func buildSetResult(_ result: RedisReply, startTime: Date) -> PluginQueryResult {
guard let array = result.stringArrayValue else {
guard let items = result.arrayValue else {
return PluginQueryResult(
columns: ["Member"],
columnTypeNames: ["String"],
Expand All @@ -1496,7 +1499,7 @@ private extension RedisPluginDriver {
)
}

let rows = array.map { [$0] as [String?] }
let rows = items.map { [redisReplyToString($0)] as [String?] }

return PluginQueryResult(
columns: ["Member"],
Expand All @@ -1508,7 +1511,7 @@ private extension RedisPluginDriver {
}

func buildSortedSetResult(_ result: RedisReply, withScores: Bool, startTime: Date) -> PluginQueryResult {
guard let array = result.stringArrayValue else {
guard let items = result.arrayValue else {
return PluginQueryResult(
columns: withScores ? ["Member", "Score"] : ["Member"],
columnTypeNames: withScores ? ["String", "Double"] : ["String"],
Expand All @@ -1521,8 +1524,8 @@ private extension RedisPluginDriver {
if withScores {
var rows: [[String?]] = []
var i = 0
while i + 1 < array.count {
rows.append([array[i], array[i + 1]])
while i + 1 < items.count {
rows.append([redisReplyToString(items[i]), redisReplyToString(items[i + 1])])
i += 2
}
return PluginQueryResult(
Expand All @@ -1533,7 +1536,7 @@ private extension RedisPluginDriver {
executionTime: Date().timeIntervalSince(startTime)
)
} else {
let rows = array.map { [$0] as [String?] }
let rows = items.map { [redisReplyToString($0)] as [String?] }
return PluginQueryResult(
columns: ["Member"],
columnTypeNames: ["String"],
Expand All @@ -1558,15 +1561,15 @@ private extension RedisPluginDriver {
var rows: [[String?]] = []
for entry in entries {
guard let entryParts = entry.arrayValue, entryParts.count >= 2,
let entryId = entryParts[0].stringValue,
let fields = entryParts[1].stringArrayValue else {
let fields = entryParts[1].arrayValue else {
continue
}
let entryId = redisReplyToString(entryParts[0])

var fieldPairs: [String] = []
var i = 0
while i + 1 < fields.count {
fieldPairs.append("\(fields[i])=\(fields[i + 1])")
fieldPairs.append("\(redisReplyToString(fields[i]))=\(redisReplyToString(fields[i + 1]))")
i += 2
}
rows.append([entryId, fieldPairs.joined(separator: ", ")])
Expand All @@ -1582,7 +1585,7 @@ private extension RedisPluginDriver {
}

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

var rows: [[String?]] = []
var i = 0
while i + 1 < array.count {
rows.append([array[i], array[i + 1]])
while i + 1 < items.count {
rows.append([redisReplyToString(items[i]), redisReplyToString(items[i + 1])])
i += 2
}

Expand Down
Loading
Loading