From f36cc2d142cf712ef66ac6e799b847511e6d3170 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sat, 4 Apr 2026 12:41:06 +0200 Subject: [PATCH 1/3] get_path_segment and is_path_matching sqlpage functions --- .../sqlpage/migrations/08_functions.sql | 111 ++++++++++++++++++ .../database/sqlpage_functions/functions.rs | 54 +++++++++ 2 files changed, 165 insertions(+) diff --git a/examples/official-site/sqlpage/migrations/08_functions.sql b/examples/official-site/sqlpage/migrations/08_functions.sql index 3a5fd4a4..3cd0b032 100644 --- a/examples/official-site/sqlpage/migrations/08_functions.sql +++ b/examples/official-site/sqlpage/migrations/08_functions.sql @@ -478,3 +478,114 @@ VALUES ( 'The string to encode.', 'TEXT' ); +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'get_path_segment', + '0.44', + 'cut', + 'Returns the Nth segment of a path. + +### Example + +#### Get the user id from the path ''/api/v1/user/42'' + +```sql +select ''text'' AS component; +select sqlpage.get_path_segment(''/api/v1/user/42'',4) AS contents; +``` + +#### Result + +`42` + +#### Notes + +- Segments are separated by ''/''. +- If the path is NULL, or the index is out of bounds, it will return an empty string. +- The index is 1-based, so the first segment is at index 1. +' + ); +INSERT INTO sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES ( + 'get_path_segment', + 1, + 'path', + 'The path to extract the segment from.', + 'TEXT' + ), + ( + 'get_path_segment', + 2, + 'index', + 'The index of the segment to extract. 1-based.', + 'INTEGER' + ); +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'is_path_matching', + '0.44', + 'flip-horizontal', + 'Returns the path if it matches the pattern, otherwise returns an empty string. + +### Example + +#### Check if the current path matches a pattern + +```sql +select ''text'' AS component; +select sqlpage.is_path_matching(sqlpage.path(),''/api/%/%'') AS contents; +``` + +#### Result + +`/api/v1/user/42` + +#### Notes + +- The pattern is a list of segments separated by ''/''. +- If the path is NULL, or the pattern is NULL, it will return an empty string. +- If the path and pattern have different numbers of segments, it will return an empty string. +- If the path and pattern have the same number of segments, it will compare them segment by segment. +- If a segment in the pattern is ''%'', it will match any non-empty segment in the path. +- If a segment in the pattern is a string, it will match the corresponding segment in the path if they are equal. +- If all segments match, it will return the path. +- Otherwise, it will return an empty string. +' + ); +INSERT INTO sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES ( + 'is_path_matching', + 1, + 'path', + 'The path to match against.', + 'TEXT' + ), + ( + 'is_path_matching', + 2, + 'pattern', + 'The template pattern.', + 'TEXT' + ); \ No newline at end of file diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index b78f64e0..6ed85c86 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -30,6 +30,9 @@ super::function_definition_macro::sqlpage_functions! { environment_variable(name: Cow); exec((&RequestInfo), program_name: Cow, args: Vec>); + get_path_segment(path: Option>, index: SqlPageFunctionParam); + is_path_matching(path: Option>, pattern: Option>); + fetch((&RequestInfo), http_request: Option>>); fetch_with_meta((&RequestInfo), http_request: Option>>); @@ -809,6 +812,57 @@ async fn url_encode(raw_text: Option>) -> Option> { }) } +/// Returns the path if it matches the pattern, otherwise returns an empty string. +async fn is_path_matching<'a>( + path: Option>, + pattern: Option>, +) -> Option> { + let (Some(p_val), Some(pattern)) = (path.as_ref(), pattern) else { + return Some(Cow::Borrowed("")); + }; + + let path_segments: Vec<&str> = p_val.split('/').collect(); + let pattern_segments: Vec<&str> = pattern.split('/').collect(); + + if path_segments.len() != pattern_segments.len() { + return Some(Cow::Borrowed("")); + } + + for (ps, pat_s) in path_segments.iter().zip(pattern_segments.iter()) { + if *pat_s == "%" { + if ps.is_empty() { + return Some(Cow::Borrowed("")); + } + } else { + let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy(); + let pat_s_decoded = percent_encoding::percent_decode_str(pat_s).decode_utf8_lossy(); + if ps_decoded != pat_s_decoded { + return Some(Cow::Borrowed("")); + } + } + } + + path +} + +/// Returns the Nth segment of a path. Segments are separated by '/'. +async fn get_path_segment(path: Option>, index: usize) -> String { + let Some(path) = path else { + return String::new(); + }; + if index == 0 { + return String::new(); + } + let segment = path + .trim_start_matches('/') + .split('/') + .nth(index - 1) + .unwrap_or_default(); + percent_encoding::percent_decode_str(segment) + .decode_utf8_lossy() + .into_owned() +} + /// Returns all variables in the request as a JSON object. async fn variables<'a>( request: &'a ExecutionContext, From 05fff46daac37b6fe2bf702a8c52dda3bfbe0891 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sat, 4 Apr 2026 16:28:16 +0200 Subject: [PATCH 2/3] Fix for an error in example. --- examples/official-site/sqlpage/migrations/08_functions.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/official-site/sqlpage/migrations/08_functions.sql b/examples/official-site/sqlpage/migrations/08_functions.sql index 3cd0b032..90aeb5b4 100644 --- a/examples/official-site/sqlpage/migrations/08_functions.sql +++ b/examples/official-site/sqlpage/migrations/08_functions.sql @@ -549,7 +549,7 @@ VALUES ( ```sql select ''text'' AS component; -select sqlpage.is_path_matching(sqlpage.path(),''/api/%/%'') AS contents; +select sqlpage.is_path_matching(sqlpage.path(),''/api/%/%/%'') AS contents; ``` #### Result From 207cb8f8b0eb012c5fbb6c119fee8f41cdfdc58b Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Mon, 6 Apr 2026 17:33:54 +0200 Subject: [PATCH 3/3] If a segment in the pattern is ''%d'', it will match any non-empty segment that is an integer. If a segment in the pattern is ''%s'', it will match any non-empty segment in the path --- .../official-site/sqlpage/migrations/08_functions.sql | 8 ++++---- src/webserver/database/sqlpage_functions/functions.rs | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/official-site/sqlpage/migrations/08_functions.sql b/examples/official-site/sqlpage/migrations/08_functions.sql index 90aeb5b4..0a8b4442 100644 --- a/examples/official-site/sqlpage/migrations/08_functions.sql +++ b/examples/official-site/sqlpage/migrations/08_functions.sql @@ -549,12 +549,12 @@ VALUES ( ```sql select ''text'' AS component; -select sqlpage.is_path_matching(sqlpage.path(),''/api/%/%/%'') AS contents; +select sqlpage.is_path_matching(sqlpage.path(),''/api/v1/user/%d/%s'') AS contents; ``` #### Result -`/api/v1/user/42` +`/api/v1/user/42/name` #### Notes @@ -562,8 +562,8 @@ select sqlpage.is_path_matching(sqlpage.path(),''/api/%/%/%'') AS contents; - If the path is NULL, or the pattern is NULL, it will return an empty string. - If the path and pattern have different numbers of segments, it will return an empty string. - If the path and pattern have the same number of segments, it will compare them segment by segment. -- If a segment in the pattern is ''%'', it will match any non-empty segment in the path. -- If a segment in the pattern is a string, it will match the corresponding segment in the path if they are equal. +- If a segment in the pattern is ''%d'', it will match any non-empty segment that is an integer. +- If a segment in the pattern is ''%s'', it will match any non-empty segment in the path. - If all segments match, it will return the path. - Otherwise, it will return an empty string. ' diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 6ed85c86..66d08f04 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -829,10 +829,15 @@ async fn is_path_matching<'a>( } for (ps, pat_s) in path_segments.iter().zip(pattern_segments.iter()) { - if *pat_s == "%" { + if *pat_s == "%s" { if ps.is_empty() { return Some(Cow::Borrowed("")); } + } else if *pat_s == "%d" { + let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy(); + if ps_decoded.is_empty() || ps_decoded.parse::().is_err() { + return Some(Cow::Borrowed("")); + } } else { let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy(); let pat_s_decoded = percent_encoding::percent_decode_str(pat_s).decode_utf8_lossy();