From b5f6988c8e4d8ea1faae29f73cd9e3317e7e6caa Mon Sep 17 00:00:00 2001 From: HexaField <10372036+HexaField@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:59:38 +1100 Subject: [PATCH 1/2] fix: remove unused SurrealDB scripting feature (~10-20MB savings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the SurrealDB `scripting` feature which embeds a QuickJS JavaScript engine. This saves ~10-20MB RSS and ~2-5MB binary size. Changes: - Remove `scripting` from surrealdb features in Cargo.toml - Remove `.with_scripting(true)` capability configuration - Rewrite fn::contains and fn::regex_match as thin wrappers around native SurrealQL functions (string::contains, string::matches) - Rewrite fn::parse_literal in pure SurrealQL with a fn::url_decode helper for percent-decoding (supports string:, number:, boolean: prefixes) - Remove fn::strip_html (requires regex replace, unavailable in pure SurrealQL) - Remove fn::json_path (requires dynamic object traversal, unavailable in SurrealQL) - Add fn::url_decode helper for common percent-encoded characters Zero production queries call any of these functions — all 29 fn:: references are inside #[cfg(test)] blocks. The literal://json: case (which required JSON.parse via QuickJS) is dropped; json: literals pass through as raw URLs. Tests updated accordingly. --- Cargo.lock | 96 ------ rust-executor/Cargo.toml | 2 +- .../src/perspectives/perspective_instance.rs | 323 ++++++++---------- rust-executor/src/surreal_service/mod.rs | 165 ++++----- 4 files changed, 196 insertions(+), 390 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eeb5118fb..73ead2f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1565,29 +1565,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools 0.11.0", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.114", - "which 4.4.2", -] - [[package]] name = "bindgen" version = "0.70.1" @@ -11907,12 +11884,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lcms2" version = "6.1.1" @@ -16811,12 +16782,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - [[package]] name = "reloadable-core" version = "0.1.0" @@ -17338,54 +17303,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "rquickjs" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5227859c4dfc83f428e58f9569bf439e628c8d139020e7faff437e6f5abaa0" -dependencies = [ - "rquickjs-core", - "rquickjs-macro", -] - -[[package]] -name = "rquickjs-core" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82e0ca83028ad5b533b53b96c395bbaab905a5774de4aaf1004eeacafa3d85d" -dependencies = [ - "async-lock 3.4.2", - "relative-path", - "rquickjs-sys", -] - -[[package]] -name = "rquickjs-macro" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d2eccd988a924a470a76fbd81a191b22d1f5f4f4619cf5662a8c1ab4ca1db7" -dependencies = [ - "convert_case 0.6.0", - "fnv", - "ident_case", - "indexmap 2.11.1", - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "rquickjs-core", - "syn 2.0.114", -] - -[[package]] -name = "rquickjs-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fed0097b0b4fbb2a87f6dd3b995a7c64ca56de30007eb7e867dfdfc78324ba5" -dependencies = [ - "bindgen 0.69.5", - "cc", -] - [[package]] name = "rsa" version = "0.9.10" @@ -19832,7 +19749,6 @@ dependencies = [ "ring 0.17.14", "rmpv", "roaring", - "rquickjs", "rust-stemmers", "rust_decimal", "scrypt", @@ -23776,18 +23692,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "which" version = "6.0.3" diff --git a/rust-executor/Cargo.toml b/rust-executor/Cargo.toml index e56b4fcb5..02ac55d0e 100644 --- a/rust-executor/Cargo.toml +++ b/rust-executor/Cargo.toml @@ -134,7 +134,7 @@ anyhow = "1.0.95" portpicker = "0.1.1" deno_error = "0.5.6" thiserror = "2.0.12" -surrealdb = { version = "2.4", default-features = false, features = ["kv-rocksdb", "kv-mem", "scripting"] } +surrealdb = { version = "2.4", default-features = false, features = ["kv-rocksdb", "kv-mem"] } [dev-dependencies] maplit = "1.0.2" diff --git a/rust-executor/src/perspectives/perspective_instance.rs b/rust-executor/src/perspectives/perspective_instance.rs index 2a998e3bf..9e0f24566 100644 --- a/rust-executor/src/perspectives/perspective_instance.rs +++ b/rust-executor/src/perspectives/perspective_instance.rs @@ -5627,39 +5627,14 @@ GROUP BY source async fn test_literal_parsing_in_surreal_queries() { let mut perspective = setup().await; - println!("\n=== Testing fn::parse_literal() in SurrealDB ==="); - - // Helper function to URL encode for literal URLs - fn url_encode(s: &str) -> String { - s.chars() - .map(|c| match c { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(), - _ => format!("%{:02X}", c as u8), - }) - .collect() - } - - // Create literal://json: URLs with Expression objects (as created by the literal language) - let recipe1_json = r#"{"author":"did:key:test","timestamp":"2025-11-19T10:00:00Z","data":"Pasta Carbonara","proof":{"signature":"abc123"}}"#; - let recipe1_name_literal = format!("literal://json:{}", url_encode(recipe1_json)); - - let recipe2_json = r#"{"author":"did:key:test","timestamp":"2025-11-19T10:00:00Z","data":"Pizza Margherita","proof":{"signature":"def456"}}"#; - let recipe2_name_literal = format!("literal://json:{}", url_encode(recipe2_json)); + println!("\n=== Testing fn::parse_literal() in SurrealDB (pure SurrealQL) ==="); - let recipe3_json = r#"{"author":"did:key:test","timestamp":"2025-11-19T10:00:00Z","data":"Salad","proof":{"signature":"ghi789"}}"#; - let recipe3_name_literal = format!("literal://json:{}", url_encode(recipe3_json)); - - println!("Created literal URLs:"); - println!(" Recipe1: {}", recipe1_name_literal); - println!(" Recipe2: {}", recipe2_name_literal); - println!(" Recipe3: {}", recipe3_name_literal); - - // Add links with literal URLs as targets + // Add links with literal:// URLs as targets (string, number, boolean types) perspective .add_link( Link { - source: "literal://recipe1".to_string(), - target: recipe1_name_literal.clone(), + source: "item://1".to_string(), + target: "literal://string:Pasta%20Carbonara".to_string(), predicate: Some("recipe://name".to_string()), }, LinkStatus::Shared, @@ -5672,8 +5647,8 @@ GROUP BY source perspective .add_link( Link { - source: "literal://recipe2".to_string(), - target: recipe2_name_literal.clone(), + source: "item://2".to_string(), + target: "literal://string:Pizza%20Margherita".to_string(), predicate: Some("recipe://name".to_string()), }, LinkStatus::Shared, @@ -5686,8 +5661,8 @@ GROUP BY source perspective .add_link( Link { - source: "literal://recipe3".to_string(), - target: recipe3_name_literal.clone(), + source: "item://3".to_string(), + target: "literal://string:Salad".to_string(), predicate: Some("recipe://name".to_string()), }, LinkStatus::Shared, @@ -5697,184 +5672,146 @@ GROUP BY source .await .unwrap(); - println!("✓ Added 3 recipe links with literal URLs"); - - // Give SurrealDB time to process - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - - // Test 1: Query without fn::parse_literal() - should match the full literal URL - println!("\n=== Test 1: Query without fn::parse_literal() ==="); - let query_raw = format!( - "SELECT source, target FROM link WHERE predicate = 'recipe://name' AND target = '{}'", - recipe1_name_literal - ); - println!("Query: {}", query_raw); - let results_raw = perspective.surreal_query(query_raw).await.unwrap(); - println!("Results: {} matches", results_raw.len()); - assert_eq!( - results_raw.len(), - 1, - "Should find exactly 1 match with full literal URL" - ); - - // Test 2A: Test if JavaScript functions work at all - println!("\n=== Test 2A: Test if JavaScript works with simple function ==="); - let query_simple_js = "RETURN function() { return 42; }"; - println!("Query: {}", query_simple_js); - let result_simple_js = perspective - .surreal_query(query_simple_js.to_string()) + perspective + .add_link( + Link { + source: "item://1".to_string(), + target: "literal://number:30".to_string(), + predicate: Some("recipe://cook_time".to_string()), + }, + LinkStatus::Shared, + None, + &AgentContext::main_agent(), + ) .await .unwrap(); - println!("Result: {:?}", result_simple_js); - - // Test 2B: Test a debug function that returns arguments - println!("\n=== Test 2B: Test what arguments contains ==="); - let query_debug = "RETURN function() { return arguments.length; }"; - println!("Query: {}", query_debug); - let result_debug = perspective - .surreal_query(query_debug.to_string()) + + perspective + .add_link( + Link { + source: "item://1".to_string(), + target: "literal://boolean:true".to_string(), + predicate: Some("recipe://vegetarian".to_string()), + }, + LinkStatus::Shared, + None, + &AgentContext::main_agent(), + ) .await .unwrap(); - println!("Arguments length: {:?}", result_debug); - - // Test 2C: Test fn::parse_literal() directly - println!("\n=== Test 2C: Test fn::parse_literal() directly ==="); - let test_simple_literal = "literal://string:Hello%20World"; - let query_test_fn = format!("RETURN fn::parse_literal('{}')", test_simple_literal); - println!("Query: {}", query_test_fn); - let result_test_fn = perspective.surreal_query(query_test_fn).await.unwrap(); - println!("Result: {:?}", result_test_fn); - - // Test 3: Now check what fn::parse_literal() returns for our data - println!("\n=== Test 3: Check what fn::parse_literal() returns on link targets ==="); - let query_check = format!( - "SELECT source, target, fn::parse_literal(target) AS parsed_data FROM link WHERE predicate = 'recipe://name'", - ); - println!("Query:\n{}", query_check); - let results_check = perspective.surreal_query(query_check).await.unwrap(); - println!("Results: {} links", results_check.len()); - - for result in &results_check { - let source = result.get("source").and_then(|v| v.as_str()).unwrap_or("?"); - let target = result.get("target").and_then(|v| v.as_str()).unwrap_or("?"); - let parsed = result.get("parsed_data"); - println!(" Source: {}", source); - println!(" Target: {}...", &target[..60.min(target.len())]); - println!(" Parsed: {:?}", parsed); - } - - // Test 4: Try to match using parsed value - println!("\n=== Test 4: Query with fn::parse_literal() to match data value ==="); - let query_parsed = format!( - "SELECT source, target, fn::parse_literal(target) AS parsed_data FROM link WHERE predicate = 'recipe://name' AND fn::parse_literal(target) = 'Pasta Carbonara'", - ); - println!("Query:\n{}", query_parsed); - let results_parsed = perspective.surreal_query(query_parsed).await.unwrap(); - println!("Results: {} matches", results_parsed.len()); - - if results_parsed.len() == 0 { - println!("WARNING: fn::parse_literal() returned 0 results - function may not be working correctly"); - println!("This could be due to:"); - println!(" 1. JavaScript functions not enabled (need --allow-scripting flag)"); - println!(" 2. Function definition syntax error"); - println!(" 3. Closure not capturing $url parameter correctly"); - } - assert_ne!( - results_parsed.len(), - 0, - "fn::parse_literal() should return results for matching data" - ); + println!("✓ Added links with string, number, and boolean literal URLs"); - assert_eq!( - results_parsed.len(), - 1, - "Should find exactly 1 match using fn::parse_literal()" - ); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - let result = &results_parsed[0]; - let source = result.get("source").and_then(|v| v.as_str()).unwrap(); - let parsed_data = result.get("parsed_data").and_then(|v| v.as_str()).unwrap(); + // Test 1: Direct fn::parse_literal() on a string literal + println!("\n=== Test 1: Parse string literal directly ==="); + let result = perspective + .surreal_query("RETURN fn::parse_literal('literal://string:Hello%20World')".to_string()) + .await + .unwrap(); + println!("Result: {:?}", result); - println!(" Source: {}", source); - println!(" Parsed data: {}", parsed_data); + // Test 2: Query and parse string literals from links + println!("\n=== Test 2: Parse string literals from link targets ==="); + let results = perspective + .surreal_query( + "SELECT source, fn::parse_literal(target) AS parsed_name FROM link WHERE predicate = 'recipe://name'" + .to_string(), + ) + .await + .unwrap(); + println!("Results: {} links", results.len()); + assert_eq!(results.len(), 3, "Should find 3 recipe name links"); - assert_eq!(source, "literal://recipe1", "Should find recipe1"); + // Test 3: Filter by parsed value + println!("\n=== Test 3: WHERE clause with fn::parse_literal() ==="); + let results = perspective + .surreal_query( + "SELECT source, fn::parse_literal(target) AS parsed_name FROM link WHERE predicate = 'recipe://name' AND fn::parse_literal(target) = 'Pasta Carbonara'" + .to_string(), + ) + .await + .unwrap(); + assert_eq!(results.len(), 1, "Should find exactly 1 match"); + let source = results[0].get("source").and_then(|v| v.as_str()).unwrap(); + assert_eq!(source, "item://1", "Should find item://1"); + let parsed = results[0] + .get("parsed_name") + .and_then(|v| v.as_str()) + .unwrap(); assert_eq!( - parsed_data, "Pasta Carbonara", - "Should extract 'data' field from JSON" - ); - - // Test 5: Query multiple values with fn::parse_literal() - println!("\n=== Test 5: Query with IN clause using fn::parse_literal() ==="); - let query_multiple = format!( - "SELECT source, fn::parse_literal(target) AS parsed_data FROM link WHERE predicate = 'recipe://name' AND fn::parse_literal(target) IN ['Pasta Carbonara', 'Pizza Margherita']", + parsed, "Pasta Carbonara", + "Should decode URL-encoded string" ); - println!("Query:\n{}", query_multiple); - let results_multiple = perspective.surreal_query(query_multiple).await.unwrap(); - println!("Results: {} matches", results_multiple.len()); + // Test 4: IN clause with parsed values + println!("\n=== Test 4: IN clause with fn::parse_literal() ==="); + let results = perspective + .surreal_query( + "SELECT source, fn::parse_literal(target) AS parsed_name FROM link WHERE predicate = 'recipe://name' AND fn::parse_literal(target) IN ['Pasta Carbonara', 'Pizza Margherita']" + .to_string(), + ) + .await + .unwrap(); assert_eq!( - results_multiple.len(), + results.len(), 2, "Should find exactly 2 matches with IN clause" ); - let names: Vec = results_multiple + let names: Vec = results .iter() .filter_map(|r| { - r.get("parsed_data") + r.get("parsed_name") .and_then(|v| v.as_str()) .map(String::from) }) .collect(); + assert!(names.contains(&"Pasta Carbonara".to_string())); + assert!(names.contains(&"Pizza Margherita".to_string())); + assert!(!names.contains(&"Salad".to_string())); - println!(" Found names: {:?}", names); - assert!( - names.contains(&"Pasta Carbonara".to_string()), - "Should find Pasta Carbonara" - ); - assert!( - names.contains(&"Pizza Margherita".to_string()), - "Should find Pizza Margherita" - ); - assert!( - !names.contains(&"Salad".to_string()), - "Should not find Salad" - ); - - // Test 6: GROUP BY with fn::parse_literal() - this should fail as SurrealDB doesn't support it - println!("\n=== Test 6: GROUP BY with fn::parse_literal() ==="); - let query_group = format!( - "SELECT fn::parse_literal(target), array::group(source) AS sources FROM link WHERE predicate = 'recipe://name' GROUP BY fn::parse_literal(target)", - ); - println!("Query:\n{}", query_group); - let result_group = perspective.surreal_query(query_group).await; + // Test 5: Parse number literal + println!("\n=== Test 5: Parse number literal ==="); + let results = perspective + .surreal_query( + "SELECT fn::parse_literal(target) AS cook_time FROM link WHERE source = 'item://1' AND predicate = 'recipe://cook_time'" + .to_string(), + ) + .await + .unwrap(); + assert_eq!(results.len(), 1, "Should find cook_time link"); + let cook_time = results[0] + .get("cook_time") + .and_then(|v| v.as_i64().or_else(|| v.as_f64().map(|f| f as i64))); + assert_eq!(cook_time, Some(30), "Should parse number literal"); + + // Test 6: Parse boolean literal + println!("\n=== Test 6: Parse boolean literal ==="); + let results = perspective + .surreal_query( + "SELECT fn::parse_literal(target) AS vegetarian FROM link WHERE source = 'item://1' AND predicate = 'recipe://vegetarian'" + .to_string(), + ) + .await + .unwrap(); + assert_eq!(results.len(), 1, "Should find vegetarian link"); + let vegetarian = results[0].get("vegetarian").and_then(|v| v.as_bool()); + assert_eq!(vegetarian, Some(true), "Should parse boolean literal"); - // This should fail - SurrealDB doesn't support grouping by function results - if result_group.is_err() { - println!(" ✓ Query failed as expected - SurrealDB doesn't support GROUP BY fn::function_call()"); - println!(" Error: {}", result_group.unwrap_err()); - } else { - println!( - " WARNING: Query succeeded unexpectedly! SurrealDB may have added this feature." - ); - let results_group = result_group.unwrap(); - println!(" Results: {} groups", results_group.len()); - for group in &results_group { - println!( - " Group object keys: {:?}", - group.as_object().map(|o| o.keys().collect::>()) - ); - } - } + // Test 7: Non-literal URL passes through unchanged + println!("\n=== Test 7: Non-literal URL passthrough ==="); + let results = perspective + .surreal_query("RETURN fn::parse_literal('https://example.com')".to_string()) + .await + .unwrap(); + println!("Passthrough result: {:?}", results); println!("\n=== ✓ SUCCESS ==="); - println!("✓ fn::parse_literal() correctly parses literal://json: URLs"); - println!("✓ Extracted 'data' field from Expression objects"); - println!("✓ WHERE clauses work with parsed values"); - println!("✓ IN clauses work with parsed values"); - println!("Note: GROUP BY fn::function_call() not supported in SurrealDB 2.1"); + println!("✓ fn::parse_literal() correctly parses string, number, boolean literals"); + println!("✓ URL-encoded strings are decoded"); + println!("✓ WHERE and IN clauses work with parsed values"); } #[tokio::test] @@ -7141,11 +7078,12 @@ GROUP BY source // Add a link pointing to a JSON literal (URL encoded) let encoded_json = "%7B%22name%22%3A%22Alice%22%2C%22age%22%3A30%7D"; // {"name":"Alice","age":30} + let json_literal = format!("literal://json:{}", encoded_json); perspective .add_link( Link { source: "user://789".to_string(), - target: format!("literal://json:{}", encoded_json), + target: json_literal.clone(), predicate: Some("ad4m://has_profile".to_string()), }, LinkStatus::Shared, @@ -7157,7 +7095,8 @@ GROUP BY source tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - // Test: Parse JSON literal (should extract .data field) + // Test: fn::parse_literal returns json: literals as-is (JSON parsing removed; + // it required the `scripting` feature's embedded JS engine for JSON.parse). let result = perspective .surreal_query( "SELECT fn::parse_literal(out.uri) AS profile FROM link WHERE in.uri = 'user://789' AND predicate = 'ad4m://has_profile'" @@ -7167,9 +7106,15 @@ GROUP BY source .unwrap(); assert_eq!(result.len(), 1, "Should find 1 result"); - // fn::parse_literal should extract the data from JSON expressions - let profile = result[0].get("profile"); - assert!(profile.is_some(), "Should have parsed JSON literal"); + // json: literals pass through as the raw literal:// URL since JSON parsing + // is not available in pure SurrealQL + let profile = result[0].get("profile").and_then(|v| v.as_str()); + assert!(profile.is_some(), "Should have profile field"); + assert_eq!( + profile.unwrap(), + json_literal, + "json: literals should pass through as raw URL without scripting feature" + ); } #[tokio::test] diff --git a/rust-executor/src/surreal_service/mod.rs b/rust-executor/src/surreal_service/mod.rs index 7eae4b223..5aa4d1e65 100644 --- a/rust-executor/src/surreal_service/mod.rs +++ b/rust-executor/src/surreal_service/mod.rs @@ -7,7 +7,7 @@ use serde_json::Value; use std::sync::Arc; use surrealdb::{ engine::local::{Db, RocksDb}, - opt::{capabilities::Capabilities, Config}, + opt::Config, Surreal, Value as SurrealValue, }; @@ -313,8 +313,7 @@ impl SurrealDBService { database: &str, data_path: Option<&str>, ) -> Result { - // Enable scripting (and any other capabilities you want) - let config = Config::default().capabilities(Capabilities::default().with_scripting(true)); + let config = Config::default(); // Initialize file-based SurrealDB instance with RocksDB // Each perspective gets its own separate database file for isolation @@ -324,7 +323,7 @@ impl SurrealDBService { std::path::Path::new(path).join(format!("surrealdb_perspectives/{}", database)); std::fs::create_dir_all(&db_path)?; - // Try to create RocksDB with config (scripting enabled) + // Try to create RocksDB with config // If we get a lock error, wait briefly and retry (in case previous instance is being cleaned up) let mut retries = 0; let max_retries = 3; @@ -402,117 +401,75 @@ impl SurrealDBService { -- This prevents race conditions during concurrent link creation DEFINE INDEX IF NOT EXISTS link_unique_idx ON link FIELDS in, out, predicate, author, timestamp UNIQUE; - DEFINE FUNCTION IF NOT EXISTS fn::parse_literal($url: option) { - RETURN function($url) { - const [url] = arguments; - - if (!url || typeof url !== 'string') { - return url; - } - - if (!url.startsWith('literal://')) { - return url; - } - - const body = url.substring(10); - - if (body.startsWith('string:')) { - return decodeURIComponent(body.substring(7)); - } - - if (body.startsWith('number:')) { - return parseFloat(body.substring(7)); - } - - if (body.startsWith('boolean:')) { - return body.substring(8) === 'true'; - } - - if (body.startsWith('json:')) { - try { - const json = decodeURIComponent(body.substring(5)); - const parsed = JSON.parse(json); - if (parsed.data !== undefined) { - return parsed.data; - } - return parsed; - } catch (e) { - return url; - } - } - - return url; - }; + -- Pure SurrealQL helper: basic percent-decoding of common URL-encoded characters + DEFINE FUNCTION IF NOT EXISTS fn::url_decode($str: option) { + IF $str IS NONE { RETURN NONE; }; + LET $r = string::replace($str, '%20', ' '); + LET $r = string::replace($r, '%21', '!'); + LET $r = string::replace($r, '%22', '\"'); + LET $r = string::replace($r, '%23', '#'); + LET $r = string::replace($r, '%24', '$'); + LET $r = string::replace($r, '%26', '&'); + LET $r = string::replace($r, '%27', \"'\"); + LET $r = string::replace($r, '%28', '('); + LET $r = string::replace($r, '%29', ')'); + LET $r = string::replace($r, '%2B', '+'); + LET $r = string::replace($r, '%2C', ','); + LET $r = string::replace($r, '%2F', '/'); + LET $r = string::replace($r, '%3A', ':'); + LET $r = string::replace($r, '%3B', ';'); + LET $r = string::replace($r, '%3D', '='); + LET $r = string::replace($r, '%3F', '?'); + LET $r = string::replace($r, '%40', '@'); + LET $r = string::replace($r, '%5B', '['); + LET $r = string::replace($r, '%5D', ']'); + LET $r = string::replace($r, '%7B', '{'); + LET $r = string::replace($r, '%7D', '}'); + -- Decode %25 (literal percent) last to avoid double-decoding + LET $r = string::replace($r, '%25', '%'); + RETURN $r; }; - DEFINE FUNCTION IF NOT EXISTS fn::strip_html($html: option) { - RETURN function($html) { - const [html] = arguments; - - if (!html || typeof html !== 'string') { - return html; - } - - // Remove HTML tags using regex - return html.replace(/<[^>]*>/g, ''); + -- Parse literal:// URLs into native SurrealQL values. + -- Supports string:, number:, and boolean: prefixes. + -- Note: literal://json: support was removed — it required an embedded JS engine + -- (QuickJS via SurrealDB's `scripting` feature) for decodeURIComponent + JSON.parse. + -- No production queries call this function; json: literals can be pre-processed + -- on the Rust side before insertion if needed in the future. + DEFINE FUNCTION IF NOT EXISTS fn::parse_literal($url: option) { + IF $url IS NONE { RETURN NONE; }; + IF !string::starts_with($url, 'literal://') { RETURN $url; }; + LET $body = string::slice($url, 10); + IF string::starts_with($body, 'string:') { + RETURN fn::url_decode(string::slice($body, 7)); }; - }; - - DEFINE FUNCTION IF NOT EXISTS fn::json_path($obj: option, $path: option) { - RETURN function($obj, $path) { - const [obj, path] = arguments; - - if (!obj || !path || typeof path !== 'string') { - return null; - } - - // Split path by dots and traverse object - const parts = path.split('.'); - let current = obj; - - for (const part of parts) { - if (current && typeof current === 'object' && part in current) { - current = current[part]; - } else { - return null; - } - } - - return current; + IF string::starts_with($body, 'number:') { + RETURN type::number(string::slice($body, 7)); }; + IF string::starts_with($body, 'boolean:') { + RETURN string::slice($body, 8) = 'true'; + }; + RETURN $url; }; - DEFINE FUNCTION IF NOT EXISTS fn::contains($str: option, $substring: option) { - RETURN function($str, $substring) { - const [str, substring] = arguments; - //console.log('🔍 fn::contains input - str:', str, 'substring:', substring); + -- fn::strip_html was removed: regex-based tag stripping (/<[^>]*>/g) cannot be + -- replicated in pure SurrealQL (no regex replace). No production queries call it. + -- If needed, pre-process HTML stripping on the Rust side before insertion. - if (!str || !substring || typeof str !== 'string' || typeof substring !== 'string') { - //console.log('🔍 fn::contains: invalid types, returning false'); - return false; - } + -- fn::json_path was removed: dynamic dot-path object traversal requires runtime + -- string splitting and property access not available in pure SurrealQL. + -- No production queries call it. Use native SurrealQL field access (obj.field) instead. - const result = str.includes(substring); - //console.log('🔍 fn::contains result:', result); - return result; - }; + -- String containment check — thin wrapper around native string::contains + DEFINE FUNCTION IF NOT EXISTS fn::contains($str: option, $substring: option) { + IF $str IS NONE OR $substring IS NONE { RETURN false; }; + RETURN string::contains($str, $substring); }; + -- Regex match — thin wrapper around native string::matches DEFINE FUNCTION IF NOT EXISTS fn::regex_match($str: option, $pattern: option) { - RETURN function($str, $pattern) { - const [str, pattern] = arguments; - - if (!str || !pattern || typeof str !== 'string' || typeof pattern !== 'string') { - return false; - } - - try { - const regex = new RegExp(pattern); - return regex.test(str); - } catch (e) { - return false; - } - }; + IF $str IS NONE OR $pattern IS NONE { RETURN false; }; + RETURN string::matches($str, $pattern); }; ", ) From b2cf70fa444d23aa5985b7239ebb6179213695cd Mon Sep 17 00:00:00 2001 From: HexaField <10372036+HexaField@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:00:51 +1100 Subject: [PATCH 2/2] ci: add exploration CI workflow --- .github/workflows/exploration-ci.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/exploration-ci.yml diff --git a/.github/workflows/exploration-ci.yml b/.github/workflows/exploration-ci.yml new file mode 100644 index 000000000..8398bc33c --- /dev/null +++ b/.github/workflows/exploration-ci.yml @@ -0,0 +1,31 @@ +name: Exploration CI + +on: + push: + branches: + - feat/wasm-language-runtime + - feat/sqlite-link-storage + - fix/remove-surrealdb-scripting + pull_request: + branches: [dev] + +jobs: + cargo-check: + name: Cargo Check + runs-on: ubuntu-22.04 + container: + image: coasys/ad4m-ci-linux:latest@sha256:3d6e8b6357224d689345eebd5f9da49ee5fd494b3fd976273d0cf5528f6903ab + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust-executor + cache-on-failure: true + - name: Cargo check + run: cd rust-executor && cargo check 2>&1 + - name: Cargo test (surreal_service) + run: cd rust-executor && cargo test surreal_service -- --nocapture 2>&1 + - name: Cargo test (perspective parse_literal) + run: cd rust-executor && cargo test test_literal_parsing test_docs_parse_literal -- --nocapture 2>&1