From 8a8831d9d2e37a1cecba29a0b90c516ce8a4cb8e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Nov 2025 20:08:51 +0000 Subject: [PATCH 1/3] Fix: fetch(null) and fetch_with_meta(null) return null Co-authored-by: contact --- CHANGELOG.md | 1 + .../database/sqlpage_functions/functions.rs | 29 ++++++++++++++----- tests/sql_test_files/data/fetch_null.sql | 1 + .../data/fetch_with_meta_null.sql | 1 + 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 tests/sql_test_files/data/fetch_null.sql create mode 100644 tests/sql_test_files/data/fetch_with_meta_null.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea53068..93640d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - `curl -H "Accept: application/json" http://example.com/page.sql`: returns a json array - `curl -H "Accept: application/x-ndjson" http://example.com/page.sql`: returns one json object per line. - Fixed a bug in `sqlpage.link`: a link with no path (link to the current page) and no url parameter now works as expected. It used to keep the existing url parameters instead of removing them. `sqlpage.link('', '{}')` now returns `'?'` instead of the empty string. + - `sqlpage.fetch(null)` and `sqlpage.fetch_with_meta(null)` now return `null` instead of throwing an error. - **New Function**: `sqlpage.set_variable(name, value)` - Returns a URL with the specified variable set to the given value, preserving other existing variables. - This is a shorthand for `sqlpage.link(sqlpage.path(), json_patch(sqlpage.variables('get'), json_object(name, value)))`. diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 450a85b8..77b1beee 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -1,3 +1,4 @@ +use super::function_traits::BorrowFromStr; use super::{ExecutionContext, RequestInfo}; use crate::webserver::{ database::{ @@ -26,8 +27,8 @@ super::function_definition_macro::sqlpage_functions! { environment_variable(name: Cow); exec((&RequestInfo), program_name: Cow, args: Vec>); - fetch((&RequestInfo), http_request: SqlPageFunctionParam>); - fetch_with_meta((&RequestInfo), http_request: SqlPageFunctionParam>); + fetch((&RequestInfo), http_request: Option>); + fetch_with_meta((&RequestInfo), http_request: Option>); hash_password(password: Option); header((&RequestInfo), name: Cow); @@ -185,8 +186,14 @@ fn prepare_request_body( async fn fetch( request: &RequestInfo, - http_request: super::http_fetch_request::HttpFetchRequest<'_>, -) -> anyhow::Result { + http_request: Option>, +) -> anyhow::Result> { + let Some(http_request_str) = http_request else { + return Ok(None); + }; + let http_request = + super::http_fetch_request::HttpFetchRequest::borrow_from_str(http_request_str) + .with_context(|| "Invalid http fetch request")?; let client = make_http_client(&request.app_state.config) .with_context(|| "Unable to create an HTTP client")?; let req = build_request(&client, &http_request)?; @@ -219,7 +226,7 @@ async fn fetch( .to_vec(); let response_str = decode_response(body, http_request.response_encoding.as_deref())?; log::debug!("Fetch response: {response_str}"); - Ok(response_str) + Ok(Some(response_str)) } fn decode_response(response: Vec, encoding: Option<&str>) -> anyhow::Result { @@ -259,9 +266,15 @@ fn decode_response(response: Vec, encoding: Option<&str>) -> anyhow::Result< async fn fetch_with_meta( request: &RequestInfo, - http_request: super::http_fetch_request::HttpFetchRequest<'_>, -) -> anyhow::Result { + http_request: Option>, +) -> anyhow::Result> { use serde::{ser::SerializeMap, Serializer}; + let Some(http_request_str) = http_request else { + return Ok(None); + }; + let http_request = + super::http_fetch_request::HttpFetchRequest::borrow_from_str(http_request_str) + .with_context(|| "Invalid http fetch request")?; let client = make_http_client(&request.app_state.config) .with_context(|| "Unable to create an HTTP client")?; @@ -337,7 +350,7 @@ async fn fetch_with_meta( obj.end()?; let return_value = String::from_utf8(resp_str)?; - Ok(return_value) + Ok(Some(return_value)) } pub(crate) async fn hash_password(password: Option) -> anyhow::Result> { diff --git a/tests/sql_test_files/data/fetch_null.sql b/tests/sql_test_files/data/fetch_null.sql new file mode 100644 index 00000000..9bff0482 --- /dev/null +++ b/tests/sql_test_files/data/fetch_null.sql @@ -0,0 +1 @@ +select null as expected, sqlpage.fetch(null) as actual; diff --git a/tests/sql_test_files/data/fetch_with_meta_null.sql b/tests/sql_test_files/data/fetch_with_meta_null.sql new file mode 100644 index 00000000..92b4fdaf --- /dev/null +++ b/tests/sql_test_files/data/fetch_with_meta_null.sql @@ -0,0 +1 @@ +select null as expected, sqlpage.fetch_with_meta(null) as actual; From b736c23df9fdb872153ab5ac08cf0bae2b93b51a Mon Sep 17 00:00:00 2001 From: lovasoa Date: Wed, 26 Nov 2025 21:43:22 +0100 Subject: [PATCH 2/3] clean up implementation --- .../sqlpage_functions/function_traits.rs | 18 +++++++++++++ .../database/sqlpage_functions/functions.rs | 25 ++++++++----------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/webserver/database/sqlpage_functions/function_traits.rs b/src/webserver/database/sqlpage_functions/function_traits.rs index 74bbbe22..b3125a0d 100644 --- a/src/webserver/database/sqlpage_functions/function_traits.rs +++ b/src/webserver/database/sqlpage_functions/function_traits.rs @@ -84,6 +84,24 @@ impl<'a, T: BorrowFromStr<'a> + Sized + 'a> FunctionParamType<'a> for SqlPageFun } } +impl<'a, T: BorrowFromStr<'a> + Sized + 'a> FunctionParamType<'a> + for Option> +{ + type TargetType = Option; + + fn from_args( + arg: &mut std::vec::IntoIter>>, + ) -> anyhow::Result { + let param = >>::from_args(arg)?; + let res = if let Some(param) = param { + Some(T::borrow_from_str(param)?) + } else { + None + }; + Ok(res) + } +} + pub(super) trait FunctionResultType<'a> { fn into_cow_result(self) -> anyhow::Result>>; } diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 1363b2a4..0ac7ed86 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -1,9 +1,9 @@ -use super::function_traits::BorrowFromStr; use super::{ExecutionContext, RequestInfo}; use crate::webserver::{ database::{ - blob_to_data_url::vec_to_data_uri_with_mime, execute_queries::DbConn, - sqlpage_functions::url_parameters::URLParameters, + blob_to_data_url::vec_to_data_uri_with_mime, + execute_queries::DbConn, + sqlpage_functions::{http_fetch_request::HttpFetchRequest, url_parameters::URLParameters}, }, http_client::make_http_client, request_variables::SetVariablesMap, @@ -27,8 +27,8 @@ super::function_definition_macro::sqlpage_functions! { environment_variable(name: Cow); exec((&RequestInfo), program_name: Cow, args: Vec>); - fetch((&RequestInfo), http_request: Option>); - fetch_with_meta((&RequestInfo), http_request: Option>); + fetch((&RequestInfo), http_request: Option>>); + fetch_with_meta((&RequestInfo), http_request: Option>>); hash_password(password: Option); header((&RequestInfo), name: Cow); @@ -186,14 +186,11 @@ fn prepare_request_body( async fn fetch( request: &RequestInfo, - http_request: Option>, + http_request: Option>, ) -> anyhow::Result> { - let Some(http_request_str) = http_request else { + let Some(http_request) = http_request else { return Ok(None); }; - let http_request = - super::http_fetch_request::HttpFetchRequest::borrow_from_str(http_request_str) - .with_context(|| "Invalid http fetch request")?; let client = make_http_client(&request.app_state.config) .with_context(|| "Unable to create an HTTP client")?; let req = build_request(&client, &http_request)?; @@ -266,15 +263,13 @@ fn decode_response(response: Vec, encoding: Option<&str>) -> anyhow::Result< async fn fetch_with_meta( request: &RequestInfo, - http_request: Option>, + http_request: Option>, ) -> anyhow::Result> { use serde::{ser::SerializeMap, Serializer}; - let Some(http_request_str) = http_request else { + + let Some(http_request) = http_request else { return Ok(None); }; - let http_request = - super::http_fetch_request::HttpFetchRequest::borrow_from_str(http_request_str) - .with_context(|| "Invalid http fetch request")?; let client = make_http_client(&request.app_state.config) .with_context(|| "Unable to create an HTTP client")?; From 0cb5d31f02040827caa17a0a5fb1972f91933451 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Wed, 26 Nov 2025 21:57:02 +0100 Subject: [PATCH 3/3] update docs --- .../sqlpage/migrations/40_fetch.sql | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/examples/official-site/sqlpage/migrations/40_fetch.sql b/examples/official-site/sqlpage/migrations/40_fetch.sql index 5a3d7b18..f93d6f50 100644 --- a/examples/official-site/sqlpage/migrations/40_fetch.sql +++ b/examples/official-site/sqlpage/migrations/40_fetch.sql @@ -36,7 +36,7 @@ In this example, we use the complex form of the function to make an authenticated POST request, with custom request headers and a custom request body. We use SQLite''s json functions to build the request body. -See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for +See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for more information on how to build JSON objects in your database. ```sql @@ -94,7 +94,22 @@ The fetch function accepts either a URL string, or a JSON object with the follow If the request fails, this function throws an error, that will be displayed to the user. The response headers are not available for inspection. -If you need to handle errors or inspect the response headers, use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta). +## Conditional data fetching + +Since v0.40, `sqlpage.fetch(null)` returns null instead of throwing an error. +This makes it easier to conditionnally query an API: + +```sql +set current_field_value = (select field from my_table where id = 1); +set target_url = nullif(''http://example.com/api/field/1'', null); -- null if the field is currently null in the db +set api_value = sqlpage.fetch($target_url); -- no http request made if the field is not null in the db +update my_table set field = $api_value where id = 1 and $api_value is not null; -- update the field only if it was not present before +``` + +## Advanced usage + +If you need to handle errors or inspect the response headers or the status code, +use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta). ' ); INSERT INTO sqlpage_function_parameters (