Skip to content

Commit 173283c

Browse files
committed
Make URL and POST parameters immutable, separate from SET variables
- URL and POST parameters are now immutable after request initialization - SET command creates user-defined variables in separate namespace - Variable lookup: SET variables shadow request parameters - Added sqlpage.variables('set') to inspect user-defined variables - Simplified API: most functions now use &RequestInfo instead of &mut - All tests passing (151 total)
1 parent 3276b5a commit 173283c

File tree

8 files changed

+135
-62
lines changed

8 files changed

+135
-62
lines changed

examples/official-site/extensions-to-sql.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,15 @@ SELECT (select 1) AS one;
7575
## Variables
7676

7777
SQLPage communicates information about incoming HTTP requests to your SQL code through prepared statement variables.
78-
You can use
79-
- `$var` to reference a GET variable (an URL parameter),
80-
- `:var` to reference a POST variable (a value filled by an user in a form field),
81-
- `set var = ...` to set the value of `$var`.
78+
79+
### Variable Types and Mutability
80+
81+
There are two types of variables in SQLPage:
82+
83+
1. **Request parameters** (immutable): URL parameters and form data from the HTTP request
84+
2. **User-defined variables** (mutable): Variables created with the `SET` command
85+
86+
Request parameters cannot be modified after the request is received. This ensures the original request data remains intact throughout request processing.
8287

8388
### POST parameters
8489

@@ -111,20 +116,30 @@ When a URL parameter is not set, its value is `NULL`.
111116

112117
### The SET command
113118

114-
`SET` stores a value in SQLPage (not in the database). Only strings and `NULL` are stored.
119+
`SET` creates or updates a user-defined variable in SQLPage (not in the database). Only strings and `NULL` are stored.
115120

116121
```sql
117122
-- Give a default value to a variable
118123
SET post_id = COALESCE($post_id, 0);
124+
125+
-- User-defined variables shadow URL parameters with the same name
126+
SET my_var = 'custom value'; -- This value takes precedence over ?my_var=...
119127
```
120128

129+
**Variable Lookup Precedence:**
130+
- `$var`: checks user-defined variables first, then URL parameters
131+
- `:var`: checks user-defined variables first, then POST parameters
132+
133+
This means `SET` variables always take precedence over request parameters when using `$var` or `:var` syntax.
134+
135+
**How SET works:**
121136
- If the right-hand side is purely literals/variables, SQLPage computes it directly. See the section about *static simple select* above.
122137
- If it needs the database (for example, calls a database function), SQLPage runs an internal `SELECT` to compute it and stores the first column of the first row of results.
123138

124139
Only a single textual value (**string or `NULL`**) is stored.
125-
`set id = 1` will store the string `'1'`, not the number `1`.
140+
`SET id = 1` will store the string `'1'`, not the number `1`.
126141

127-
On databases with a strict type system, such as PostgreSQL, if you need a number, you will need to cast your variables: `select * from post where id = $id::int`.
142+
On databases with a strict type system, such as PostgreSQL, if you need a number, you will need to cast your variables: `SELECT * FROM post WHERE id = $id::int`.
128143

129144
Complex structures can be stored as json strings.
130145

examples/official-site/sqlpage/migrations/20_variables_function.sql

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,27 @@ VALUES (
99
'variables',
1010
'0.15.0',
1111
'variable',
12-
'Returns a JSON string containing all variables passed as URL parameters or posted through a form.
12+
'Returns a JSON string containing variables from the HTTP request and user-defined variables.
1313
1414
The database''s json handling functions can then be used to process the data.
1515
16+
## Variable Types
17+
18+
SQLPage distinguishes between three types of variables:
19+
20+
- **GET variables**: URL parameters from the query string (immutable)
21+
- **POST variables**: Form data from POST requests (immutable)
22+
- **SET variables**: User-defined variables created with the `SET` command (mutable)
23+
24+
## Usage
25+
26+
- `sqlpage.variables()` - returns all variables (GET, POST, and SET combined, with SET variables taking precedence)
27+
- `sqlpage.variables(''get'')` - returns only URL parameters
28+
- `sqlpage.variables(''post'')` - returns only POST form data
29+
- `sqlpage.variables(''set'')` - returns only user-defined variables created with `SET`
30+
31+
When a SET variable has the same name as a GET or POST variable, the SET variable takes precedence in the combined result.
32+
1633
## Example: a form with a variable number of fields
1734
1835
### Making a form based on questions in a database table
@@ -95,6 +112,6 @@ VALUES (
95112
'variables',
96113
1,
97114
'method',
98-
'Optional. The HTTP request method (GET or POST). Must be a literal string. When not provided, all variables are returned.',
115+
'Optional. Filter variables by source: ''get'' (URL parameters), ''post'' (form data), or ''set'' (user-defined variables). When not provided, all variables are returned with SET variables taking precedence over request parameters.',
99116
'TEXT'
100117
);

src/webserver/database/execute_queries.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl Database {
4444

4545
pub fn stream_query_results_with_conn<'a>(
4646
sql_file: &'a ParsedSqlFile,
47-
request: &'a mut RequestInfo,
47+
request: &'a RequestInfo,
4848
db_connection: &'a mut DbConn,
4949
) -> impl Stream<Item = DbItem> + 'a {
5050
let source_file = &sql_file.source_path;
@@ -175,7 +175,7 @@ async fn extract_req_param_as_json(
175175
/// This allows recursive calls.
176176
pub fn stream_query_results_boxed<'a>(
177177
sql_file: &'a ParsedSqlFile,
178-
request: &'a mut RequestInfo,
178+
request: &'a RequestInfo,
179179
db_connection: &'a mut DbConn,
180180
) -> Pin<Box<dyn Stream<Item = DbItem> + 'a>> {
181181
Box::pin(stream_query_results_with_conn(
@@ -187,7 +187,7 @@ pub fn stream_query_results_boxed<'a>(
187187

188188
async fn execute_set_variable_query<'a>(
189189
db_connection: &'a mut DbConn,
190-
request: &'a mut RequestInfo,
190+
request: &'a RequestInfo,
191191
variable: &StmtParam,
192192
statement: &StmtWithParams,
193193
source_file: &Path,
@@ -209,7 +209,7 @@ async fn execute_set_variable_query<'a>(
209209
}
210210
};
211211

212-
let (vars, name) = vars_and_name(request, variable)?;
212+
let (mut vars, name) = vars_and_name(request, variable)?;
213213

214214
if let Some(value) = value {
215215
log::debug!("Setting variable {name} to {value:?}");
@@ -223,7 +223,7 @@ async fn execute_set_variable_query<'a>(
223223

224224
async fn execute_set_simple_static<'a>(
225225
db_connection: &'a mut DbConn,
226-
request: &'a mut RequestInfo,
226+
request: &'a RequestInfo,
227227
variable: &StmtParam,
228228
value: &SimpleSelectValue,
229229
_source_file: &Path,
@@ -241,7 +241,7 @@ async fn execute_set_simple_static<'a>(
241241
}
242242
};
243243

244-
let (vars, name) = vars_and_name(request, variable)?;
244+
let (mut vars, name) = vars_and_name(request, variable)?;
245245

246246
if let Some(value) = value_str {
247247
log::debug!("Setting variable {name} to static value {value:?}");
@@ -254,20 +254,13 @@ async fn execute_set_simple_static<'a>(
254254
}
255255

256256
fn vars_and_name<'a, 'b>(
257-
request: &'a mut RequestInfo,
257+
request: &'a RequestInfo,
258258
variable: &'b StmtParam,
259-
) -> anyhow::Result<(&'a mut HashMap<String, SingleOrVec>, &'b str)> {
259+
) -> anyhow::Result<(std::cell::RefMut<'a, HashMap<String, SingleOrVec>>, &'b str)> {
260260
match variable {
261-
StmtParam::PostOrGet(name) => {
262-
if request.post_variables.contains_key(name) {
263-
log::warn!("Deprecation warning! Setting the value of ${name}, but there is already a form field named :{name}. This will stop working soon. Please rename the variable, or use :{name} directly if you intended to overwrite the posted form field value.");
264-
Ok((&mut request.post_variables, name))
265-
} else {
266-
Ok((&mut request.get_variables, name))
267-
}
261+
StmtParam::PostOrGet(name) | StmtParam::Get(name) | StmtParam::Post(name) => {
262+
Ok((request.set_variables.borrow_mut(), name))
268263
}
269-
StmtParam::Get(name) => Ok((&mut request.get_variables, name)),
270-
StmtParam::Post(name) => Ok((&mut request.post_variables, name)),
271264
_ => Err(anyhow!(
272265
"Only GET and POST variables can be set, not {variable:?}"
273266
)),

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -569,10 +569,10 @@ async fn run_sql<'a>(
569569
)
570570
.await
571571
.with_context(|| format!("run_sql: invalid path {sql_file_path:?}"))?;
572-
let mut tmp_req = if let Some(variables) = variables {
573-
let mut tmp_req = request.clone_without_variables();
572+
let tmp_req = if let Some(variables) = variables {
573+
let tmp_req = request.clone_without_variables();
574574
let variables: ParamMap = serde_json::from_str(&variables)?;
575-
tmp_req.get_variables = variables;
575+
tmp_req.set_variables.replace(variables);
576576
tmp_req
577577
} else {
578578
request.clone()
@@ -589,7 +589,7 @@ async fn run_sql<'a>(
589589
let mut results_stream =
590590
crate::webserver::database::execute_queries::stream_query_results_boxed(
591591
&sql_file,
592-
&mut tmp_req,
592+
&tmp_req,
593593
db_connection,
594594
);
595595
let mut json_results_bytes = Vec::new();
@@ -684,22 +684,30 @@ async fn variables<'a>(
684684
) -> anyhow::Result<String> {
685685
Ok(if let Some(get_or_post) = get_or_post {
686686
if get_or_post.eq_ignore_ascii_case("get") {
687-
serde_json::to_string(&request.get_variables)?
687+
serde_json::to_string(&request.url_params)?
688688
} else if get_or_post.eq_ignore_ascii_case("post") {
689689
serde_json::to_string(&request.post_variables)?
690+
} else if get_or_post.eq_ignore_ascii_case("set") {
691+
serde_json::to_string(&*request.set_variables.borrow())?
690692
} else {
691693
return Err(anyhow!(
692-
"Expected 'get' or 'post' as the argument to sqlpage.all_variables"
694+
"Expected 'get', 'post', or 'set' as the argument to sqlpage.variables"
693695
));
694696
}
695697
} else {
696698
use serde::{ser::SerializeMap, Serializer};
697699
let mut res = Vec::new();
698700
let mut serializer = serde_json::Serializer::new(&mut res);
699-
let len = request.get_variables.len() + request.post_variables.len();
701+
let set_vars = request.set_variables.borrow();
702+
let len = request.url_params.len() + request.post_variables.len() + set_vars.len();
700703
let mut ser = serializer.serialize_map(Some(len))?;
701-
let iter = request.get_variables.iter().chain(&request.post_variables);
702-
for (k, v) in iter {
704+
for (k, v) in &request.url_params {
705+
ser.serialize_entry(k, v)?;
706+
}
707+
for (k, v) in &request.post_variables {
708+
ser.serialize_entry(k, v)?;
709+
}
710+
for (k, v) in &*set_vars {
703711
ser.serialize_entry(k, v)?;
704712
}
705713
ser.end()?;

src/webserver/database/syntax_tree.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -156,24 +156,24 @@ pub(super) async fn extract_req_param<'a>(
156156
) -> anyhow::Result<Option<Cow<'a, str>>> {
157157
Ok(match param {
158158
// sync functions
159-
StmtParam::Get(x) => request.get_variables.get(x).map(SingleOrVec::as_json_str),
160-
StmtParam::Post(x) => request.post_variables.get(x).map(SingleOrVec::as_json_str),
159+
StmtParam::Get(x) => request.url_params.get(x).map(SingleOrVec::as_json_str),
160+
StmtParam::Post(x) => {
161+
if let Some(val) = request.set_variables.borrow().get(x) {
162+
Some(Cow::Owned(val.as_json_str().into_owned()))
163+
} else {
164+
request.post_variables.get(x).map(SingleOrVec::as_json_str)
165+
}
166+
}
161167
StmtParam::PostOrGet(x) => {
162-
let post_val = request.post_variables.get(x);
163-
let get_val = request.get_variables.get(x);
164-
if let Some(v) = post_val {
165-
if let Some(get_val) = get_val {
166-
log::warn!(
167-
"Deprecation warning! There is both a URL parameter named '{x}' with value '{get_val}' and a form field named '{x}' with value '{v}'. \
168-
SQLPage is using the value from the form submission, but this is ambiguous, can lead to unexpected behavior, and will stop working in a future version of SQLPage. \
169-
To fix this, please rename the URL parameter to something else, and reference the form field with :{x}."
170-
);
171-
} else {
172-
log::warn!("Deprecation warning! ${x} was used to reference a form field value (a POST variable) instead of a URL parameter. This will stop working soon. Please use :{x} instead.");
173-
}
174-
Some(v.as_json_str())
168+
if let Some(val) = request.set_variables.borrow().get(x) {
169+
Some(Cow::Owned(val.as_json_str().into_owned()))
170+
} else if let Some(url_val) = request.url_params.get(x) {
171+
Some(url_val.as_json_str())
172+
} else if let Some(post_val) = request.post_variables.get(x) {
173+
log::warn!("Deprecation warning! ${x} was used to reference a form field value (a POST variable) instead of a URL parameter. This will stop working soon. Please use :{x} instead.");
174+
Some(post_val.as_json_str())
175175
} else {
176-
get_val.map(SingleOrVec::as_json_str)
176+
None
177177
}
178178
}
179179
StmtParam::Error(x) => anyhow::bail!("{x}"),

src/webserver/http.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ async fn render_sql(
176176
.clone()
177177
.into_inner();
178178

179-
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
179+
let req_param = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
180180
.await
181181
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
182182
log::debug!("Received a request with the following parameters: {req_param:?}");
@@ -187,14 +187,14 @@ async fn render_sql(
187187
let source_path: PathBuf = sql_file.source_path.clone();
188188
actix_web::rt::spawn(async move {
189189
let request_context = RequestContext {
190-
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
190+
is_embedded: req_param.url_params.contains_key("_sqlpage_embed"),
191191
source_path,
192192
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
193193
server_timing: Arc::clone(&req_param.server_timing),
194194
};
195195
let mut conn = None;
196196
let database_entries_stream =
197-
stream_query_results_with_conn(&sql_file, &mut req_param, &mut conn);
197+
stream_query_results_with_conn(&sql_file, &req_param, &mut conn);
198198
let database_entries_stream = stop_at_first_error(database_entries_stream);
199199
let response_with_writer = build_response_header_and_stream(
200200
Arc::clone(&app_state),

src/webserver/http_request_info.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use actix_web_httpauth::headers::authorization::Authorization;
1717
use actix_web_httpauth::headers::authorization::Basic;
1818
use anyhow::anyhow;
1919
use anyhow::Context;
20+
use std::cell::RefCell;
2021
use std::collections::HashMap;
2122
use std::net::IpAddr;
2223
use std::rc::Rc;
@@ -32,8 +33,9 @@ pub struct RequestInfo {
3233
pub method: actix_web::http::Method,
3334
pub path: String,
3435
pub protocol: String,
35-
pub get_variables: ParamMap,
36+
pub url_params: ParamMap,
3637
pub post_variables: ParamMap,
38+
pub set_variables: RefCell<ParamMap>,
3739
pub uploaded_files: Rc<HashMap<String, TempFile>>,
3840
pub headers: ParamMap,
3941
pub client_ip: Option<IpAddr>,
@@ -53,8 +55,9 @@ impl RequestInfo {
5355
method: self.method.clone(),
5456
path: self.path.clone(),
5557
protocol: self.protocol.clone(),
56-
get_variables: ParamMap::new(),
58+
url_params: ParamMap::new(),
5759
post_variables: ParamMap::new(),
60+
set_variables: RefCell::new(ParamMap::new()),
5861
uploaded_files: self.uploaded_files.clone(),
5962
headers: self.headers.clone(),
6063
client_ip: self.client_ip,
@@ -72,9 +75,12 @@ impl RequestInfo {
7275
impl Clone for RequestInfo {
7376
fn clone(&self) -> Self {
7477
let mut clone = self.clone_without_variables();
75-
clone.get_variables.clone_from(&self.get_variables);
78+
clone.url_params.clone_from(&self.url_params);
7679
clone.post_variables.clone_from(&self.post_variables);
7780
clone
81+
.set_variables
82+
.replace(self.set_variables.borrow().clone());
83+
clone
7884
}
7985
}
8086

@@ -116,8 +122,9 @@ pub(crate) async fn extract_request_info(
116122
method,
117123
path: req.path().to_string(),
118124
headers: param_map(headers),
119-
get_variables: param_map(get_variables),
125+
url_params: param_map(get_variables),
120126
post_variables: param_map(post_variables),
127+
set_variables: RefCell::new(ParamMap::new()),
121128
uploaded_files: Rc::new(HashMap::from_iter(uploaded_files)),
122129
client_ip,
123130
cookies: param_map(cookies),
@@ -295,7 +302,7 @@ mod test {
295302
.unwrap();
296303
assert_eq!(request_info.post_variables.len(), 0);
297304
assert_eq!(request_info.uploaded_files.len(), 0);
298-
assert_eq!(request_info.get_variables.len(), 0);
305+
assert_eq!(request_info.url_params.len(), 0);
299306
}
300307

301308
#[actix_web::test]
@@ -326,7 +333,7 @@ mod test {
326333
);
327334
assert_eq!(request_info.uploaded_files.len(), 0);
328335
assert_eq!(
329-
request_info.get_variables,
336+
request_info.url_params,
330337
vec![(
331338
"my_array".to_string(),
332339
SingleOrVec::Vec(vec!["5".to_string()])
@@ -374,8 +381,8 @@ mod test {
374381
assert_eq!(request_info.uploaded_files.len(), 1);
375382
let my_upload = &request_info.uploaded_files["my_uploaded_file"];
376383
assert_eq!(my_upload.file_name.as_ref().unwrap(), "test.txt");
377-
assert_eq!(request_info.get_variables.len(), 0);
384+
assert_eq!(request_info.url_params.len(), 0);
378385
assert_eq!(std::fs::read(&my_upload.file).unwrap(), b"Hello World");
379-
assert_eq!(request_info.get_variables.len(), 0);
386+
assert_eq!(request_info.url_params.len(), 0);
380387
}
381388
}

0 commit comments

Comments
 (0)