@@ -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\n EXPIRE\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\n EXPIREAT\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\n TTL\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 ×tamp_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+
848924static 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
10121118void RedisExtension::Load (ExtensionLoader &loader) {
@@ -1018,7 +1124,7 @@ std::string RedisExtension::Name() {
10181124}
10191125
10201126std::string RedisExtension::Version () const {
1021- return " 2025120401 " ;
1127+ return " 2026011401 " ;
10221128}
10231129
10241130} // namespace duckdb
0 commit comments