From fb1c685bd256bf2ca70a0dffd81a392950149482 Mon Sep 17 00:00:00 2001 From: lmangani <> Date: Sat, 19 Apr 2025 15:11:30 +0000 Subject: [PATCH 1/6] temp secrets, needs fixing --- duckdb | 2 +- extension-ci-tools | 2 +- src/include/redis_extension.hpp | 3 +- src/include/redis_secret.hpp | 14 ++ src/redis_extension.cpp | 418 ++++++++++++++++++++++++++++---- src/redis_secret.cpp | 58 +++++ 6 files changed, 449 insertions(+), 48 deletions(-) create mode 100644 src/include/redis_secret.hpp create mode 100644 src/redis_secret.cpp diff --git a/duckdb b/duckdb index 8e52ec4..4c7770c 160000 --- a/duckdb +++ b/duckdb @@ -1 +1 @@ -Subproject commit 8e52ec43959ab363643d63cb78ee214577111da4 +Subproject commit 4c7770c49c40ca81394818ee8a68fe920cbf7a10 diff --git a/extension-ci-tools b/extension-ci-tools index 58970c5..414dc75 160000 --- a/extension-ci-tools +++ b/extension-ci-tools @@ -1 +1 @@ -Subproject commit 58970c538d35919db875096460c05806056f4de0 +Subproject commit 414dc7505e6ebb5cc464d08f603145184f372ddf diff --git a/src/include/redis_extension.hpp b/src/include/redis_extension.hpp index 174cbbc..0617195 100644 --- a/src/include/redis_extension.hpp +++ b/src/include/redis_extension.hpp @@ -1,6 +1,7 @@ #pragma once #include "duckdb.hpp" +#include "redis_secret.hpp" namespace duckdb { @@ -10,4 +11,4 @@ class RedisExtension : public Extension { std::string Name() override; }; -} // namespace duckdb \ No newline at end of file +} // namespace duckdb diff --git a/src/include/redis_secret.hpp b/src/include/redis_secret.hpp new file mode 100644 index 0000000..03ccf79 --- /dev/null +++ b/src/include/redis_secret.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "duckdb/main/secret/secret.hpp" +#include "duckdb/main/secret/secret_manager.hpp" +#include "duckdb/main/extension_util.hpp" + +namespace duckdb { + +class CreateRedisSecretFunctions { +public: + static void Register(DatabaseInstance &instance); +}; + +} // namespace duckdb diff --git a/src/redis_extension.cpp b/src/redis_extension.cpp index f01e1b3..cf389cb 100644 --- a/src/redis_extension.cpp +++ b/src/redis_extension.cpp @@ -9,6 +9,9 @@ #include #include +#include +#include +#include namespace duckdb { @@ -17,6 +20,10 @@ using boost::asio::ip::tcp; // Simple Redis protocol formatter class RedisProtocol { public: + static std::string formatAuth(const std::string& password) { + return "*2\r\n$4\r\nAUTH\r\n$" + std::to_string(password.length()) + "\r\n" + password + "\r\n"; + } + static std::string formatGet(const std::string& key) { return "*2\r\n$3\r\nGET\r\n$" + std::to_string(key.length()) + "\r\n" + key + "\r\n"; } @@ -32,64 +39,217 @@ class RedisProtocol { // Bulk string response size_t pos = response.find("\r\n"); if (pos == std::string::npos) return ""; - return response.substr(pos + 2); + + // Skip the length prefix and first \r\n + pos += 2; + std::string value = response.substr(pos); + + // Remove trailing \r\n if present + if (value.size() >= 2 && value.substr(value.size() - 2) == "\r\n") { + value = value.substr(0, value.size() - 2); + } + return value; + } else if (response[0] == '+') { + // Simple string response + return response.substr(1, response.find("\r\n") - 1); + } else if (response[0] == '-') { + // Error response + throw InvalidInputException("Redis error: " + response.substr(1)); } return response; } + + // Hash operations + static std::string formatHGet(const std::string& key, const std::string& field) { + return "*3\r\n$4\r\nHGET\r\n$" + std::to_string(key.length()) + "\r\n" + key + + "\r\n$" + std::to_string(field.length()) + "\r\n" + field + "\r\n"; + } + + static std::string formatHSet(const std::string& key, const std::string& field, const std::string& value) { + return "*4\r\n$4\r\nHSET\r\n$" + std::to_string(key.length()) + "\r\n" + key + + "\r\n$" + std::to_string(field.length()) + "\r\n" + field + + "\r\n$" + std::to_string(value.length()) + "\r\n" + value + "\r\n"; + } + + static std::string formatHGetAll(const std::string& key) { + return "*2\r\n$7\r\nHGETALL\r\n$" + std::to_string(key.length()) + "\r\n" + key + "\r\n"; + } + + // List operations + static std::string formatLPush(const std::string& key, const std::string& value) { + return "*3\r\n$5\r\nLPUSH\r\n$" + std::to_string(key.length()) + "\r\n" + key + + "\r\n$" + std::to_string(value.length()) + "\r\n" + value + "\r\n"; + } + + static std::string formatLRange(const std::string& key, int64_t start, int64_t stop) { + auto start_str = std::to_string(start); + auto stop_str = std::to_string(stop); + return "*4\r\n$6\r\nLRANGE\r\n$" + std::to_string(key.length()) + "\r\n" + key + + "\r\n$" + std::to_string(start_str.length()) + "\r\n" + start_str + + "\r\n$" + std::to_string(stop_str.length()) + "\r\n" + stop_str + "\r\n"; + } + + // Key scanning + static std::string formatScan(const std::string& cursor, const std::string& pattern = "*", int64_t count = 10) { + auto count_str = std::to_string(count); + return "*6\r\n$4\r\nSCAN\r\n$" + std::to_string(cursor.length()) + "\r\n" + cursor + + "\r\n$5\r\nMATCH\r\n$" + std::to_string(pattern.length()) + "\r\n" + pattern + + "\r\n$5\r\nCOUNT\r\n$" + std::to_string(count_str.length()) + "\r\n" + count_str + "\r\n"; + } + + static std::vector parseArrayResponse(const std::string& response) { + std::vector result; + if (response.empty() || response[0] != '*') return result; + + size_t pos = 1; + size_t end = response.find("\r\n", pos); + int array_size = std::stoi(response.substr(pos, end - pos)); + pos = end + 2; + + for (int i = 0; i < array_size; i++) { + if (response[pos] == '$') { + pos++; + end = response.find("\r\n", pos); + int str_len = std::stoi(response.substr(pos, end - pos)); + pos = end + 2; + if (str_len >= 0) { + result.push_back(response.substr(pos, str_len)); + pos += str_len + 2; + } + } + } + return result; + } }; -// Redis client using Boost.Asio -class RedisClient { +// Redis connection class +class RedisConnection { public: - RedisClient(const std::string& host, const std::string& port) + RedisConnection(const std::string& host, const std::string& port, const std::string& password = "") : io_context_(), socket_(io_context_) { - tcp::resolver resolver(io_context_); - auto endpoints = resolver.resolve(host, port); - boost::asio::connect(socket_, endpoints); - } + try { + tcp::resolver resolver(io_context_); + auto endpoints = resolver.resolve(host, port); + boost::asio::connect(socket_, endpoints); - std::string get(const std::string& key) { - std::string request = RedisProtocol::formatGet(key); - boost::asio::write(socket_, boost::asio::buffer(request)); - - boost::asio::streambuf response; - boost::asio::read_until(socket_, response, "\r\n"); - - std::string result((std::istreambuf_iterator(&response)), - std::istreambuf_iterator()); - return RedisProtocol::parseResponse(result); + if (!password.empty()) { + std::string auth_cmd = RedisProtocol::formatAuth(password); + boost::asio::write(socket_, boost::asio::buffer(auth_cmd)); + + boost::asio::streambuf response; + boost::asio::read_until(socket_, response, "\r\n"); + + std::string auth_response((std::istreambuf_iterator(&response)), + std::istreambuf_iterator()); + RedisProtocol::parseResponse(auth_response); + } + } catch (std::exception& e) { + throw InvalidInputException("Redis connection error: " + std::string(e.what())); + } } - std::string set(const std::string& key, const std::string& value) { - std::string request = RedisProtocol::formatSet(key, value); - boost::asio::write(socket_, boost::asio::buffer(request)); - - boost::asio::streambuf response; - boost::asio::read_until(socket_, response, "\r\n"); - - std::string result((std::istreambuf_iterator(&response)), - std::istreambuf_iterator()); - return RedisProtocol::parseResponse(result); + std::string execute(const std::string& command) { + std::lock_guard lock(mutex_); + try { + boost::asio::write(socket_, boost::asio::buffer(command)); + + boost::asio::streambuf response; + boost::asio::read_until(socket_, response, "\r\n"); + + return std::string((std::istreambuf_iterator(&response)), + std::istreambuf_iterator()); + } catch (std::exception& e) { + throw InvalidInputException("Redis execution error: " + std::string(e.what())); + } } private: boost::asio::io_context io_context_; tcp::socket socket_; + std::mutex mutex_; }; +// Connection pool manager +class ConnectionPool { +public: + static ConnectionPool& getInstance() { + static ConnectionPool instance; + return instance; + } + + std::shared_ptr getConnection(const std::string& host, + const std::string& port, + const std::string& password = "") { + std::string key = host + ":" + port; + std::lock_guard lock(mutex_); + + auto it = connections_.find(key); + if (it == connections_.end()) { + auto conn = std::make_shared(host, port, password); + connections_[key] = conn; + return conn; + } + return it->second; + } + +private: + ConnectionPool() {} + std::mutex mutex_; + std::unordered_map> connections_; +}; + +// Add this helper function +static bool GetRedisSecret(ClientContext &context, const string &secret_name, string &host, string &port, string &password) { + auto &secret_manager = SecretManager::Get(context); + try { + auto transaction = CatalogTransaction::GetSystemCatalogTransaction(context); + auto secret_match = secret_manager.LookupSecret(transaction, "redis", secret_name); + if (secret_match.HasMatch()) { + auto &secret = secret_match.GetSecret(); + if (secret.GetType() != "redis") { + throw InvalidInputException("Invalid secret type. Expected 'redis', got '%s'", secret.GetType()); + } + const auto *kv_secret = dynamic_cast(&secret); + if (!kv_secret) { + throw InvalidInputException("Invalid secret format for 'redis' secret"); + } + + Value host_val, port_val, password_val; + if (!kv_secret->TryGetValue("host", host_val) || + !kv_secret->TryGetValue("port", port_val) || + !kv_secret->TryGetValue("password", password_val)) { + return false; + } + + host = host_val.ToString(); + port = port_val.ToString(); + password = password_val.ToString(); + return true; + } + } catch (...) { + return false; + } + return false; +} + +// Modify the function signatures to accept secret name instead of connection details static void RedisGetFunction(DataChunk &args, ExpressionState &state, Vector &result) { auto &key_vector = args.data[0]; - auto &host_vector = args.data[1]; - auto &port_vector = args.data[2]; + auto &secret_vector = args.data[1]; UnaryExecutor::Execute( key_vector, result, args.size(), [&](string_t key) { try { - RedisClient client(host_vector.GetValue(0).ToString(), - port_vector.GetValue(0).ToString()); - auto response = client.get(key.GetString()); - return StringVector::AddString(result, response); + string host, port, password; + if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), + host, port, password)) { + throw InvalidInputException("Redis secret not found"); + } + + auto conn = ConnectionPool::getInstance().getConnection(host, port, password); + auto response = conn->execute(RedisProtocol::formatGet(key.GetString())); + return StringVector::AddString(result, RedisProtocol::parseResponse(response)); } catch (std::exception &e) { throw InvalidInputException("Redis GET error: %s", e.what()); } @@ -99,40 +259,209 @@ static void RedisGetFunction(DataChunk &args, ExpressionState &state, Vector &re static void RedisSetFunction(DataChunk &args, ExpressionState &state, Vector &result) { auto &key_vector = args.data[0]; auto &value_vector = args.data[1]; - auto &host_vector = args.data[2]; - auto &port_vector = args.data[3]; + auto &secret_vector = args.data[2]; BinaryExecutor::Execute( key_vector, value_vector, result, args.size(), [&](string_t key, string_t value) { try { - RedisClient client(host_vector.GetValue(0).ToString(), - port_vector.GetValue(0).ToString()); - auto response = client.set(key.GetString(), value.GetString()); - return StringVector::AddString(result, response); + string host, port, password; + if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), + host, port, password)) { + throw InvalidInputException("Redis secret not found"); + } + + auto conn = ConnectionPool::getInstance().getConnection(host, port, password); + auto response = conn->execute(RedisProtocol::formatSet(key.GetString(), value.GetString())); + return StringVector::AddString(result, RedisProtocol::parseResponse(response)); } catch (std::exception &e) { throw InvalidInputException("Redis SET error: %s", e.what()); } }); } +// Hash operations +static void RedisHGetFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto &key_vector = args.data[0]; + auto &field_vector = args.data[1]; + auto &secret_vector = args.data[2]; + + BinaryExecutor::Execute( + key_vector, field_vector, result, args.size(), + [&](string_t key, string_t field) { + try { + string host, port, password; + if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), + host, port, password)) { + throw InvalidInputException("Redis secret not found"); + } + + auto conn = ConnectionPool::getInstance().getConnection(host, port, password); + auto response = conn->execute(RedisProtocol::formatHGet(key.GetString(), field.GetString())); + return StringVector::AddString(result, RedisProtocol::parseResponse(response)); + } catch (std::exception &e) { + throw InvalidInputException("Redis HGET error: %s", e.what()); + } + }); +} + +static void RedisHSetFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto &key_vector = args.data[0]; + auto &field_vector = args.data[1]; + auto &value_vector = args.data[2]; + auto &secret_vector = args.data[3]; + + BinaryExecutor::Execute( + key_vector, field_vector, result, args.size(), + [&](string_t key, string_t field) { + try { + string host, port, password; + if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), + host, port, password)) { + throw InvalidInputException("Redis secret not found"); + } + + auto conn = ConnectionPool::getInstance().getConnection(host, port, password); + auto response = conn->execute(RedisProtocol::formatHSet( + key.GetString(), + field.GetString(), + value_vector.GetValue(0).ToString() + )); + return StringVector::AddString(result, RedisProtocol::parseResponse(response)); + } catch (std::exception &e) { + throw InvalidInputException("Redis HSET error: %s", e.what()); + } + }); +} + +// List operations +static void RedisLPushFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto &key_vector = args.data[0]; + auto &value_vector = args.data[1]; + auto &secret_vector = args.data[2]; + + BinaryExecutor::Execute( + key_vector, value_vector, result, args.size(), + [&](string_t key, string_t value) { + try { + string host, port, password; + if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), + host, port, password)) { + throw InvalidInputException("Redis secret not found"); + } + + auto conn = ConnectionPool::getInstance().getConnection(host, port, password); + auto response = conn->execute(RedisProtocol::formatLPush(key.GetString(), value.GetString())); + return StringVector::AddString(result, RedisProtocol::parseResponse(response)); + } catch (std::exception &e) { + throw InvalidInputException("Redis LPUSH error: %s", e.what()); + } + }); +} + +static void RedisLRangeFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto &key_vector = args.data[0]; + auto &start_vector = args.data[1]; + auto &stop_vector = args.data[2]; + auto &secret_vector = args.data[3]; + + BinaryExecutor::Execute( + key_vector, start_vector, result, args.size(), + [&](string_t key, int64_t start) { + try { + string host, port, password; + if (!GetRedisSecret(state.GetContext(), secret_vector.GetValue(0).ToString(), + host, port, password)) { + throw InvalidInputException("Redis secret not found"); + } + + auto conn = ConnectionPool::getInstance().getConnection(host, port, password); + auto stop = stop_vector.GetValue(0).GetValue(); + auto response = conn->execute(RedisProtocol::formatLRange(key.GetString(), start, stop)); + auto values = RedisProtocol::parseArrayResponse(response); + // Join array values with comma for string result + std::string joined; + for (size_t i = 0; i < values.size(); i++) { + if (i > 0) joined += ","; + joined += values[i]; + } + return StringVector::AddString(result, joined); + } catch (std::exception &e) { + throw InvalidInputException("Redis LRANGE error: %s", e.what()); + } + }); +} + static void LoadInternal(DatabaseInstance &instance) { - // Register functions + // Register Redis GET function auto redis_get_func = ScalarFunction( "redis_get", - {LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::VARCHAR}, + {LogicalType::VARCHAR, // key + LogicalType::VARCHAR}, // secret_name LogicalType::VARCHAR, RedisGetFunction ); ExtensionUtil::RegisterFunction(instance, redis_get_func); + // Register Redis SET function auto redis_set_func = ScalarFunction( "redis_set", - {LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::VARCHAR}, + {LogicalType::VARCHAR, // key + LogicalType::VARCHAR, // value + LogicalType::VARCHAR}, // secret_name LogicalType::VARCHAR, RedisSetFunction ); ExtensionUtil::RegisterFunction(instance, redis_set_func); + + // Register HGET + auto redis_hget_func = ScalarFunction( + "redis_hget", + {LogicalType::VARCHAR, // key + LogicalType::VARCHAR, // field + LogicalType::VARCHAR}, // secret_name + LogicalType::VARCHAR, + RedisHGetFunction + ); + ExtensionUtil::RegisterFunction(instance, redis_hget_func); + + // Register HSET + auto redis_hset_func = ScalarFunction( + "redis_hset", + {LogicalType::VARCHAR, // key + LogicalType::VARCHAR, // field + LogicalType::VARCHAR, // value + LogicalType::VARCHAR}, // secret_name + LogicalType::VARCHAR, + RedisHSetFunction + ); + ExtensionUtil::RegisterFunction(instance, redis_hset_func); + + // Register LPUSH + auto redis_lpush_func = ScalarFunction( + "redis_lpush", + {LogicalType::VARCHAR, // key + LogicalType::VARCHAR, // value + LogicalType::VARCHAR}, // secret_name + LogicalType::VARCHAR, + RedisLPushFunction + ); + ExtensionUtil::RegisterFunction(instance, redis_lpush_func); + + // Register LRANGE + auto redis_lrange_func = ScalarFunction( + "redis_lrange", + {LogicalType::VARCHAR, // key + LogicalType::BIGINT, // start + LogicalType::BIGINT, // stop + LogicalType::VARCHAR}, // secret_name + LogicalType::VARCHAR, + RedisLRangeFunction + ); + ExtensionUtil::RegisterFunction(instance, redis_lrange_func); + + // Register the secret functions + CreateRedisSecretFunctions::Register(instance); } void RedisExtension::Load(DuckDB &db) { @@ -146,7 +475,6 @@ std::string RedisExtension::Name() { } // namespace duckdb extern "C" { - DUCKDB_EXTENSION_API void redis_init(duckdb::DatabaseInstance &db) { duckdb::DuckDB db_wrapper(db); db_wrapper.LoadExtension(); @@ -155,4 +483,4 @@ DUCKDB_EXTENSION_API void redis_init(duckdb::DatabaseInstance &db) { DUCKDB_EXTENSION_API const char *redis_version() { return duckdb::DuckDB::LibraryVersion(); } -} \ No newline at end of file +} diff --git a/src/redis_secret.cpp b/src/redis_secret.cpp new file mode 100644 index 0000000..a37bd1e --- /dev/null +++ b/src/redis_secret.cpp @@ -0,0 +1,58 @@ +#include "redis_secret.hpp" +#include "duckdb/common/exception.hpp" +#include "duckdb/main/secret/secret.hpp" +#include "duckdb/main/extension_util.hpp" + +namespace duckdb { + +static void CopySecret(const std::string &key, const CreateSecretInput &input, KeyValueSecret &result) { + auto val = input.options.find(key); + if (val != input.options.end()) { + result.secret_map[key] = val->second; + } +} + +static void RegisterCommonSecretParameters(CreateSecretFunction &function) { + // Register redis connection parameters + function.named_parameters["host"] = LogicalType::VARCHAR; + function.named_parameters["port"] = LogicalType::VARCHAR; + function.named_parameters["password"] = LogicalType::VARCHAR; +} + +static void RedactCommonKeys(KeyValueSecret &result) { + // Redact sensitive information + result.redact_keys.insert("password"); +} + +static unique_ptr CreateRedisSecretFromConfig(ClientContext &context, CreateSecretInput &input) { + auto scope = input.scope; + auto result = make_uniq(scope, input.type, input.provider, input.name); + + // Copy all relevant secrets + CopySecret("host", input, *result); + CopySecret("port", input, *result); + CopySecret("password", input, *result); + + // Redact sensitive keys + RedactCommonKeys(*result); + + return std::move(result); +} + +void CreateRedisSecretFunctions::Register(DatabaseInstance &instance) { + string type = "redis"; + + // Register the new type + SecretType secret_type; + secret_type.name = type; + secret_type.deserializer = KeyValueSecret::Deserialize; + secret_type.default_provider = "config"; + ExtensionUtil::RegisterSecretType(instance, secret_type); + + // Register the config secret provider + CreateSecretFunction config_function = {type, "config", CreateRedisSecretFromConfig}; + RegisterCommonSecretParameters(config_function); + ExtensionUtil::RegisterFunction(instance, config_function); +} + +} // namespace duckdb From ec73e9c053240ae646e4034f9b6e8d8950e1231c Mon Sep 17 00:00:00 2001 From: Lorenzo Mangani Date: Sat, 19 Apr 2025 17:14:21 +0200 Subject: [PATCH 2/6] fix include --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 406dc4a..247e598 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,11 @@ include_directories( ${Boost_INCLUDE_DIRS} ) -set(EXTENSION_SOURCES src/redis_extension.cpp) +# Add redis_secret.cpp to the sources +set(EXTENSION_SOURCES + src/redis_extension.cpp + src/redis_secret.cpp +) build_static_extension(${TARGET_NAME} ${EXTENSION_SOURCES}) build_loadable_extension(${TARGET_NAME} " " ${EXTENSION_SOURCES}) From 55dd04666492a7ea2d569c75bb750e554fb5ba81 Mon Sep 17 00:00:00 2001 From: Lorenzo Mangani Date: Sat, 19 Apr 2025 17:18:28 +0200 Subject: [PATCH 3/6] fix serializer --- src/redis_secret.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/redis_secret.cpp b/src/redis_secret.cpp index a37bd1e..4fe2cf9 100644 --- a/src/redis_secret.cpp +++ b/src/redis_secret.cpp @@ -24,7 +24,7 @@ static void RedactCommonKeys(KeyValueSecret &result) { result.redact_keys.insert("password"); } -static unique_ptr CreateRedisSecretFromConfig(ClientContext &context, CreateSecretInput &input) { +static unique_ptr CreateRedisSecretFromConfig(ClientContext &context, CreateSecretInput &input) { auto scope = input.scope; auto result = make_uniq(scope, input.type, input.provider, input.name); @@ -39,13 +39,19 @@ static unique_ptr CreateRedisSecretFromConfig(ClientContext &con return std::move(result); } +static unique_ptr RedisSecretDeserialize(Deserializer &deserializer, BaseSecret base_secret) { + auto result = KeyValueSecret::Deserialize(deserializer, std::move(base_secret)); + RedactCommonKeys(*result); + return std::move(result); +} + void CreateRedisSecretFunctions::Register(DatabaseInstance &instance) { string type = "redis"; // Register the new type SecretType secret_type; secret_type.name = type; - secret_type.deserializer = KeyValueSecret::Deserialize; + secret_type.deserializer = RedisSecretDeserialize; secret_type.default_provider = "config"; ExtensionUtil::RegisterSecretType(instance, secret_type); From fd4bf2a888cea43a836b8433c2b564060170de78 Mon Sep 17 00:00:00 2001 From: Lorenzo Mangani Date: Sat, 19 Apr 2025 17:22:58 +0200 Subject: [PATCH 4/6] fix cast --- src/redis_secret.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/redis_secret.cpp b/src/redis_secret.cpp index 4fe2cf9..7265650 100644 --- a/src/redis_secret.cpp +++ b/src/redis_secret.cpp @@ -41,7 +41,10 @@ static unique_ptr CreateRedisSecretFromConfig(ClientContext &context static unique_ptr RedisSecretDeserialize(Deserializer &deserializer, BaseSecret base_secret) { auto result = KeyValueSecret::Deserialize(deserializer, std::move(base_secret)); - RedactCommonKeys(*result); + auto kv_secret = dynamic_cast(result.get()); + if (kv_secret) { + RedactCommonKeys(*kv_secret); + } return std::move(result); } From cf839429e61812b16725fa063a13b1f13fe8ef53 Mon Sep 17 00:00:00 2001 From: Lorenzo Mangani Date: Sat, 19 Apr 2025 17:36:11 +0200 Subject: [PATCH 5/6] Update README.md --- docs/README.md | 259 ++++++++++++++++--------------------------------- 1 file changed, 82 insertions(+), 177 deletions(-) diff --git a/docs/README.md b/docs/README.md index 5d3b52c..661ae01 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,8 +7,9 @@ This extension provides Redis client functionality for DuckDB, allowing you to i ## Features Currently supported Redis operations: -- `redis_get(key, host, port)`: Retrieves a value from Redis for a given key -- `redis_set(key, value, host, port)`: Sets a value in Redis for a given key +- String operations: `GET`, `SET` +- Hash operations: `HGET`, `HSET` +- List operations: `LPUSH`, `LRANGE` ## Installation ```sql @@ -16,207 +17,111 @@ INSTALL redis FROM community; LOAD redis; ``` -## Usage Examples -### Setting Values in Redis +## Usage +### Setting up Redis Connection +First, create a secret to store your Redis connection details: ```sql --- Set a single value -SELECT redis_set('user:1', 'John Doe', 'localhost', '6379') as result; +-- Create a Redis connection secret +CALL redis_create_secret('my_redis', { + 'host': 'localhost', + 'port': '6379', + 'password': 'optional_password' +}); + +-- For cloud Redis services (e.g., Redis Labs) +CALL redis_create_secret('redis_cloud', { + 'host': 'redis-xxxxx.cloud.redislabs.com', + 'port': '16379', + 'password': 'your_password' +}); +``` + +### String Operations +```sql +-- Set a value +SELECT redis_set('user:1', 'John Doe', 'my_redis') as result; + +-- Get a value +SELECT redis_get('user:1', 'my_redis') as user_name; -- Set multiple values in a query -INSERT INTO users (id, name, age) -SELECT redis_set( +INSERT INTO users (id, name) +SELECT id, redis_set( 'user:' || id::VARCHAR, name, - 'localhost', - '6379' + 'my_redis' ) FROM new_users; ``` -### Getting Values from Redis +### Hash Operations ```sql --- Get a single value -SELECT redis_get('user:1', 'localhost', '6379') as user_name; - --- Get multiple values -SELECT - id, - redis_get('user:' || id::VARCHAR, 'localhost', '6379') as user_data -FROM user_ids; +-- Set hash fields +SELECT redis_hset('user:1', 'email', 'john@example.com', 'my_redis'); +SELECT redis_hset('user:1', 'age', '30', 'my_redis'); + +-- Get hash field +SELECT redis_hget('user:1', 'email', 'my_redis') as email; + +-- Store user profile as hash +WITH profile(id, field, value) AS ( + VALUES + (1, 'name', 'John Doe'), + (1, 'email', 'john@example.com'), + (1, 'age', '30') +) +SELECT redis_hset( + 'user:' || id::VARCHAR, + field, + value, + 'my_redis' +) +FROM profile; ``` -## Building from Source -Follow the standard DuckDB extension build process: +### List Operations +```sql +-- Push items to list +SELECT redis_lpush('mylist', 'first_item', 'my_redis'); +SELECT redis_lpush('mylist', 'second_item', 'my_redis'); -```sh -# Install vcpkg dependencies -./vcpkg/vcpkg install boost-asio +-- Get range from list (returns comma-separated values) +-- Get all items (0 to -1 means start to end) +SELECT redis_lrange('mylist', 0, -1, 'my_redis') as items; -# Build the extension -make -``` +-- Get first 5 items +SELECT redis_lrange('mylist', 0, 4, 'my_redis') as items; -## Dependencies -- Boost.Asio (header-only, installed via vcpkg) +-- Push multiple items +WITH items(value) AS ( + VALUES ('item1'), ('item2'), ('item3') +) +SELECT redis_lpush('mylist', value, 'my_redis') +FROM items; +``` ## Error Handling The extension functions will throw exceptions with descriptive error messages when: +- Redis secret is not found or invalid - Unable to connect to Redis server - Network communication errors occur - Invalid Redis protocol responses are received -## Future Enhancements -Planned features include: -- Support for Redis authentication -- Connection pooling for better performance -- Additional Redis commands (HGET, HSET, LPUSH, etc.) -- Table functions for scanning Redis keys -- Batch operations using Redis pipelines -- Connection timeout handling - -# DuckDB Extension Template -This repository contains a template for creating a DuckDB extension. The main goal of this template is to allow users to easily develop, test and distribute their own DuckDB extension. The main branch of the template is always based on the latest stable DuckDB allowing you to try out your extension right away. +## Building from Source +Follow the standard DuckDB extension build process: -## Getting started -First step to getting started is to create your own repo from this template by clicking `Use this template`. Then clone your new repository using ```sh -git clone --recurse-submodules https://github.com//.git -``` -Note that `--recurse-submodules` will ensure DuckDB is pulled which is required to build the extension. - -## Building -### Managing dependencies -DuckDB extensions uses VCPKG for dependency management. Enabling VCPKG is very simple: follow the [installation instructions](https://vcpkg.io/en/getting-started) or just run the following: -```shell -cd -git clone https://github.com/Microsoft/vcpkg.git -sh ./vcpkg/scripts/bootstrap.sh -disableMetrics -export VCPKG_TOOLCHAIN_PATH=`pwd`/vcpkg/scripts/buildsystems/vcpkg.cmake -``` -Note: VCPKG is only required for extensions that want to rely on it for dependency management. If you want to develop an extension without dependencies, or want to do your own dependency management, just skip this step. Note that the example extension uses VCPKG to build with a dependency for instructive purposes, so when skipping this step the build may not work without removing the dependency. +# Install vcpkg dependencies +./vcpkg/vcpkg install boost-asio -### Build steps -Now to build the extension, run: -```sh +# Build the extension make ``` -The main binaries that will be built are: -```sh -./build/release/duckdb -./build/release/test/unittest -./build/release/extension//.duckdb_extension -``` -- `duckdb` is the binary for the duckdb shell with the extension code automatically loaded. -- `unittest` is the test runner of duckdb. Again, the extension is already linked into the binary. -- `.duckdb_extension` is the loadable binary as it would be distributed. - -### Tips for speedy builds -DuckDB extensions currently rely on DuckDB's build system to provide easy testing and distributing. This does however come at the downside of requiring the template to build DuckDB and its unittest binary every time you build your extension. To mitigate this, we highly recommend installing [ccache](https://ccache.dev/) and [ninja](https://ninja-build.org/). This will ensure you only need to build core DuckDB once and allows for rapid rebuilds. - -To build using ninja and ccache ensure both are installed and run: - -```sh -GEN=ninja make -``` - -## Running the extension -To run the extension code, simply start the shell with `./build/release/duckdb`. This shell will have the extension pre-loaded. - -Now we can use the features from the extension directly in DuckDB. The template contains a single scalar function `quack()` that takes a string arguments and returns a string: -``` -D select quack('Jane') as result; -┌───────────────┐ -│ result │ -│ varchar │ -├───────────────┤ -│ Quack Jane 🐥 │ -└───────────────┘ -``` - -## Running the tests -Different tests can be created for DuckDB extensions. The primary way of testing DuckDB extensions should be the SQL tests in `./test/sql`. These SQL tests can be run using: -```sh -make test -``` - -## Getting started with your own extension -After creating a repository from this template, the first step is to name your extension. To rename the extension, run: -``` -python3 ./scripts/bootstrap-template.py -``` -Feel free to delete the script after this step. - -Now you're good to go! After a (re)build, you should now be able to use your duckdb extension: -``` -./build/release/duckdb -D select ('Jane') as result; -┌─────────────────────────────────────┐ -│ result │ -│ varchar │ -├─────────────────────────────────────┤ -│ Jane 🐥 │ -└─────────────────────────────────────┘ -``` -For inspiration/examples on how to extend DuckDB in a more meaningful way, check out the [test extensions](https://github.com/duckdb/duckdb/blob/main/test/extension), -the [in-tree extensions](https://github.com/duckdb/duckdb/tree/main/extension), and the [out-of-tree extensions](https://github.com/duckdblabs). - -## Distributing your extension -To distribute your extension binaries, there are a few options. - -### Community extensions -The recommended way of distributing extensions is through the [community extensions repository](https://github.com/duckdb/community-extensions). -This repository is designed specifically for extensions that are built using this extension template, meaning that as long as your extension can be -built using the default CI in this template, submitting it to the community extensions is a very simple process. The process works similarly to popular -package managers like homebrew and vcpkg, where a PR containing a descriptor file is submitted to the package manager repository. After the CI in the -community extensions repository completes, the extension can be installed and loaded in DuckDB with: -```SQL -INSTALL FROM community; -LOAD -``` -For more information, see the [community extensions documentation](https://duckdb.org/community_extensions/documentation). - -### Downloading artifacts from GitHub -The default CI in this template will automatically upload the binaries for every push to the main branch as GitHub Actions artifacts. These -can be downloaded manually and then loaded directly using: -```SQL -LOAD '/path/to/downloaded/extension.duckdb_extension'; -``` -Note that this will require starting DuckDB with the -`allow_unsigned_extensions` option set to true. How to set this will depend on the client you're using. For the CLI it is done like: -```shell -duckdb -unsigned -``` - -### Uploading to a custom repository -If for some reason distributing through community extensions is not an option, extensions can also be uploaded to a custom extension repository. -This will give some more control over where and how the extensions are distributed, but comes with the downside of requiring the `allow_unsigned_extensions` -option to be set. For examples of how to configure a manual GitHub Actions deploy pipeline, check out the extension deploy script in https://github.com/duckdb/extension-ci-tools. -Some examples of extensions that use this CI/CD workflow check out [spatial](https://github.com/duckdblabs/duckdb_spatial) or [aws](https://github.com/duckdb/duckdb_aws). - -Extensions in custom repositories can be installed and loaded using: -```SQL -INSTALL FROM 'http://my-custom-repo' -LOAD -``` - -### Versioning of your extension -Extension binaries will only work for the specific DuckDB version they were built for. The version of DuckDB that is targeted -is set to the latest stable release for the main branch of the template so initially that is all you need. As new releases -of DuckDB are published however, the extension repository will need to be updated. The template comes with a workflow set-up -that will automatically build the binaries for all DuckDB target architectures that are available in the corresponding DuckDB -version. This workflow is found in `.github/workflows/MainDistributionPipeline.yml`. It is up to the extension developer to keep -this up to date with DuckDB. Note also that its possible to distribute binaries for multiple DuckDB versions in this workflow -by simply duplicating the jobs. - -## Setting up CLion - -### Opening project -Configuring CLion with the extension template requires a little work. Firstly, make sure that the DuckDB submodule is available. -Then make sure to open `./duckdb/CMakeLists.txt` (so not the top level `CMakeLists.txt` file from this repo) as a project in CLion. -Now to fix your project path go to `tools->CMake->Change Project Root`([docs](https://www.jetbrains.com/help/clion/change-project-root-directory.html)) to set the project root to the root dir of this repo. - -### Debugging -To set up debugging in CLion, there are two simple steps required. Firstly, in `CLion -> Settings / Preferences -> Build, Execution, Deploy -> CMake` you will need to add the desired builds (e.g. Debug, Release, RelDebug, etc). There's different ways to configure this, but the easiest is to leave all empty, except the `build path`, which needs to be set to `../build/{build type}`. Now on a clean repository you will first need to run `make {build type}` to initialize the CMake build directory. After running make, you will be able to (re)build from CLion by using the build target we just created. If you use the CLion editor, you can create a CLion CMake profiles matching the CMake variables that are described in the makefile, and then you don't need to invoke the Makefile. +## Future Enhancements +Planned features include: +- Table functions for scanning Redis keys +- Additional Redis commands (SADD, SMEMBERS, etc.) +- Batch operations using Redis pipelines +- Connection timeout handling -The second step is to configure the unittest runner as a run/debug configuration. To do this, go to `Run -> Edit Configurations` and click `+ -> Cmake Application`. The target and executable should be `unittest`. This will run all the DuckDB tests. To specify only running the extension specific tests, add `--test-dir ../../.. [sql]` to the `Program Arguments`. Note that it is recommended to use the `unittest` executable for testing/development within CLion. The actual DuckDB CLI currently does not reliably work as a run target in CLion. From a17d73cff2a1ce06eab6a395dfa2749c5de7e7cd Mon Sep 17 00:00:00 2001 From: Lorenzo Mangani Date: Sat, 19 Apr 2025 18:00:33 +0200 Subject: [PATCH 6/6] Update instructions --- docs/README.md | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/docs/README.md b/docs/README.md index b3a870e..8a6cef9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,27 +24,31 @@ First, create a secret to store your Redis connection details: ```sql -- Create a Redis connection secret -CALL redis_create_secret('my_redis', { - 'host': 'localhost', - 'port': '6379', - 'password': 'optional_password' -}); - --- For cloud Redis services (e.g., Redis Labs) -CALL redis_create_secret('redis_cloud', { - 'host': 'redis-xxxxx.cloud.redislabs.com', - 'port': '16379', - 'password': 'your_password' -}); +CREATE SECRET IF NOT EXISTS redis ( + TYPE redis, + PROVIDER config, + host 'localhost', + port '6379', + password 'optional_password' + ); + +-- Create a Redis cloud connection secret +CREATE SECRET IF NOT EXISTS redis ( + TYPE redis, + PROVIDER config, + host 'redis-1234.ec2.redns.redis-cloud.com', + port '16959', + password 'xxxxxx' + ); ``` ### String Operations ```sql -- Set a value -SELECT redis_set('user:1', 'John Doe', 'my_redis') as result; +SELECT redis_set('user:1', 'John Doe', 'redis') as result; -- Get a value -SELECT redis_get('user:1', 'my_redis') as user_name; +SELECT redis_get('user:1', 'redis') as user_name; -- Set multiple values in a query INSERT INTO users (id, name) @@ -59,11 +63,11 @@ FROM new_users; ### Hash Operations ```sql -- Set hash fields -SELECT redis_hset('user:1', 'email', 'john@example.com', 'my_redis'); -SELECT redis_hset('user:1', 'age', '30', 'my_redis'); +SELECT redis_hset('user:1', 'email', 'john@example.com', 'redis'); +SELECT redis_hset('user:1', 'age', '30', 'redis'); -- Get hash field -SELECT redis_hget('user:1', 'email', 'my_redis') as email; +SELECT redis_hget('user:1', 'email', 'redis') as email; -- Store user profile as hash WITH profile(id, field, value) AS ( @@ -76,7 +80,7 @@ SELECT redis_hset( 'user:' || id::VARCHAR, field, value, - 'my_redis' + 'redis' ) FROM profile; ``` @@ -84,21 +88,21 @@ FROM profile; ### List Operations ```sql -- Push items to list -SELECT redis_lpush('mylist', 'first_item', 'my_redis'); -SELECT redis_lpush('mylist', 'second_item', 'my_redis'); +SELECT redis_lpush('mylist', 'first_item', 'redis'); +SELECT redis_lpush('mylist', 'second_item', 'redis'); -- Get range from list (returns comma-separated values) -- Get all items (0 to -1 means start to end) -SELECT redis_lrange('mylist', 0, -1, 'my_redis') as items; +SELECT redis_lrange('mylist', 0, -1, 'redis') as items; -- Get first 5 items -SELECT redis_lrange('mylist', 0, 4, 'my_redis') as items; +SELECT redis_lrange('mylist', 0, 4, 'redis') as items; -- Push multiple items WITH items(value) AS ( VALUES ('item1'), ('item2'), ('item3') ) -SELECT redis_lpush('mylist', value, 'my_redis') +SELECT redis_lpush('mylist', value, 'redis') FROM items; ```