Replies: 6 comments 20 replies
-
From your requirements, it appears that the best way is to do it via a custom syntax. Custom syntax in Rhai extends Rhai's parser to user code. Since SQL is basically a stream of whitespace-delimited tokens, and the tokens are not dissimilar to Rhai's, you can simply reassemble the SQL statement from the Rhai tokens received. The only problem I can think of are strings... since SQL needs You can simply make the keywords With a custom syntax, you can simply do: let id = 1;
let name = "data";
try {
// Interpolations can hold any arbitrary block of Rhai code!
let n = INSERT INTO my_table (id, name) VALUES (${id}, ${prettify_string(name)});
if n < 1 { throw "No rows added!"; }
print(`${n} row(s) added.`);
} catch (ex) {
....
}
let name = SELECT name FROM my_table WHERE id = ${id};
if name != () {
print("FOUND!");
} else {
print("Somehow the record is still not there...");
} The strings issue we need a solution though... |
Beta Was this translation helpful? Give feedback.
-
Here's some quick pseudo-code, is this directionally correct? I know it doesn't handle the string interpolation to bind parameters translation since I couldn't figure that out. I wasn't sure if I should be using use rhai::{Engine, Dynamic, EvalAltResult, Scope};
use rusqlite::{params, Connection};
// Execute SELECT SQL queries
fn execute_sql_select(start_token: &str, sql: &str, conn: &Connection) -> Result<Dynamic, Box<EvalAltResult>> {
let sql_query = format!("{} {}", start_token, sql);
let mut stmt = conn.prepare(&sql_query).map_err(|e| e.to_string())?;
let mut rows = stmt.query(params![]).map_err(|e| e.to_string())?;
let mut result = Vec::new();
while let Some(row) = rows.next().map_err(|e| e.to_string())? {
let value: String = row.get(0).map_err(|e| e.to_string())?;
result.push(value);
}
Ok(result.into())
}
// Execute INSERT, UPDATE, DELETE SQL queries
fn execute_sql_dml(start_token: &str, sql: &str, conn: &Connection) -> Result<Dynamic, Box<EvalAltResult>> {
let sql_query = format!("{} {}", start_token, sql);
let mut stmt = conn.prepare(&sql_query).map_err(|e| e.to_string())?;
let affected_rows = stmt.execute(params![]).map_err(|e| e.to_string())?;
Ok(affected_rows.into())
}
// Register custom SQL syntax
fn register_sql_syntax(engine: &mut Engine, conn: &Connection) {
engine.register_custom_syntax(
[ "SELECT", "$string$", ";" ], true,
move |_, inputs| {
let sql = inputs[0].get_string_value().unwrap();
execute_sql_select("SELECT", &sql, conn)
}
);
engine.register_custom_syntax(
[ "INSERT INTO", "$string$", ";" ], true,
move |_, inputs| {
let sql = inputs[0].get_string_value().unwrap();
execute_sql_dml("INSERT INTO", &sql, conn)
}
);
engine.register_custom_syntax(
[ "UPDATE", "$string$", ";" ], true,
move |_, inputs| {
let sql = inputs[0].get_string_value().unwrap();
execute_sql_dml("UPDATE", &sql, conn)
}
);
engine.register_custom_syntax(
[ "DELETE", "$string$", ";" ], true,
move |_, inputs| {
let sql = inputs[0].get_string_value().unwrap();
execute_sql_dml("DELETE", &sql, conn)
}
);
}
fn main() {
let conn = Connection::open_in_memory().unwrap();
// Create a demo table
conn.execute("CREATE TABLE my_table (id INTEGER, name TEXT)", params![]).unwrap();
let mut engine = Engine::new();
let mut scope = Scope::new();
// Register SQL syntax
register_sql_syntax(&mut engine, &conn);
let script = r#"
let id = 1;
let name = "data";
// Insert statement
INSERT INTO my_table (id, name) VALUES (${id}, ${name});
// Select statement
let result = SELECT name FROM my_table WHERE id = ${id};
if result != () {
print("FOUND!");
} else {
print("Somehow the record is still not there...");
}
"#;
let ast = engine.compile(&script).unwrap();
engine.eval_ast_with_scope(&mut scope, &ast).unwrap();
} |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
I would not suggest to use I suggest you use https://rhai.rs/book/engine/custom-syntax-parsers.html Basically, you'd want to look at each token, store the SQL statement up to that point in a string inside the custom state. The SQL statement is simply the text representation of each token, translated into SQL lingo, concat'ed and delimited by spaces. Then for the next token, see if it is If the next token is For example, you see the following tokens: SELECT MAX ( hello ) FROM table WHERE id = $ { record.id }; You string the token texts up to Then you return Push Once you reach Beware that, this way, it is impossible to treat the SQL statement as an expression. It can only be a statement. You cannot do: let x = SELECT MAX(hello) FROM table + 1; But you can do: let x = SELECT MAX(hello) FROM table;
x += 1; That is because I cannot see a way, short of writing an SQL parser, to determine whether the SQL statement is already complete. For example, there will be this ambiguity: let x = SELECT MAX(hello) FROM table WHERE count > ${limit} + 10; Does the |
Beta Was this translation helpful? Give feedback.
-
Try this: #[test]
fn test_custom_syntax_raw_sql() {
let mut engine = Engine::new();
engine.register_custom_syntax_with_state_raw(
"SELECT",
|symbols, lookahead, state| {
// Build a SQL statement as the state
let mut sql: String = if state.is_unit() { Default::default() } else { state.take().cast::<ImmutableString>().into() };
// At every iteration, the last symbol is the new one
let r = match symbols.last().unwrap().as_str() {
// Terminate parsing when we see `;`
";" => None,
// Variable substitution -- parse the following as a block
"$" => Some("$block$".into()),
// Block parsed, replace it with `?` as SQL parameter
"$block$" => {
if !sql.is_empty() {
sql.push(' ');
}
sql.push('?');
Some(lookahead.into()) // Always accept the next token
}
// Otherwise simply concat the tokens
_ => {
if !sql.is_empty() {
sql.push(' ');
}
sql.push_str(symbols.last().unwrap().as_str());
Some(lookahead.into()) // Always accept the next token
}
};
// SQL statement done!
*state = sql.into();
match lookahead {
// End of script?
"{EOF}" => Ok(None),
_ => Ok(r),
}
},
false,
|context, inputs, state| {
// Our SQL statement
let sql = state.as_immutable_string_ref().unwrap();
let mut output = sql.to_string();
// Inputs will be parameters
for input in inputs {
let value = context.eval_expression_tree(input).unwrap();
output.push('\n');
output.push_str(&value.to_string());
}
Ok(output.into())
},
);
let mut scope = Scope::new();
scope.push("id", 123 as INT);
assert_eq!(
engine.eval_with_scope::<String>(&mut scope, "SELECT * FROM table WHERE id = ${id}").unwrap(),
"SELECT * FROM table WHERE id = ?\n123"
);
} Sorry for taking time. |
Beta Was this translation helpful? Give feedback.
-
Some suggestions on flexible syntax using a new lead character e.g. // If `table_expr` is a string, then it is the table name. Return data is an array of object maps
let x = SELECT * FROM @{table_expr} WHERE id = ${id};
// Return data is a single value, or () if NULL
let x = SELECT MAX(timestamp) FROM table;
// If `fields` is an array of strings, it is the fields list. Return data is an array of object maps
// If `fields` is a single string, it is the target field. Return data is an array of values
let x = SELECT @{fields} FROM table WHERE id = ${id};
// If `field` is a string, then it is a field name. Return data is an array of values
let x = SELECT name FROM table WHERE @{field} = ${criteria};
// If `criteria` is an object map, it is a list of `key` = `value` WHERE clauses AND'ed together. Return data is an array of values
let x = SELECT name FROM table WHERE @{criteria};
// If `keys` is an array of strings, it is the list of fields. If `keys` is an object map, the keys are the fields list.
// Return data is an array of object maps
let x = SELECT @{keys}, SUM(value) FROM table GROUP BY @{keys} ORDER BY @{keys} In order to do this, you'll have to insert markers into the prepared SQL statement, and those markers will later be replaced by the actual field names to form the actual prepared SQL statement. So SELECT @{fields} FROM table WHERE id = ${id} may become
And the Finally, the expression values that correspond to markers will be removed, leaving only parameters that can be passed directly to the now-complete SQL statement. |
Beta Was this translation helpful? Give feedback.
-
We're looking to integrate SQL functionality into Rhai with minimal changes to the existing language, aiming to embed SQL expressions and DML statements in a more natural way, without requiring function prefixes or additional syntax changes.
Our Desired Enhancements
SQL Expression Returning a Matrix of Rows:
We want to implement an expression similar to JavaScript's string literal, where the following will return a matrix of rows (or zero rows if no results are found):
SQL Expression Returning a Single Value:
Similar to the above, but returning a single value or
undefined
/null
if not found:SQL DML Statements:
We want to write SQL DML statements like
INSERT
,UPDATE
, andDELETE
directly in the Rhai scripts, terminated with a semicolon, and have them execute using Rusqlite. For example:Each of these statements would be passed as full SQL text to Rusqlite for execution. We are not trying to make Rhai into a SQL-like language, just wanted to find an easy way to use embedded SQL to pass to Rusqlite when SQL is encountered as part of Rhai.
We understand we can define custom functions like
sql_execute
and call those in Rhai but we're looking to make it easier on our users who already know SQL.Current Roadblocks
SQL
andSQLv
string literal style expressions, we’d like to understand the best way to implement a custom handler for JavaScript-like backtick syntax, where the SQL text is processed and passed directly to Rusqlite whenSQL
orSQLv
is encountered.Additional Request: Variable Interpolation
Additionally, we want to know how we can pass Rhai variables into the SQL expressions and DML statements using JavaScript-style
${x}
interpolation for parameters. For example:Is There a Recommended Approach?
INSERT
,UPDATE
,DELETE
) as native statements that Rhai will process and then execute via Rusqlite?SQL
andSQLv
to handle backtick-delimited SQL queries in Rhai, while maintaining the simplicity and natural flow of the language?${x}
style variable interpolation for SQL statements, passing Rhai variables into SQL queries and DML statements?Any guidance on the best way to implement this within Rhai's framework would be greatly appreciated!
Beta Was this translation helpful? Give feedback.
All reactions