Skip to content

Commit 747cc78

Browse files
lovasoacursoragent
andauthored
sqlpage.fetch(null) = null (#1131)
* Fix: fetch(null) and fetch_with_meta(null) return null Co-authored-by: contact <contact@ophir.dev> * clean up implementation * update docs --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 26612c7 commit 747cc78

File tree

6 files changed

+56
-12
lines changed

6 files changed

+56
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- `curl -H "Accept: application/json" http://example.com/page.sql`: returns a json array
66
- `curl -H "Accept: application/x-ndjson" http://example.com/page.sql`: returns one json object per line.
77
- 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.
8+
- `sqlpage.fetch(null)` and `sqlpage.fetch_with_meta(null)` now return `null` instead of throwing an error.
89
- **New Function**: `sqlpage.set_variable(name, value)`
910
- Returns a URL with the specified variable set to the given value, preserving other existing variables.
1011
- This is a shorthand for `sqlpage.link(sqlpage.path(), json_patch(sqlpage.variables('get'), json_object(name, value)))`.

examples/official-site/sqlpage/migrations/40_fetch.sql

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ In this example, we use the complex form of the function to make an
3636
authenticated POST request, with custom request headers and a custom request body.
3737
3838
We use SQLite''s json functions to build the request body.
39-
See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for
39+
See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for
4040
more information on how to build JSON objects in your database.
4141
4242
```sql
@@ -94,7 +94,22 @@ The fetch function accepts either a URL string, or a JSON object with the follow
9494
If the request fails, this function throws an error, that will be displayed to the user.
9595
The response headers are not available for inspection.
9696
97-
If you need to handle errors or inspect the response headers, use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta).
97+
## Conditional data fetching
98+
99+
Since v0.40, `sqlpage.fetch(null)` returns null instead of throwing an error.
100+
This makes it easier to conditionnally query an API:
101+
102+
```sql
103+
set current_field_value = (select field from my_table where id = 1);
104+
set target_url = nullif(''http://example.com/api/field/1'', null); -- null if the field is currently null in the db
105+
set api_value = sqlpage.fetch($target_url); -- no http request made if the field is not null in the db
106+
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
107+
```
108+
109+
## Advanced usage
110+
111+
If you need to handle errors or inspect the response headers or the status code,
112+
use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta).
98113
'
99114
);
100115
INSERT INTO sqlpage_function_parameters (

src/webserver/database/sqlpage_functions/function_traits.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ impl<'a, T: BorrowFromStr<'a> + Sized + 'a> FunctionParamType<'a> for SqlPageFun
8484
}
8585
}
8686

87+
impl<'a, T: BorrowFromStr<'a> + Sized + 'a> FunctionParamType<'a>
88+
for Option<SqlPageFunctionParam<T>>
89+
{
90+
type TargetType = Option<T>;
91+
92+
fn from_args(
93+
arg: &mut std::vec::IntoIter<Option<Cow<'a, str>>>,
94+
) -> anyhow::Result<Self::TargetType> {
95+
let param = <Option<Cow<'a, str>>>::from_args(arg)?;
96+
let res = if let Some(param) = param {
97+
Some(T::borrow_from_str(param)?)
98+
} else {
99+
None
100+
};
101+
Ok(res)
102+
}
103+
}
104+
87105
pub(super) trait FunctionResultType<'a> {
88106
fn into_cow_result(self) -> anyhow::Result<Option<Cow<'a, str>>>;
89107
}

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use super::{ExecutionContext, RequestInfo};
22
use crate::webserver::{
33
database::{
4-
blob_to_data_url::vec_to_data_uri_with_mime, execute_queries::DbConn,
5-
sqlpage_functions::url_parameters::URLParameters,
4+
blob_to_data_url::vec_to_data_uri_with_mime,
5+
execute_queries::DbConn,
6+
sqlpage_functions::{http_fetch_request::HttpFetchRequest, url_parameters::URLParameters},
67
},
78
http_client::make_http_client,
89
request_variables::SetVariablesMap,
@@ -26,8 +27,8 @@ super::function_definition_macro::sqlpage_functions! {
2627
environment_variable(name: Cow<str>);
2728
exec((&RequestInfo), program_name: Cow<str>, args: Vec<Cow<str>>);
2829

29-
fetch((&RequestInfo), http_request: SqlPageFunctionParam<super::http_fetch_request::HttpFetchRequest<'_>>);
30-
fetch_with_meta((&RequestInfo), http_request: SqlPageFunctionParam<super::http_fetch_request::HttpFetchRequest<'_>>);
30+
fetch((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);
31+
fetch_with_meta((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);
3132

3233
hash_password(password: Option<String>);
3334
header((&RequestInfo), name: Cow<str>);
@@ -185,8 +186,11 @@ fn prepare_request_body(
185186

186187
async fn fetch(
187188
request: &RequestInfo,
188-
http_request: super::http_fetch_request::HttpFetchRequest<'_>,
189-
) -> anyhow::Result<String> {
189+
http_request: Option<HttpFetchRequest<'_>>,
190+
) -> anyhow::Result<Option<String>> {
191+
let Some(http_request) = http_request else {
192+
return Ok(None);
193+
};
190194
let client = make_http_client(&request.app_state.config)
191195
.with_context(|| "Unable to create an HTTP client")?;
192196
let req = build_request(&client, &http_request)?;
@@ -219,7 +223,7 @@ async fn fetch(
219223
.to_vec();
220224
let response_str = decode_response(body, http_request.response_encoding.as_deref())?;
221225
log::debug!("Fetch response: {response_str}");
222-
Ok(response_str)
226+
Ok(Some(response_str))
223227
}
224228

225229
fn decode_response(response: Vec<u8>, encoding: Option<&str>) -> anyhow::Result<String> {
@@ -259,10 +263,14 @@ fn decode_response(response: Vec<u8>, encoding: Option<&str>) -> anyhow::Result<
259263

260264
async fn fetch_with_meta(
261265
request: &RequestInfo,
262-
http_request: super::http_fetch_request::HttpFetchRequest<'_>,
263-
) -> anyhow::Result<String> {
266+
http_request: Option<HttpFetchRequest<'_>>,
267+
) -> anyhow::Result<Option<String>> {
264268
use serde::{ser::SerializeMap, Serializer};
265269

270+
let Some(http_request) = http_request else {
271+
return Ok(None);
272+
};
273+
266274
let client = make_http_client(&request.app_state.config)
267275
.with_context(|| "Unable to create an HTTP client")?;
268276
let req = build_request(&client, &http_request)?;
@@ -337,7 +345,7 @@ async fn fetch_with_meta(
337345

338346
obj.end()?;
339347
let return_value = String::from_utf8(resp_str)?;
340-
Ok(return_value)
348+
Ok(Some(return_value))
341349
}
342350

343351
pub(crate) async fn hash_password(password: Option<String>) -> anyhow::Result<Option<String>> {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select null as expected, sqlpage.fetch(null) as actual;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select null as expected, sqlpage.fetch_with_meta(null) as actual;

0 commit comments

Comments
 (0)