diff --git a/docs/README.md b/docs/README.md index aba3ad6..5d4a115 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,9 @@ LOAD http_client; ### Functions - `http_get(url)` - `http_post(url, headers, params)` + - Sends POST request with params encoded as a JSON object +- `http_post_form(url, headers, params)` + - Sends POST request with params being `application/x-www-form-urlencoded` encoded (used by many forms and some APIs) ### Examples #### GET @@ -83,6 +86,44 @@ D WITH __input AS ( └────────┴─────────┴─────────────┘ ``` +#### POST using form encoding(application/x-www-form-urlencoded, not multipart/form-data) +```sql +D WITH __input AS ( + SELECT + http_post_form( + 'https://httpbin.org/delay/0', + headers => MAP { + 'accept': 'application/json', + }, + params => MAP { + 'limit': 10 + } + ) AS res +), +__response AS ( + SELECT + (res->>'status')::INT AS status, + (res->>'reason') AS reason, + unnest( from_json(((res->>'body')::JSON)->'form', '{"limit": "VARCHAR"}') ) AS features + FROM + __input +) +SELECT + __response.status, + __response.reason, + __response.limit AS limit +FROM + __response +; +┌────────┬─────────┬─────────┐ +│ status │ reason │ limit │ +│ int32 │ varchar │ varchar │ +├────────┼─────────┼─────────┤ +│ 200 │ OK │ 10 │ +└────────┴─────────┴─────────┘ +``` + + #### Full Example w/ spatial data This is the original example by @ahuarte47 inspiring this community extension. diff --git a/src/http_client_extension.cpp b/src/http_client_extension.cpp index 47dab1c..6680c9b 100644 --- a/src/http_client_extension.cpp +++ b/src/http_client_extension.cpp @@ -261,6 +261,46 @@ static void HTTPPostRequestFunction(DataChunk &args, ExpressionState &state, Vec }); } +static void HTTPPostFormRequestFunction(DataChunk &args, ExpressionState &state, Vector &result) { + D_ASSERT(args.data.size() == 3); + + using STRING_TYPE = PrimitiveType; + using LENTRY_TYPE = PrimitiveType; + + auto &url_vector = args.data[0]; + auto &headers_vector = args.data[1]; + auto &headers_entry = ListVector::GetEntry(headers_vector); + auto &body_vector = args.data[2]; + auto &body_entry = ListVector::GetEntry(body_vector); + + GenericExecutor::ExecuteTernary( + url_vector, headers_vector, body_vector, result, args.size(), + [&](STRING_TYPE url, LENTRY_TYPE headers, LENTRY_TYPE params) { + std::string url_str = url.val.GetString(); + + // Use helper to setup client and parse URL + auto client_and_path = SetupHttpClient(url_str); + auto &client = client_and_path.first; + auto &path = client_and_path.second; + + // Prepare headers and parameters + duckdb_httplib_openssl::Headers header_map; + duckdb_httplib_openssl::Params params_map; + ConvertListEntryToMap(headers.val, headers_entry, header_map); + ConvertListEntryToMap(params.val, body_entry, params_map); + + // Make the POST request with headers and params + auto res = client.Post(path.c_str(), header_map, params_map); + if (res) { + std::string response = GetJsonResponse(res->status, res->reason, res->body); + return StringVector::AddString(result, response); + } else { + std::string response = GetJsonResponse(-1, GetHttpErrorMessage(res, "POST"), ""); + return StringVector::AddString(result, response); + } + }); +} + static void LoadInternal(DatabaseInstance &instance) { ScalarFunctionSet http_get("http_get"); @@ -276,6 +316,13 @@ static void LoadInternal(DatabaseInstance &instance) { {LogicalType::VARCHAR, LogicalType::MAP(LogicalType::VARCHAR, LogicalType::VARCHAR), LogicalType::JSON()}, LogicalType::JSON(), HTTPPostRequestFunction)); ExtensionUtil::RegisterFunction(instance, http_post); + + ScalarFunctionSet http_post_form("http_post_form"); + http_post_form.AddFunction(ScalarFunction( + {LogicalType::VARCHAR, LogicalType::MAP(LogicalType::VARCHAR, LogicalType::VARCHAR), + LogicalType::MAP(LogicalType::VARCHAR, LogicalType::VARCHAR)}, + LogicalType::JSON(), HTTPPostFormRequestFunction)); + ExtensionUtil::RegisterFunction(instance, http_post_form); } void HttpClientExtension::Load(DuckDB &db) { diff --git a/test/sql/httpclient.test b/test/sql/httpclient.test index 7744239..2631ebe 100644 --- a/test/sql/httpclient.test +++ b/test/sql/httpclient.test @@ -134,3 +134,68 @@ FROM ; ---- S2A_56LPN_20210930_0_L2A + +# Confirm the POST function with form request works +query III +WITH __input AS ( + SELECT + http_post_form( + 'https://httpbin.org/delay/0', + headers => MAP { + 'accept': 'application/json', + }, + params => MAP { + 'limit': 10 + } + ) AS res +), +__response AS ( + SELECT + (res->>'status')::INT AS status, + (res->>'reason') AS reason, + unnest( from_json(((res->>'body')::JSON)->'headers', '{"Host": "VARCHAR"}') ) AS features + FROM + __input +) +SELECT + __response.status, + __response.reason, + __response.Host AS host +FROM + __response +; +---- +200 OK httpbin.org + +# Confirm the POST function with form encoding transmits a single value +query III +WITH __input AS ( + SELECT + http_post_form( + 'https://httpbin.org/delay/0', + headers => MAP { + 'accept': 'application/json', + }, + params => MAP { + 'limit': 10 + } + ) AS res +), +__response AS ( + SELECT + (res->>'status')::INT AS status, + (res->>'reason') AS reason, + unnest( from_json(((res->>'body')::JSON)->'form', '{"limit": "VARCHAR"}') ) AS features + FROM + __input +) +SELECT + __response.status, + __response.reason, + __response.limit AS limit +FROM + __response +; +---- +200 OK 10 +