rhai implementation added

This commit is contained in:
filipriec
2025-03-06 19:32:45 +01:00
parent a52e2b05f8
commit 0e0578d158
8 changed files with 290 additions and 1 deletions

146
Cargo.lock generated
View File

@@ -17,6 +17,20 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@@ -181,6 +195,26 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bincode"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad1fa75f77bbd06f187540aa1d70ca50b80b27ce85e7f41c0ce7ff42b34ed3b"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1cef5dd4a4457dd11529e743d18ba4fabbd5f20b6895f4c865cb257337dcf9f"
dependencies = [
"virtue",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.8.0" version = "2.8.0"
@@ -318,6 +352,26 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.15",
"once_cell",
"tiny-keccak",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -398,6 +452,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@@ -1144,6 +1204,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@@ -1404,6 +1473,9 @@ name = "once_cell"
version = "1.20.3" version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "openssl" name = "openssl"
@@ -1574,6 +1646,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@@ -1784,6 +1862,34 @@ version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags",
"instant",
"num-traits",
"once_cell",
"rhai_codegen",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_codegen"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.7" version = "0.9.7"
@@ -1975,11 +2081,13 @@ dependencies = [
name = "server" name = "server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode",
"chrono", "chrono",
"common", "common",
"dotenvy", "dotenvy",
"lazy_static", "lazy_static",
"prost", "prost",
"rhai",
"rstest", "rstest",
"serde", "serde",
"serde_json", "serde_json",
@@ -2077,6 +2185,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.8" version = "0.5.8"
@@ -2401,6 +2520,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "thin-vec"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.11" version = "2.0.11"
@@ -2454,6 +2579,15 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.7.6"
@@ -2769,6 +2903,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unty"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a88342087869553c259588a3ec9ca73ce9b2d538b7051ba5789ff236b6c129"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@@ -2804,6 +2944,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"

View File

@@ -0,0 +1,14 @@
syntax = "proto3";
package multieko2.table_script;
message PostTableScriptRequest {
int64 table_definition_id = 1;
string target_column = 2;
string rhai_script = 3;
}
message TableScriptResponse {
int64 id = 1;
string created_at = 2;
string warnings = 3;
}

View File

@@ -18,6 +18,8 @@ tonic = "0.12.3"
tonic-reflection = "0.12.3" tonic-reflection = "0.12.3"
tracing = "0.1.41" tracing = "0.1.41"
time = { version = "0.3.37", features = ["local-offset"] } time = { version = "0.3.37", features = ["local-offset"] }
rhai = "1.21.0"
bincode = "2.0.0"
[lib] [lib]
name = "server" name = "server"

View File

@@ -4,7 +4,7 @@ CREATE TABLE table_scripts (
table_definitions_id BIGINT NOT NULL REFERENCES table_definitions(id), table_definitions_id BIGINT NOT NULL REFERENCES table_definitions(id),
target_column TEXT NOT NULL, target_column TEXT NOT NULL,
rhai_script TEXT NOT NULL, rhai_script TEXT NOT NULL,
compiled_script BYTEA NOT NULL; compiled_script BYTEA NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(table_definitions_id, target_column) UNIQUE(table_definitions_id, target_column)
); );

View File

@@ -7,6 +7,7 @@ pub mod shared;
pub mod table_structure; pub mod table_structure;
pub mod table_definition; pub mod table_definition;
pub mod tables_data; pub mod tables_data;
pub mod table_script;
// Re-export run_server from the inner server module: // Re-export run_server from the inner server module:
pub use server::run_server; pub use server::run_server;

View File

@@ -0,0 +1,4 @@
// src/table_script/handlers.rs
pub mod post_table_script;
pub use post_table_script::post_table_script;

View File

@@ -0,0 +1,119 @@
// src/table_script/handlers/post_table_script.rs
use tonic::Status;
use sqlx::{PgPool, Error as SqlxError};
use rhai::Engine;
use time::OffsetDateTime;
use bincode::serialize;
use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse};
pub async fn post_table_script(
db_pool: &PgPool,
request: PostTableScriptRequest,
) -> Result<TableScriptResponse, Status> {
// Fetch table definition
let table_def = sqlx::query!(
r#"SELECT table_name, columns, linked_table_id
FROM table_definitions WHERE id = $1"#,
request.table_definition_id
)
.fetch_optional(db_pool)
.await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?
.ok_or_else(|| Status::not_found("Table definition not found"))?;
// Validate target column exists
let columns: Vec<String> = serde_json::from_value(table_def.columns.clone())
.map_err(|e| Status::invalid_argument(format!("Invalid columns format: {}", e)))?;
let column_names: Vec<&str> = columns.iter()
.filter_map(|col| col.split_whitespace().next())
.map(|name| name.trim_matches('"'))
.collect();
if !column_names.contains(&request.target_column.as_str()) {
return Err(Status::invalid_argument("Target column not found in table definition"));
}
// Check for existing data
let row_count: i64 = sqlx::query_scalar(&format!(
"SELECT COUNT(*) FROM {}",
sanitize_identifier(&table_def.table_name)
))
.fetch_one(db_pool)
.await
.map_err(|e| Status::internal(format!("Data check failed: {}", e)))?;
if row_count > 0 {
return Err(Status::failed_precondition(
"Cannot add scripts to tables with existing data"
));
}
// Compile Rhai script
let (ast, warnings) = compile_rhai_script(&request.rhai_script)
.map_err(|e| Status::invalid_argument(format!("Script error: {}", e)))?;
// Store script in database
let script_record = sqlx::query!(
r#"INSERT INTO table_scripts
(table_definitions_id, target_column, rhai_script, compiled_script)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at"#,
request.table_definition_id,
request.target_column,
request.rhai_script,
bincode::serialize(&ast).map_err(|e| Status::internal(format!("Serialization failed: {}", e)))?,
)
.fetch_one(db_pool)
.await
.map_err(|e| match e {
SqlxError::Database(db_err) if db_err.constraint() == Some("table_scripts_table_definitions_id_target_column_key") => {
Status::already_exists("Script already exists for this column")
}
_ => Status::internal(format!("Database error: {}", e)),
})?;
Ok(TableScriptResponse {
id: script_record.id,
created_at: Some(script_record.created_at.into()),
warnings,
})
Ok(TableScriptResponse {
id: script_record.id,
created_at: script_record.created_at.to_string(),
warnings,
})
}
fn sanitize_identifier(s: &str) -> String {
s.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "")
.trim()
.to_lowercase()
}
fn compile_rhai_script(script: &str) -> Result<(AST, String), String> {
let mut engine = Engine::new();
let mut scope = Scope::new();
// Add custom API bindings
engine.register_fn("get_column", |_: &str, _: &str| Ok(()));
engine.register_fn("set_result", |_: &mut Scope, _: rhai::Dynamic| Ok(()));
let ast = engine.compile(script).map_err(|e| e.to_string())?;
// Validate script structure
if !script.contains("set_result") {
return Err("Script must call set_result()".into());
}
let warnings = engine.gen_ast_clear_comments(&ast)
.iter()
.filter_map(|(_, err)| match err {
rhai::ParseError(warn, _) => Some(warn.to_string()),
_ => None
})
.collect::<Vec<_>>()
.join(", ");
Ok((ast, warnings))
}

View File

@@ -0,0 +1,3 @@
// src/tables_data/mod.rs
pub mod handlers;