Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ignore commits from showing up on git diffs.

# === large formatting commits ===
# TODO :: add commit once merged into mainline
Comment on lines +1 to +4

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is to ignore the formatting commit so that the blame correctly attributes the person who wrote it rather than the person who formatted it. if you don't care about that I can take it out

2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ official documentation website sql tables:
#### Project Conventions

- Built-in UI component templates: `sqlpage/templates/*.handlebars`; header/control components: `src/render.rs`.
- SQLPage functions: one `async fn` module under `src/webserver/database/sqlpage_functions/functions/`, registered with `sqlpage_functions!` in `functions.rs`.
- SQLPage functions: one `async fn` module under `src/webserver/database/sqlpage_functions/functions/`, declared with `mod` and registered with `sqlpage_functions!` in `functions.rs`. See its [README](./src/webserver/database/sqlpage_functions/README.md).
- [Configuration](./configuration.md): see [AppConfig](./src/app_config.rs)
- Routing: file-based in `src/webserver/routing.rs`. Missing paths use the nearest ancestor `404.sql`; without one, HTML uses `src/default_404.sql` and other formats receive a plain-text 404.
- Follow patterns from similar modules before introducing new abstractions.
Expand Down
9 changes: 6 additions & 3 deletions src/webserver/database/sqlpage_functions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ pub(super) async fn example(request: &RequestInfo, value: Option<Cow<'_, str>>)
}
```

To add `sqlpage.example`, create `functions/example.rs` and add it to the
[`sqlpage_functions!`](function_traits.rs) call in [`functions.rs`](functions.rs):
To add `sqlpage.example`, create `functions/example.rs`, declare its module in
[`functions.rs`](functions.rs) and add it to the [`sqlpage_functions!`](function_traits.rs) call in
the same file:

```rust
mod example;

sqlpage_functions! {
// ...
example,
}
```

The [`sqlpage_functions!`](function_traits.rs) macro declares the modules and generates the
The [`sqlpage_functions!`](function_traits.rs) macro generates the
`SqlPageFunctionName` enum the SQL engine dispatches on. Per-function argument extraction, dispatch,
and return-value conversion are handled generically in [`function_traits.rs`](function_traits.rs) by
the `Extract`, `Handler`, and `IntoCowResult` traits. A function's argument and return types are read
Expand Down
6 changes: 1 addition & 5 deletions src/webserver/database/sqlpage_functions/function_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,9 @@ impl<'a, T: IntoCow<'a>> IntoCow<'a> for Option<T> {
}
}

/// Declares the listed function modules and builds the [`SqlPageFunctionName`] dispatch enum from
/// them.
/// Builds the [`SqlPageFunctionName`] dispatch enum from the listed function modules.
macro_rules! sqlpage_functions {
($($func:ident),* $(,)?) => {
$(
mod $func;
)*

/// One variant per built-in `sqlpage.*` function.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
Expand Down
76 changes: 72 additions & 4 deletions src/webserver/database/sqlpage_functions/functions.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,53 @@
//! Built-in `SQLPage` SQL functions.
//!
//! Every function is a plain `async fn` in its own module under [`functions/`](self). To add one,
//! create `functions/<name>.rs` with an `async fn <name>` and add it to the
//! [`sqlpage_functions!`](super::function_traits::sqlpage_functions) call below. The macro declares
//! the module and adds it to the dispatch enum. Argument conversion and
//! dispatch are handled generically in [`super::function_traits`].
//! create `functions/<name>.rs` with an `async fn <name>`, declare the module below and add it to
//! the [`sqlpage_functions!`](super::function_traits::sqlpage_functions) call. Argument conversion
//! and dispatch are handled generically in [`super::function_traits`].

use std::fmt::Write;

use super::function_traits::sqlpage_functions;

mod basic_auth_password;
mod basic_auth_username;
mod client_ip;
mod configuration_directory;
mod cookie;
mod current_working_directory;
mod environment_variable;
mod exec;
mod fetch;
mod fetch_with_meta;
mod hash_password;
mod header;
mod headers;
mod hmac;
mod link;
mod oidc_logout_url;
mod path;
mod persist_uploaded_file;
mod protocol;
mod random_string;
mod read_file_as_data_url;
mod read_file_as_text;
mod regex_match;
mod request_body;
mod request_body_base64;
mod request_method;
mod run_sql;
mod send_mail;
mod set_variable;
mod uploaded_file_mime_type;
mod uploaded_file_name;
mod uploaded_file_path;
mod url_encode;
mod user_info;
mod user_info_token;
mod variables;
mod version;
mod web_root;
Comment on lines +12 to +49

@lovasoa lovasoa Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, we need the macro to be able to keep the files in the functions folder and the actual functions in sync. this is documented, we can't change the behavior without changing the instructions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is some prior art to doing it this way :: apache/datafusion#9281

that said I have added a test to check for this assumption so that it pragmatically holds. While I do believe in my engineering skills, I do not consider myself an expert in Rust yet so not sure if there is a better approach here. lmk if you have any ideas, happy to iterate and learn along the way 😄


sqlpage_functions! {
basic_auth_password,
basic_auth_username,
Expand Down Expand Up @@ -82,3 +120,33 @@ fn supported_function_list() -> String {
}
supported
}

#[cfg(test)]
mod tests {
use super::SqlPageFunctionName;
use std::collections::BTreeSet;

#[test]
fn functions_directory_matches_registered_functions() {
let directory = concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/webserver/database/sqlpage_functions/functions"
);
let files: BTreeSet<String> = std::fs::read_dir(directory)
.expect("functions directory")
.map(|entry| entry.expect("directory entry").path())
.filter(|path| path.extension().is_some_and(|extension| extension == "rs"))
.map(|path| {
path.file_stem()
.expect("file stem")
.to_string_lossy()
.into_owned()
})
.collect();
let registered: BTreeSet<String> = SqlPageFunctionName::ALL
.iter()
.map(|function| function.name().to_owned())
.collect();
assert_eq!(files, registered);
}
}
5 changes: 4 additions & 1 deletion src/webserver/database/sqlpage_functions/functions/cookie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use std::borrow::Cow;

use crate::webserver::{http_request_info::RequestInfo, single_or_vec::SingleOrVec};

pub(super) async fn cookie<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option<Cow<'a, str>> {
pub(super) async fn cookie<'a>(
request: &'a RequestInfo,
name: Cow<'a, str>,
) -> Option<Cow<'a, str>> {
request.cookies.get(&*name).map(SingleOrVec::as_json_str)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::borrow::Cow;
use anyhow::Context;

/// Returns the value of an environment variable.
pub(super) async fn environment_variable(name: Cow<'_, str>) -> anyhow::Result<Option<Cow<'_, str>>> {
pub(super) async fn environment_variable(
name: Cow<'_, str>,
) -> anyhow::Result<Option<Cow<'_, str>>> {
match std::env::var(&*name) {
Ok(value) => Ok(Some(Cow::Owned(value))),
Err(std::env::VarError::NotPresent) if name.contains(['=', '\0']) => anyhow::bail!(
Expand Down
7 changes: 3 additions & 4 deletions src/webserver/database/sqlpage_functions/functions/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use tracing::Instrument;

use crate::webserver::{
database::sqlpage_functions::http_fetch_request::HttpFetchRequest,
http_client::make_http_client,
http_request_info::RequestInfo,
http_client::make_http_client, http_request_info::RequestInfo,
};

pub(super) fn build_request<'a>(
Expand Down Expand Up @@ -94,8 +93,8 @@ pub(super) async fn fetch(

async {
let response_result = send_request(request, &http_request)?.await;
let mut response = response_result
.map_err(|e| anyhow!("Unable to fetch {}: {e}", http_request.url))?;
let mut response =
response_result.map_err(|e| anyhow!("Unable to fetch {}: {e}", http_request.url))?;

tracing::Span::current().record(
otel::HTTP_RESPONSE_STATUS_CODE,
Expand Down
5 changes: 4 additions & 1 deletion src/webserver/database/sqlpage_functions/functions/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use std::borrow::Cow;

use crate::webserver::{http_request_info::RequestInfo, single_or_vec::SingleOrVec};

pub(super) async fn header<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option<Cow<'a, str>> {
pub(super) async fn header<'a>(
request: &'a RequestInfo,
name: Cow<'a, str>,
) -> Option<Cow<'a, str>> {
let lower_name = name.to_ascii_lowercase();
request
.headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ pub(super) async fn persist_uploaded_file<'a>(
}

#[cfg(unix)]
pub(super) async fn set_file_mode(path: &std::path::Path, mode: Option<&str>) -> anyhow::Result<()> {
pub(super) async fn set_file_mode(
path: &std::path::Path,
mode: Option<&str>,
) -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt;
let mode = if let Some(mode) = mode {
u32::from_str_radix(mode, 8)
Expand All @@ -87,6 +90,9 @@ pub(super) async fn set_file_mode(path: &std::path::Path, mode: Option<&str>) ->
}

#[cfg(not(unix))]
pub(super) async fn set_file_mode(_path: &std::path::Path, _mode: Option<&str>) -> anyhow::Result<()> {
pub(super) async fn set_file_mode(
_path: &std::path::Path,
_mode: Option<&str>,
) -> anyhow::Result<()> {
Ok(())
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/// Returns a random string of the specified length.
pub(super) async fn random_string(len: usize) -> anyhow::Result<String> {
// OsRng can block on Linux, so we run this on a blocking thread.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ use anyhow::Context;
use crate::{
filesystem::FileAccess,
webserver::{
database::blob_to_data_url::vec_to_data_uri_with_mime,
http_request_info::RequestInfo,
database::blob_to_data_url::vec_to_data_uri_with_mime, http_request_info::RequestInfo,
},
};

use super::uploaded_file_mime_type::{mime_from_upload_path, mime_guess_from_filename};

pub(super) async fn read_file_bytes(request: &RequestInfo, path_str: &str) -> Result<Vec<u8>, anyhow::Error> {
pub(super) async fn read_file_bytes(
request: &RequestInfo,
path_str: &str,
) -> Result<Vec<u8>, anyhow::Error> {
let path = std::path::Path::new(path_str);
// If the path is relative, it's relative to the web root, not the current working directory,
// and it can be fetched from the on-database filesystem table
Expand Down
Loading