-
-
Notifications
You must be signed in to change notification settings - Fork 142
Expand file tree
/
Copy pathRedisKeyTreeViewModel.swift
More file actions
161 lines (132 loc) · 5.29 KB
/
RedisKeyTreeViewModel.swift
File metadata and controls
161 lines (132 loc) · 5.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//
// RedisKeyTreeViewModel.swift
// TablePro
//
import Foundation
import Observation
import os
@MainActor @Observable
internal final class RedisKeyTreeViewModel {
private static let logger = Logger(subsystem: "com.TablePro", category: "RedisKeyTree")
private static let maxKeys = 50_000
var rootNodes: [RedisKeyNode] = []
var expandedPrefixes: Set<String> = []
var isLoading = false
var isTruncated = false
var separator: String = ":"
private(set) var allKeys: [(key: String, type: String)] = []
/// Test-only setter for allKeys
var allKeysForTesting: [(key: String, type: String)] {
get { allKeys }
set { allKeys = newValue }
}
func loadKeys(connectionId: UUID, database: String, separator: String) async {
self.separator = separator
isLoading = true
isTruncated = false
defer { isLoading = false }
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
clear()
return
}
do {
// Use KEYS command for simplicity — returns all keys matching pattern
let result = try await driver.execute(query: "KEYS *")
let keyColumnIndex = result.columns.firstIndex(of: "Key") ?? 0
let typeColumnIndex = result.columns.firstIndex(of: "Type") ?? 1
var keys: [(key: String, type: String)] = []
for row in result.rows {
guard keyColumnIndex < row.count,
let keyName = row[keyColumnIndex] else { continue }
let keyType = typeColumnIndex < row.count ? (row[typeColumnIndex] ?? "string") : "string"
keys.append((key: keyName, type: keyType))
if keys.count >= Self.maxKeys { break }
}
isTruncated = keys.count >= Self.maxKeys
allKeys = keys
rootNodes = Self.buildTree(keys: keys, separator: separator)
} catch {
Self.logger.error("Failed to load Redis keys: \(error.localizedDescription, privacy: .public)")
clear()
}
}
func clear() {
rootNodes = []
allKeys = []
expandedPrefixes = []
isTruncated = false
}
func displayNodes(searchText: String) -> [RedisKeyNode] {
guard !searchText.isEmpty else { return rootNodes }
let filtered = allKeys.filter { $0.key.localizedCaseInsensitiveContains(searchText) }
if filtered.isEmpty { return [] }
return Self.buildTree(keys: filtered, separator: separator)
}
// MARK: - Tree Building (Pure Function)
static func buildTree(keys: [(key: String, type: String)], separator: String) -> [RedisKeyNode] {
guard !separator.isEmpty else {
return keys.sorted { $0.key < $1.key }
.map { .key(name: $0.key, fullKey: $0.key, keyType: $0.type) }
}
var root = TrieNode()
for entry in keys {
let parts = entry.key.components(separatedBy: separator)
root.insert(parts: parts, fullKey: entry.key, keyType: entry.type)
}
return root.toRedisKeyNodes(parentPrefix: "", separator: separator)
}
}
// MARK: - Trie for Tree Building
private class TrieNode {
var children: [String: TrieNode] = [:]
var leafKeys: [(fullKey: String, keyType: String)] = []
func insert(parts: [String], fullKey: String, keyType: String) {
guard !parts.isEmpty else {
leafKeys.append((fullKey: fullKey, keyType: keyType))
return
}
if parts.count == 1 {
leafKeys.append((fullKey: fullKey, keyType: keyType))
} else {
let segment = parts[0]
let child = children[segment] ?? TrieNode()
children[segment] = child
child.insert(parts: Array(parts.dropFirst()), fullKey: fullKey, keyType: keyType)
}
}
func toRedisKeyNodes(parentPrefix: String, separator: String) -> [RedisKeyNode] {
var nodes: [RedisKeyNode] = []
let sortedChildren = children.sorted { $0.key < $1.key }
for (segment, child) in sortedChildren {
let fullPrefix = parentPrefix.isEmpty ? "\(segment)\(separator)" : "\(parentPrefix)\(segment)\(separator)"
let childNodes = child.toRedisKeyNodes(parentPrefix: fullPrefix, separator: separator)
let keyCount = child.countLeafKeys()
if !childNodes.isEmpty || !child.leafKeys.isEmpty {
nodes.append(.namespace(
name: segment,
fullPrefix: fullPrefix,
children: childNodes,
keyCount: keyCount
))
}
}
let sortedLeafs = leafKeys.sorted { $0.fullKey < $1.fullKey }
for leaf in sortedLeafs {
let displayName: String
if parentPrefix.isEmpty {
displayName = leaf.fullKey
} else {
displayName = String(leaf.fullKey.dropFirst(parentPrefix.count))
}
nodes.append(.key(name: displayName, fullKey: leaf.fullKey, keyType: leaf.keyType))
}
return nodes
}
func countLeafKeys() -> Int {
var count = leafKeys.count
for child in children.values {
count += child.countLeafKeys()
}
return count
}
}