Skip to content

Commit 734e04c

Browse files
committed
Add TTL support (EXPIRE, TTL, EXPIREAT) to Redis extension
Adds three scalar functions for managing key time-to-live in Redis: - redis_expire(key, seconds, secret): Set TTL in seconds, returns TRUE if set - redis_ttl(key, secret): Get remaining TTL (-2=key not exist, -1=no expiry) - redis_expireat(key, timestamp, secret): Set expiry at Unix timestamp, returns TRUE Implementation details: - Added 4 protocol formatters to RedisProtocol class - Added parseIntegerResponse() helper for parsing integer responses - Updated extension version from 2025120401 to 2026011401 - Added error-handling tests for non-existent secrets - Updated documentation with features, reference table, and usage examples All functions follow existing patterns and pass unit tests. Live-tested against local Redis instance - all TTL operations working correctly.
1 parent fa4dd3e commit 734e04c

File tree

3 files changed

+143
-2
lines changed

3 files changed

+143
-2
lines changed

docs/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Currently supported Redis operations:
1717
- Hash operations: `HGET`, `HSET`, `HGETALL`, `HSCAN`, `HSCAN_OVER_SCAN`
1818
- List operations: `LPUSH`, `LRANGE`, `LRANGE_TABLE`
1919
- Key operations: `DEL`, `EXISTS`, `TYPE`, `SCAN`, `KEYS`
20+
- TTL operations: `EXPIRE`, `TTL`, `EXPIREAT`
2021
- Batch and discovery operations: `SCAN`, `HSCAN_OVER_SCAN`, `KEYS`
2122

2223
## Quick Reference: Available Functions
@@ -33,6 +34,9 @@ Currently supported Redis operations:
3334
| `redis_del(key, secret)` | Scalar | Delete a key (returns TRUE if deleted) |
3435
| `redis_exists(key, secret)` | Scalar | Check if a key exists (returns TRUE if exists) |
3536
| `redis_type(key, secret)` | Scalar | Get the type of a key |
37+
| `redis_expire(key, seconds, secret)` | Scalar | Set TTL in seconds (returns TRUE if set) |
38+
| `redis_ttl(key, secret)` | Scalar | Get remaining TTL (-2=key not exists, -1=no expiry) |
39+
| `redis_expireat(key, timestamp, secret)` | Scalar | Set expiry at Unix timestamp (returns TRUE if set) |
3640
| `redis_scan(cursor, pattern, count, secret)` | Scalar | Scan keys (returns cursor:keys_csv) |
3741
| `redis_hscan(key, cursor, pattern, count, secret)` | Scalar | Scan fields in a hash |
3842
| `redis_keys(pattern, secret)` | Table | List all keys matching a pattern |
@@ -157,6 +161,22 @@ SELECT redis_exists('user:1', 'redis');
157161
SELECT redis_type('user:1', 'redis');
158162
```
159163

164+
### TTL Operations
165+
```sql
166+
-- Set a key to expire in 1 hour
167+
SELECT redis_expire('session:123', 3600, 'redis');
168+
169+
-- Check remaining TTL
170+
SELECT redis_ttl('session:123', 'redis') as remaining_seconds;
171+
172+
-- Set expiry at specific timestamp
173+
SELECT redis_expireat('cache:item', 1736918400, 'redis');
174+
175+
-- Expire all session keys in a query
176+
UPDATE sessions
177+
SET expired = CASE WHEN redis_expire('session:' || id, 300, 'redis') THEN TRUE ELSE FALSE END;
178+
```
179+
160180
### Batch and Discovery Operations
161181
```sql
162182
-- Get multiple keys at once

src/redis_extension.cpp

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ class RedisProtocol {
150150
}
151151
return cmd;
152152
}
153+
154+
static std::string formatExpire(const std::string &key, int64_t seconds) {
155+
return "*3\r\n$6\r\nEXPIRE\r\n$" + std::to_string(key.length()) + "\r\n" + key + "\r\n$" +
156+
std::to_string(std::to_string(seconds).length()) + "\r\n" + std::to_string(seconds) + "\r\n";
157+
}
158+
159+
static std::string formatExpireAt(const std::string &key, int64_t timestamp) {
160+
return "*3\r\n$8\r\nEXPIREAT\r\n$" + std::to_string(key.length()) + "\r\n" + key + "\r\n$" +
161+
std::to_string(std::to_string(timestamp).length()) + "\r\n" + std::to_string(timestamp) + "\r\n";
162+
}
163+
164+
static std::string formatTtl(const std::string &key) {
165+
return "*2\r\n$3\r\nTTL\r\n$" + std::to_string(key.length()) + "\r\n" + key + "\r\n";
166+
}
167+
168+
static int64_t parseIntegerResponse(const std::string &response) {
169+
if (response.empty() || response[0] != ':') {
170+
throw InvalidInputException("Invalid Redis integer response");
171+
}
172+
size_t end = response.find("\r\n");
173+
if (end == std::string::npos) {
174+
throw InvalidInputException("Invalid Redis integer response");
175+
}
176+
return std::stoll(response.substr(1, end - 1));
177+
}
153178
};
154179

155180
// Redis connection class
@@ -845,6 +870,57 @@ static void RedisTypeFunction(DataChunk &args, ExpressionState &state, Vector &r
845870
});
846871
}
847872

873+
static void RedisExpireFunction(DataChunk &args, ExpressionState &state, Vector &result) {
874+
auto &key_vector = args.data[0];
875+
auto &seconds_vector = args.data[1];
876+
auto &secret_vector = args.data[2];
877+
878+
BinaryExecutor::Execute<string_t, int64_t, bool>(
879+
key_vector, seconds_vector, result, args.size(), [&](string_t key, int64_t seconds) {
880+
string host, port, password;
881+
if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), host, port, password)) {
882+
throw InvalidInputException("Redis secret not found");
883+
}
884+
auto conn = ConnectionPool::getInstance().getConnection(host, port, password);
885+
auto response = conn->execute(RedisProtocol::formatExpire(key.GetString(), seconds));
886+
auto result_int = RedisProtocol::parseIntegerResponse(response);
887+
return result_int == 1;
888+
});
889+
}
890+
891+
static void RedisTTLFunction(DataChunk &args, ExpressionState &state, Vector &result) {
892+
auto &key_vector = args.data[0];
893+
auto &secret_vector = args.data[1];
894+
895+
UnaryExecutor::Execute<string_t, int64_t>(key_vector, result, args.size(), [&](string_t key) {
896+
string host, port, password;
897+
if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), host, port, password)) {
898+
throw InvalidInputException("Redis secret not found");
899+
}
900+
auto conn = ConnectionPool::getInstance().getConnection(host, port, password);
901+
auto response = conn->execute(RedisProtocol::formatTtl(key.GetString()));
902+
return RedisProtocol::parseIntegerResponse(response);
903+
});
904+
}
905+
906+
static void RedisExpireAtFunction(DataChunk &args, ExpressionState &state, Vector &result) {
907+
auto &key_vector = args.data[0];
908+
auto &timestamp_vector = args.data[1];
909+
auto &secret_vector = args.data[2];
910+
911+
BinaryExecutor::Execute<string_t, int64_t, bool>(
912+
key_vector, timestamp_vector, result, args.size(), [&](string_t key, int64_t timestamp) {
913+
string host, port, password;
914+
if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), host, port, password)) {
915+
throw InvalidInputException("Redis secret not found");
916+
}
917+
auto conn = ConnectionPool::getInstance().getConnection(host, port, password);
918+
auto response = conn->execute(RedisProtocol::formatExpireAt(key.GetString(), timestamp));
919+
auto result_int = RedisProtocol::parseIntegerResponse(response);
920+
return result_int == 1;
921+
});
922+
}
923+
848924
static void LoadInternal(ExtensionLoader &loader) {
849925
// Register the secret functions first!
850926
CreateRedisSecretFunctions::Register(loader);
@@ -972,6 +1048,36 @@ static void LoadInternal(ExtensionLoader &loader) {
9721048
"stream) or 'none' if the key does not exist.",
9731049
{"key", "secret_name"}, {"SELECT redis_type('mykey', 'my_redis_secret');"});
9741050

1051+
// Register redis_expire scalar function
1052+
add_scalar_function(
1053+
ScalarFunction("redis_expire", {LogicalType::VARCHAR, LogicalType::BIGINT, LogicalType::VARCHAR},
1054+
LogicalType::BOOLEAN, RedisExpireFunction),
1055+
"Set a time-to-live (TTL) in seconds for a key. Returns true if the TTL was set, false if the key does not exist.",
1056+
{"key", "seconds", "secret_name"},
1057+
{"SELECT redis_expire('mykey', 3600, 'my_redis_secret');",
1058+
"SELECT redis_expire('session:' || id, 300, 'my_redis_secret') FROM users;"});
1059+
1060+
// Register redis_ttl scalar function
1061+
add_scalar_function(ScalarFunction("redis_ttl", {LogicalType::VARCHAR, LogicalType::VARCHAR}, LogicalType::BIGINT,
1062+
RedisTTLFunction),
1063+
"Get the remaining time-to-live (TTL) of a key in seconds. Returns -2 if the key does not exist, "
1064+
"-1 if the key exists but has no expiry set.",
1065+
{"key", "secret_name"},
1066+
{"SELECT redis_ttl('mykey', 'my_redis_secret');",
1067+
"SELECT key, redis_ttl(key, 'my_redis_secret') FROM (SELECT redis_get(key) as key FROM "
1068+
"redis_keys('*', 'my_redis_secret')); "});
1069+
1070+
// Register redis_expireat scalar function
1071+
add_scalar_function(
1072+
ScalarFunction("redis_expireat", {LogicalType::VARCHAR, LogicalType::BIGINT, LogicalType::VARCHAR},
1073+
LogicalType::BOOLEAN, RedisExpireAtFunction),
1074+
"Set an expiry time (Unix timestamp) for a key. Returns true if the expiry was set, false if the key does not "
1075+
"exist.",
1076+
{"key", "timestamp", "secret_name"},
1077+
{"SELECT redis_expireat('mykey', 1736918400, 'my_redis_secret');",
1078+
"SELECT redis_expireat('event:' || id, EXTRACT(EPOCH FROM (event_time + INTERVAL '1 day'))::BIGINT, "
1079+
"'my_redis_secret') FROM events;"});
1080+
9751081
// Register redis_keys table function
9761082
add_table_function(
9771083
TableFunction("redis_keys", {LogicalType::VARCHAR, LogicalType::VARCHAR}, RedisKeysFunction, RedisKeysBind),
@@ -1006,7 +1112,7 @@ static void LoadInternal(ExtensionLoader &loader) {
10061112
{"scan_pattern", "hscan_pattern", "count", "secret_name"},
10071113
{"SELECT * FROM redis_hscan_over_scan('user:*', '*', 100, 'my_redis_secret');"});
10081114

1009-
QueryFarmSendTelemetry(loader, "redis", "2025120401");
1115+
QueryFarmSendTelemetry(loader, "redis", "2026011401");
10101116
}
10111117

10121118
void RedisExtension::Load(ExtensionLoader &loader) {
@@ -1018,7 +1124,7 @@ std::string RedisExtension::Name() {
10181124
}
10191125

10201126
std::string RedisExtension::Version() const {
1021-
return "2025120401";
1127+
return "2026011401";
10221128
}
10231129

10241130
} // namespace duckdb

test/sql/redis.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ SELECT * FROM redis_hscan_over_scan('*', '*', 100, 'nonexistent_secret');
8787
----
8888
Redis secret not found
8989

90+
statement error
91+
SELECT redis_expire('mykey', 3600, 'nonexistent_secret');
92+
----
93+
Redis secret not found
94+
95+
statement error
96+
SELECT redis_ttl('mykey', 'nonexistent_secret');
97+
----
98+
Redis secret not found
99+
100+
statement error
101+
SELECT redis_expireat('mykey', 1736918400, 'nonexistent_secret');
102+
----
103+
Redis secret not found
104+
90105
# Test that we can create a redis secret
91106
statement ok
92107
CREATE SECRET my_test_secret (

0 commit comments

Comments
 (0)