Skip to content
Merged
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
14 changes: 14 additions & 0 deletions Docs/04-advanced-features/databases.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ store row as rows[0]
store new_id as row["id"]
```

## Returning Results from Actions

`query` and `execute` — with or without `and parameters [...]` — can be
returned directly from an action, which keeps small data-access helpers to
one line:

```wfl
define action called ages_for_name with parameters conn and who:
return query conn with "SELECT age FROM users WHERE name = ?" and parameters [who]
end action

store rows as call ages_for_name with db and "Alice"
```

## Waiting Explicitly

Database statements run asynchronously inside WFL's runtime; you can make the
Expand Down
32 changes: 30 additions & 2 deletions TestPrograms/database_sqlite_test.wfl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// End-to-end database test using SQLite (no external services required)
// Exercises: open database, execute (DDL/INSERT/UPDATE/DELETE with bound
// parameters), query (SELECT with bound parameters), NULL handling, and
// close database.
// parameters), query (SELECT with bound parameters), NULL handling,
// returning parameterized query/execute results directly from actions,
// and close database.

display "=== Database SQLite E2E Test ==="

Expand Down Expand Up @@ -108,6 +109,33 @@ otherwise:
add 1 to failures
end check

// 9. Return a parameterized query directly from an action (issue #559)
define action called ages_for_name with parameters conn and who:
return query conn with "SELECT age FROM users WHERE name = ?" and parameters [who]
end action

store alice_rows as call ages_for_name with db and "Alice"
store alice_row as alice_rows[0]
check if alice_row["age"] is equal to 31:
display "✓ return query with parameters works from an action"
otherwise:
display "✗ FAILED: return query with parameters gave wrong row"
add 1 to failures
end check

// 10. Return a parameterized execute directly from an action
define action called add_user with parameters conn and user_name and user_age:
return execute conn with "INSERT INTO users (name, age) VALUES (?, ?)" and parameters [user_name and user_age]
end action

store add_result as call add_user with db and "Carol" and 20
check if add_result["affected_rows"] is equal to 1:
display "✓ return execute with parameters works from an action"
otherwise:
display "✗ FAILED: return execute with parameters affected_rows wrong"
add 1 to failures
end check

close database db
delete file at "wfl_e2e_database_test.db"
display "Database closed and file cleaned up"
Expand Down
12 changes: 12 additions & 0 deletions src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2605,6 +2605,18 @@ impl Analyzer {
} => {
self.analyze_expression(process_id);
}
Expression::DatabaseQuery {
db,
sql,
parameters,
..
} => {
self.analyze_expression(db);
self.analyze_expression(sql);
if let Some(params) = parameters {
self.analyze_expression(params);
}
}
}
}
}
Expand Down
182 changes: 113 additions & 69 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ fn expr_type(expr: &Expression) -> String {
format!("CurrentTimeFormatted '{format}'")
}
Expression::ProcessRunning { .. } => "ProcessRunning".to_string(),
Expression::DatabaseQuery { .. } => "DatabaseQuery".to_string(),
}
}

Expand Down Expand Up @@ -2884,75 +2885,17 @@ impl Interpreter {
line,
column,
} => {
let db_value = self.evaluate_expression(db, Rc::clone(&env)).await?;
let handle = match &db_value {
Value::Text(s) => s.clone(),
_ => {
return Err(RuntimeError::new(
format!("Expected a database handle, got {db_value:?}"),
*line,
*column,
));
}
};

let sql_value = self.evaluate_expression(sql, Rc::clone(&env)).await?;
let sql_str = match &sql_value {
Value::Text(s) => s.clone(),
_ => {
return Err(RuntimeError::new(
format!("Expected text for SQL statement, got {sql_value:?}"),
*line,
*column,
));
}
};

let params = match parameters {
Some(params_expr) => {
let params_value = self
.evaluate_expression(params_expr, Rc::clone(&env))
.await?;
match &params_value {
Value::List(list) => {
let mut sql_params = Vec::new();
for value in list.borrow().iter() {
sql_params.push(
database::value_to_sql_param(value)
.map_err(|e| RuntimeError::new(e, *line, *column))?,
);
}
sql_params
}
_ => {
return Err(RuntimeError::new(
format!(
"Expected a list of query parameters, got {params_value:?}"
),
*line,
*column,
));
}
}
}
None => Vec::new(),
};

let pool = self
.io_client
.get_database(&handle)
.await
.map_err(|e| RuntimeError::new(e, *line, *column))?;

let result = match kind {
crate::parser::ast::DatabaseQueryKind::Query => {
database::run_query(&pool, &sql_str, &params).await
}
crate::parser::ast::DatabaseQueryKind::Execute => {
database::run_execute(&pool, &sql_str, &params).await
}
}
.map_err(|e| RuntimeError::new(e, *line, *column))?;
let result = self
.evaluate_database_query(
db,
sql,
parameters.as_ref(),
*kind,
*line,
*column,
Rc::clone(&env),
)
.await?;

match env.borrow_mut().define(variable_name, result) {
Ok(_) => Ok((Value::Null, ControlFlow::None)),
Expand Down Expand Up @@ -6726,6 +6669,88 @@ impl Interpreter {
}
}

/// Run a database query/execute and return the result value. Shared by
/// `DatabaseQueryStatement` and the expression form (`return query ...`).
#[allow(clippy::too_many_arguments)]
async fn evaluate_database_query(
&self,
db: &Expression,
sql: &Expression,
parameters: Option<&Expression>,
kind: crate::parser::ast::DatabaseQueryKind,
line: usize,
column: usize,
env: Rc<RefCell<Environment>>,
) -> Result<Value, RuntimeError> {
let db_value = self.evaluate_expression(db, Rc::clone(&env)).await?;
let handle = match &db_value {
Value::Text(s) => s.clone(),
_ => {
return Err(RuntimeError::new(
format!("Expected a database handle, got {db_value:?}"),
line,
column,
));
}
};

let sql_value = self.evaluate_expression(sql, Rc::clone(&env)).await?;
let sql_str = match &sql_value {
Value::Text(s) => s.clone(),
_ => {
return Err(RuntimeError::new(
format!("Expected text for SQL statement, got {sql_value:?}"),
line,
column,
));
}
};

let params = match parameters {
Some(params_expr) => {
let params_value = self
.evaluate_expression(params_expr, Rc::clone(&env))
.await?;
match &params_value {
Value::List(list) => {
let mut sql_params = Vec::new();
for value in list.borrow().iter() {
sql_params.push(
database::value_to_sql_param(value)
.map_err(|e| RuntimeError::new(e, line, column))?,
);
}
sql_params
}
_ => {
return Err(RuntimeError::new(
format!("Expected a list of query parameters, got {params_value:?}"),
line,
column,
));
}
}
}
None => Vec::new(),
};

let pool = self
.io_client
.get_database(&handle)
.await
.map_err(|e| RuntimeError::new(e, line, column))?;

match kind {
crate::parser::ast::DatabaseQueryKind::Query => {
database::run_query(&pool, &sql_str, &params).await
}
crate::parser::ast::DatabaseQueryKind::Execute => {
database::run_execute(&pool, &sql_str, &params).await
}
}
.map_err(|e| RuntimeError::new(e, line, column))
}

async fn evaluate_expression(
&self,
expr: &Expression,
Expand Down Expand Up @@ -7936,6 +7961,25 @@ impl Interpreter {
let is_running = self.io_client.is_process_running(proc_id).await;
Ok(Value::Bool(is_running))
}
Expression::DatabaseQuery {
db,
sql,
parameters,
kind,
line,
column,
} => {
self.evaluate_database_query(
db,
sql,
parameters.as_deref(),
*kind,
*line,
*column,
Rc::clone(&env),
)
.await
}
};
self.assert_invariants();
result
Expand Down
10 changes: 10 additions & 0 deletions src/parser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,16 @@ pub enum Expression {
line: usize,
column: usize,
},
/// Database query/execute in expression position:
/// `return query <db> with <sql> [and parameters <list>]`
DatabaseQuery {
db: Box<Expression>,
sql: Box<Expression>,
parameters: Option<Box<Expression>>,
kind: DatabaseQueryKind,
line: usize,
column: usize,
},
}

#[derive(Debug, Clone, PartialEq)]
Expand Down
8 changes: 8 additions & 0 deletions src/parser/stmt/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use super::super::{Parameter, ParseError, Parser, Statement, Type};
use super::StmtParser;
use super::database::DatabaseParser;
use crate::exec_trace;
use crate::lexer::token::{Token, TokenWithPosition};
use crate::parser::expr::ExprParser;
Expand Down Expand Up @@ -537,9 +538,16 @@ impl<'a> ActionParser<'a> for Parser<'a> {
}

let value = if let Some(token) = self.cursor.peek() {
// Position of the returned expression itself (not the `return`
// keyword), so diagnostics point at the actual operation.
let (value_line, value_column) = (token.line, token.column);
if matches!(&token.token, Token::NothingLiteral) {
self.bump_sync(); // Consume "nothing"
None
} else if let Some(kind) = self.peek_database_query_kind() {
// Database forms: `return query/execute <db> with <sql>
// [and parameters <list>]`, mirroring the `store ... as` value side.
Some(self.parse_database_query_expression(kind, value_line, value_column)?)
} else {
Some(self.parse_expression()?)
}
Expand Down
Loading
Loading