Skip to content

Commit 2326e35

Browse files
committed
release: v0.13.0
1 parent 1704704 commit 2326e35

9 files changed

Lines changed: 236 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.13.0] - 2026-03-04
11+
1012
### Added
1113

1214
- Redis database support with key-value browsing, database-level sidebar (db0–db15), TTL management, and interactive CLI
@@ -645,7 +647,8 @@ TablePro is a native macOS database client built with SwiftUI and AppKit, design
645647
- Custom SQL query templates
646648
- Performance optimized for large datasets
647649

648-
[Unreleased]: https://github.com/datlechin/tablepro/compare/v0.12.0...HEAD
650+
[Unreleased]: https://github.com/datlechin/tablepro/compare/v0.13.0...HEAD
651+
[0.13.0]: https://github.com/datlechin/tablepro/compare/v0.12.0...v0.13.0
649652
[0.12.0]: https://github.com/datlechin/tablepro/compare/v0.11.1...v0.12.0
650653
[0.11.1]: https://github.com/datlechin/tablepro/compare/v0.11.0...v0.11.1
651654
[0.11.0]: https://github.com/datlechin/tablepro/compare/v0.10.0...v0.11.0

TablePro.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@
379379
CODE_SIGN_IDENTITY = "Apple Development";
380380
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
381381
CODE_SIGN_STYLE = Automatic;
382-
CURRENT_PROJECT_VERSION = 24;
382+
CURRENT_PROJECT_VERSION = 25;
383383
DEAD_CODE_STRIPPING = YES;
384384
DEVELOPMENT_TEAM = D7HJ5TFYCU;
385385
ENABLE_APP_SANDBOX = NO;
@@ -413,7 +413,7 @@
413413
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
414414
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/Libs";
415415
MACOSX_DEPLOYMENT_TARGET = 14.0;
416-
MARKETING_VERSION = 0.12.0;
416+
MARKETING_VERSION = 0.13.0;
417417
OTHER_LDFLAGS = (
418418
"-force_load",
419419
"$(PROJECT_DIR)/Libs/libmariadb.a",
@@ -471,7 +471,7 @@
471471
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
472472
CODE_SIGN_STYLE = Automatic;
473473
COPY_PHASE_STRIP = YES;
474-
CURRENT_PROJECT_VERSION = 24;
474+
CURRENT_PROJECT_VERSION = 25;
475475
DEAD_CODE_STRIPPING = YES;
476476
DEPLOYMENT_POSTPROCESSING = YES;
477477
DEVELOPMENT_TEAM = D7HJ5TFYCU;
@@ -506,7 +506,7 @@
506506
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
507507
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/Libs";
508508
MACOSX_DEPLOYMENT_TARGET = 14.0;
509-
MARKETING_VERSION = 0.12.0;
509+
MARKETING_VERSION = 0.13.0;
510510
OTHER_LDFLAGS = (
511511
"-force_load",
512512
"$(PROJECT_DIR)/Libs/libmariadb.a",

TablePro/Core/Database/RedisConnection.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -462,8 +462,7 @@ final class RedisConnection: @unchecked Sendable {
462462
) async throws -> (cursor: Int, keys: [String]) {
463463
#if canImport(CRedis)
464464
resetCancellation()
465-
return try await withCheckedThrowingContinuation {
466-
[self] (cont: CheckedContinuation<(cursor: Int, keys: [String]), Error>) in
465+
return try await withCheckedThrowingContinuation { [self] (cont: CheckedContinuation<(cursor: Int, keys: [String]), Error>) in
467466
queue.async { [self] in
468467
guard !isShuttingDown, context != nil else {
469468
cont.resume(throwing: RedisError.notConnected)
@@ -704,7 +703,6 @@ final class RedisConnection: @unchecked Sendable {
704703

705704
#if canImport(CRedis)
706705
private extension RedisConnection {
707-
708706
func connectSSL(_ ctx: UnsafeMutablePointer<redisContext>) throws {
709707
var sslError = redisSSLContextError(0)
710708

TablePro/Core/Database/RedisDriver+ResultBuilding.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ extension RedisDriver {
5555
private static let previewLimit = 100
5656

5757
/// Maximum character length for preview strings before truncation
58-
private static let previewMaxChars = 1000
58+
private static let previewMaxChars = 1_000
5959

6060
/// Fetch the value for a key based on its type, serialized as a raw string.
6161
/// Matches TablePlus behavior: hashes as JSON objects, lists/sets as JSON arrays.

TablePro/Core/Database/RedisDriver.swift

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,37 @@ private extension RedisDriver {
496496
_ operation: RedisOperation,
497497
connection conn: RedisConnection,
498498
startTime: Date
499+
) async throws -> QueryResult {
500+
switch operation {
501+
case .get, .set, .del, .keys, .scan, .type, .ttl, .pttl, .expire, .persist, .rename, .exists:
502+
return try await executeKeyOperation(operation, connection: conn, startTime: startTime)
503+
504+
case .hget, .hset, .hgetall, .hdel:
505+
return try await executeHashOperation(operation, connection: conn, startTime: startTime)
506+
507+
case .lrange, .lpush, .rpush, .llen:
508+
return try await executeListOperation(operation, connection: conn, startTime: startTime)
509+
510+
case .smembers, .sadd, .srem, .scard:
511+
return try await executeSetOperation(operation, connection: conn, startTime: startTime)
512+
513+
case .zrange, .zadd, .zrem, .zcard:
514+
return try await executeSortedSetOperation(operation, connection: conn, startTime: startTime)
515+
516+
case .xrange, .xlen:
517+
return try await executeStreamOperation(operation, connection: conn, startTime: startTime)
518+
519+
case .ping, .info, .dbsize, .flushdb, .select, .configGet, .configSet, .command, .multi, .exec, .discard:
520+
return try await executeServerOperation(operation, connection: conn, startTime: startTime)
521+
}
522+
}
523+
524+
// MARK: - Key Operations
525+
526+
func executeKeyOperation(
527+
_ operation: RedisOperation,
528+
connection conn: RedisConnection,
529+
startTime: Date
499530
) async throws -> QueryResult {
500531
switch operation {
501532
case .get(let key):
@@ -612,6 +643,19 @@ private extension RedisDriver {
612643
error: nil
613644
)
614645

646+
default:
647+
fatalError("Unexpected operation in executeKeyOperation")
648+
}
649+
}
650+
651+
// MARK: - Hash Operations
652+
653+
func executeHashOperation(
654+
_ operation: RedisOperation,
655+
connection conn: RedisConnection,
656+
startTime: Date
657+
) async throws -> QueryResult {
658+
switch operation {
615659
case .hget(let key, let field):
616660
let result = try await conn.executeCommand(["HGET", key, field])
617661
let value = result .stringValue
@@ -657,6 +701,19 @@ private extension RedisDriver {
657701
error: nil
658702
)
659703

704+
default:
705+
fatalError("Unexpected operation in executeHashOperation")
706+
}
707+
}
708+
709+
// MARK: - List Operations
710+
711+
func executeListOperation(
712+
_ operation: RedisOperation,
713+
connection conn: RedisConnection,
714+
startTime: Date
715+
) async throws -> QueryResult {
716+
switch operation {
660717
case .lrange(let key, let start, let stop):
661718
let result = try await conn.executeCommand(["LRANGE", key, String(start), String(stop)])
662719
return buildListResult(result, startTime: startTime)
@@ -699,6 +756,19 @@ private extension RedisDriver {
699756
error: nil
700757
)
701758

759+
default:
760+
fatalError("Unexpected operation in executeListOperation")
761+
}
762+
}
763+
764+
// MARK: - Set Operations
765+
766+
func executeSetOperation(
767+
_ operation: RedisOperation,
768+
connection conn: RedisConnection,
769+
startTime: Date
770+
) async throws -> QueryResult {
771+
switch operation {
702772
case .smembers(let key):
703773
let result = try await conn.executeCommand(["SMEMBERS", key])
704774
return buildSetResult(result, startTime: startTime)
@@ -741,6 +811,19 @@ private extension RedisDriver {
741811
error: nil
742812
)
743813

814+
default:
815+
fatalError("Unexpected operation in executeSetOperation")
816+
}
817+
}
818+
819+
// MARK: - Sorted Set Operations
820+
821+
func executeSortedSetOperation(
822+
_ operation: RedisOperation,
823+
connection conn: RedisConnection,
824+
startTime: Date
825+
) async throws -> QueryResult {
826+
switch operation {
744827
case .zrange(let key, let start, let stop, let withScores):
745828
var args = ["ZRANGE", key, String(start), String(stop)]
746829
if withScores { args.append("WITHSCORES") }
@@ -788,6 +871,19 @@ private extension RedisDriver {
788871
error: nil
789872
)
790873

874+
default:
875+
fatalError("Unexpected operation in executeSortedSetOperation")
876+
}
877+
}
878+
879+
// MARK: - Stream Operations
880+
881+
func executeStreamOperation(
882+
_ operation: RedisOperation,
883+
connection conn: RedisConnection,
884+
startTime: Date
885+
) async throws -> QueryResult {
886+
switch operation {
791887
case .xrange(let key, let start, let end, let count):
792888
var args = ["XRANGE", key, start, end]
793889
if let c = count { args += ["COUNT", String(c)] }
@@ -806,6 +902,19 @@ private extension RedisDriver {
806902
error: nil
807903
)
808904

905+
default:
906+
fatalError("Unexpected operation in executeStreamOperation")
907+
}
908+
}
909+
910+
// MARK: - Server Operations
911+
912+
func executeServerOperation(
913+
_ operation: RedisOperation,
914+
connection conn: RedisConnection,
915+
startTime: Date
916+
) async throws -> QueryResult {
917+
switch operation {
809918
case .ping:
810919
_ = try await conn.executeCommand(["PING"])
811920
return QueryResult(
@@ -874,6 +983,9 @@ private extension RedisDriver {
874983
case .discard:
875984
_ = try await conn.executeCommand(["DISCARD"])
876985
return buildStatusResult("OK", startTime: startTime)
986+
987+
default:
988+
fatalError("Unexpected operation in executeServerOperation")
877989
}
878990
}
879991
}

TablePro/Core/Redis/RedisCommandParser.swift

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,38 @@ struct RedisCommandParser {
110110
let command = first.uppercased()
111111
let args = Array(tokens.dropFirst())
112112

113+
switch command {
114+
case "GET", "SET", "DEL", "KEYS", "SCAN", "TYPE", "TTL", "PTTL",
115+
"EXPIRE", "PERSIST", "RENAME", "EXISTS":
116+
return try parseKeyCommand(command, args: args)
117+
118+
case "HGET", "HSET", "HGETALL", "HDEL":
119+
return try parseHashCommand(command, args: args)
120+
121+
case "LRANGE", "LPUSH", "RPUSH", "LLEN":
122+
return try parseListCommand(command, args: args)
123+
124+
case "SMEMBERS", "SADD", "SREM", "SCARD":
125+
return try parseSetCommand(command, args: args)
126+
127+
case "ZRANGE", "ZADD", "ZREM", "ZCARD":
128+
return try parseSortedSetCommand(command, args: args)
129+
130+
case "XRANGE", "XLEN":
131+
return try parseStreamCommand(command, args: args)
132+
133+
case "PING", "INFO", "DBSIZE", "FLUSHDB", "SELECT", "CONFIG",
134+
"MULTI", "EXEC", "DISCARD":
135+
return try parseServerCommand(command, args: args, tokens: tokens)
136+
137+
default:
138+
return .command(args: tokens)
139+
}
140+
}
141+
142+
// MARK: - Key Commands
143+
144+
private static func parseKeyCommand(_ command: String, args: [String]) throws -> RedisOperation {
113145
switch command {
114146
case "GET":
115147
guard args.count >= 1 else { throw RedisParseError.missingArgument("GET requires a key") }
@@ -166,6 +198,15 @@ struct RedisCommandParser {
166198
guard !args.isEmpty else { throw RedisParseError.missingArgument("EXISTS requires at least one key") }
167199
return .exists(keys: args)
168200

201+
default:
202+
throw RedisParseError.invalidArgument("Unknown key command: \(command)")
203+
}
204+
}
205+
206+
// MARK: - Hash Commands
207+
208+
private static func parseHashCommand(_ command: String, args: [String]) throws -> RedisOperation {
209+
switch command {
169210
case "HGET":
170211
guard args.count >= 2 else { throw RedisParseError.missingArgument("HGET requires key and field") }
171212
return .hget(key: args[0], field: args[1])
@@ -190,6 +231,15 @@ struct RedisCommandParser {
190231
guard args.count >= 2 else { throw RedisParseError.missingArgument("HDEL requires key and at least one field") }
191232
return .hdel(key: args[0], fields: Array(args.dropFirst()))
192233

234+
default:
235+
throw RedisParseError.invalidArgument("Unknown hash command: \(command)")
236+
}
237+
}
238+
239+
// MARK: - List Commands
240+
241+
private static func parseListCommand(_ command: String, args: [String]) throws -> RedisOperation {
242+
switch command {
193243
case "LRANGE":
194244
guard args.count >= 3 else { throw RedisParseError.missingArgument("LRANGE requires key, start, and stop") }
195245
guard let start = Int(args[1]), let stop = Int(args[2]) else {
@@ -209,6 +259,15 @@ struct RedisCommandParser {
209259
guard args.count >= 1 else { throw RedisParseError.missingArgument("LLEN requires a key") }
210260
return .llen(key: args[0])
211261

262+
default:
263+
throw RedisParseError.invalidArgument("Unknown list command: \(command)")
264+
}
265+
}
266+
267+
// MARK: - Set Commands
268+
269+
private static func parseSetCommand(_ command: String, args: [String]) throws -> RedisOperation {
270+
switch command {
212271
case "SMEMBERS":
213272
guard args.count >= 1 else { throw RedisParseError.missingArgument("SMEMBERS requires a key") }
214273
return .smembers(key: args[0])
@@ -225,6 +284,15 @@ struct RedisCommandParser {
225284
guard args.count >= 1 else { throw RedisParseError.missingArgument("SCARD requires a key") }
226285
return .scard(key: args[0])
227286

287+
default:
288+
throw RedisParseError.invalidArgument("Unknown set command: \(command)")
289+
}
290+
}
291+
292+
// MARK: - Sorted Set Commands
293+
294+
private static func parseSortedSetCommand(_ command: String, args: [String]) throws -> RedisOperation {
295+
switch command {
228296
case "ZRANGE":
229297
guard args.count >= 3 else { throw RedisParseError.missingArgument("ZRANGE requires key, start, and stop") }
230298
guard let start = Int(args[1]), let stop = Int(args[2]) else {
@@ -256,6 +324,15 @@ struct RedisCommandParser {
256324
guard args.count >= 1 else { throw RedisParseError.missingArgument("ZCARD requires a key") }
257325
return .zcard(key: args[0])
258326

327+
default:
328+
throw RedisParseError.invalidArgument("Unknown sorted set command: \(command)")
329+
}
330+
}
331+
332+
// MARK: - Stream Commands
333+
334+
private static func parseStreamCommand(_ command: String, args: [String]) throws -> RedisOperation {
335+
switch command {
259336
case "XRANGE":
260337
guard args.count >= 3 else { throw RedisParseError.missingArgument("XRANGE requires key, start, and end") }
261338
var count: Int?
@@ -268,6 +345,17 @@ struct RedisCommandParser {
268345
guard args.count >= 1 else { throw RedisParseError.missingArgument("XLEN requires a key") }
269346
return .xlen(key: args[0])
270347

348+
default:
349+
throw RedisParseError.invalidArgument("Unknown stream command: \(command)")
350+
}
351+
}
352+
353+
// MARK: - Server Commands
354+
355+
private static func parseServerCommand(
356+
_ command: String, args: [String], tokens: [String]
357+
) throws -> RedisOperation {
358+
switch command {
271359
case "PING":
272360
return .ping
273361

@@ -313,7 +401,7 @@ struct RedisCommandParser {
313401
return .discard
314402

315403
default:
316-
return .command(args: tokens)
404+
throw RedisParseError.invalidArgument("Unknown server command: \(command)")
317405
}
318406
}
319407

0 commit comments

Comments
 (0)