Skip to content
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

137 changes: 137 additions & 0 deletions packages/sqlite-web-core/src/database_functions/float_negate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use super::*;

// Helper to negate a Rain Float hex string while keeping full precision by
// operating on the binary representation directly.
fn float_negate_hex_to_hex(input_hex: &str) -> Result<String, String> {
let trimmed = input_hex.trim();

if trimmed.is_empty() {
return Err("Empty string is not a valid hex number".to_string());
}

// Parse the input hex into a Float
let float_val =
Float::from_hex(trimmed).map_err(|e| format!("Failed to parse Float hex: {e}"))?;

// Negate the float directly to avoid any formatting or precision loss.
let neg_float = (-float_val).map_err(|e| format!("Failed to negate Float value: {e}"))?;

// Return as hex string
Ok(neg_float.as_hex())
}

// SQLite scalar function wrapper: FLOAT_NEGATE(hex_text)
pub unsafe extern "C" fn float_negate(
context: *mut sqlite3_context,
argc: c_int,
argv: *mut *mut sqlite3_value,
) {
if argc != 1 {
sqlite3_result_error(
context,
c"FLOAT_NEGATE() requires exactly 1 argument".as_ptr(),
-1,
);
return;
}

// Return early for NULL inputs using the documented type check.
if sqlite3_value_type(*argv) == SQLITE_NULL {
sqlite3_result_null(context);
return;
}

// Get the text value (now known to be non-NULL).
let value_ptr = sqlite3_value_text(*argv);

let value_cstr = CStr::from_ptr(value_ptr as *const c_char);
let value_str = match value_cstr.to_str() {
Ok(value_str) => value_str,
Err(_) => {
sqlite3_result_error(context, c"invalid UTF-8".as_ptr(), -1);
return;
}
};

match float_negate_hex_to_hex(value_str) {
Ok(result_hex) => {
if let Ok(result_cstr) = CString::new(result_hex) {
sqlite3_result_text(
context,
result_cstr.as_ptr(),
result_cstr.as_bytes().len() as c_int,
SQLITE_TRANSIENT(),
);
} else {
sqlite3_result_error(context, c"Failed to create result string".as_ptr(), -1);
}
}
Err(e) => match CString::new(e) {
Ok(error_msg) => {
sqlite3_result_error(context, error_msg.as_ptr(), -1);
}
Err(_) => {
sqlite3_result_error(
context,
c"Error message contained interior NUL".as_ptr(),
-1,
);
}
},
}
}

#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_pos_to_neg() {
let pos_hex = Float::parse("1.5".to_string()).unwrap().as_hex();
let expected_neg_hex = Float::parse("-1.5".to_string()).unwrap().as_hex();
let out = float_negate_hex_to_hex(&pos_hex).unwrap();
assert_eq!(out, expected_neg_hex);
}

#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_neg_to_pos() {
let neg_hex = Float::parse("-2.25".to_string()).unwrap().as_hex();
let expected_pos_hex = Float::parse("2.25".to_string()).unwrap().as_hex();
let out = float_negate_hex_to_hex(&neg_hex).unwrap();
assert_eq!(out, expected_pos_hex);
}

#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_zero() {
let zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
let expected_zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
let out = float_negate_hex_to_hex(&zero_hex).unwrap();
assert_eq!(out, expected_zero_hex);
}
Comment on lines +105 to +111
Copy link

@coderabbitai coderabbitai bot Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Add a test for signed zero behavior.

If Float distinguishes -0 and 0, assert expected normalization.

     fn test_float_negate_hex_to_hex_zero() {
         let zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
         let expected_zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
         let out = float_negate_hex_to_hex(&zero_hex).unwrap();
         assert_eq!(out, expected_zero_hex);
     }
+    #[wasm_bindgen_test]
+    fn test_float_negate_hex_to_hex_negative_zero() {
+        let neg_zero_hex = Float::parse("-0".to_string()).unwrap().as_hex();
+        let out = float_negate_hex_to_hex(&neg_zero_hex).unwrap();
+        // Expect normalization to +0; adjust if library preserves -0
+        let zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
+        assert_eq!(out, zero_hex);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_zero() {
let zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
let expected_zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
let out = float_negate_hex_to_hex(&zero_hex).unwrap();
assert_eq!(out, expected_zero_hex);
}
#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_zero() {
let zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
let expected_zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
let out = float_negate_hex_to_hex(&zero_hex).unwrap();
assert_eq!(out, expected_zero_hex);
}
#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_negative_zero() {
let neg_zero_hex = Float::parse("-0".to_string()).unwrap().as_hex();
let out = float_negate_hex_to_hex(&neg_zero_hex).unwrap();
// Expect normalization to +0; adjust if library preserves -0
let zero_hex = Float::parse("0".to_string()).unwrap().as_hex();
assert_eq!(out, zero_hex);
}
🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/database_functions/float_negate.rs around lines
91 to 97, add a test covering signed zero: parse "-0" and "0" to hex, call
float_negate_hex_to_hex on each, and assert the output matches the expected
normalized representation (decide whether negating -0 yields "0" or "-0" based
on Float's normalization and assert that explicitly); ensure the test verifies
both parsing and negation round-trips so signed-zero behavior is asserted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to test this out. out of scope

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@findolor Understood! No problem at all - I'll skip this test suggestion since it's out of scope for this PR. The current test coverage looks good for the main functionality.


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.165Z
Learning: Applies to packages/sqlite-web-core/src/database_functions.rs : Integrate and expose rain.math.float-backed custom database functions for SQLite queries

Learnt from: findolor
PR: rainlanguage/sqlite-web#11
File: svelte-test/tests/database-functions/float-negate.test.ts:105-119
Timestamp: 2025-09-17T17:17:41.817Z
Learning: The toMixedCase function in svelte-test/tests/fixtures/float-utils.ts only modifies characters matching /[a-f]/ (hex digits a-f), preserving the 'x' in '0x' prefixes and all other characters unchanged, so it won't create conflicts with tests that reject '0X' prefixes.

Learnt from: findolor
PR: rainlanguage/sqlite-web#11
File: svelte-test/tests/database-functions/float-negate.test.ts:105-119
Timestamp: 2025-09-17T17:17:41.817Z
Learning: The toMixedCase function in svelte-test/tests/fixtures/float-utils.ts only modifies [a-f] characters, preserving the 'x' in '0x' prefixes, so it won't create conflicts with tests that reject '0X' prefixes.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Float doesn't distinguish between -0 and 0 as far as I remember

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_high_precision() {
let input = "300.123456789012345678";
let in_hex = Float::parse(input.to_string()).unwrap().as_hex();
let expected_hex = Float::parse(format!("-{input}")).unwrap().as_hex();
let out = float_negate_hex_to_hex(&in_hex).unwrap();
assert_eq!(out, expected_hex);
}

#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_whitespace() {
let in_hex = Float::parse("10".to_string()).unwrap().as_hex();
let wrapped = format!(" {in_hex} ");
let expected_hex = Float::parse("-10".to_string()).unwrap().as_hex();
let out = float_negate_hex_to_hex(&wrapped).unwrap();
assert_eq!(out, expected_hex);
}

#[wasm_bindgen_test]
fn test_float_negate_hex_to_hex_invalid() {
assert!(float_negate_hex_to_hex("0XBADHEX").is_err());
assert!(float_negate_hex_to_hex("").is_err());
assert!(float_negate_hex_to_hex("not_hex").is_err());
}
}
31 changes: 19 additions & 12 deletions packages/sqlite-web-core/src/database_functions/float_sum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,18 @@ mod tests {
fn test_float_sum_context_add_hex_without_prefix() {
let mut context = FloatSumContext::new();

assert!(context
.add_value("ffffffff0000000000000000000000000000000000000000000000000000000f")
.is_ok()); // 1.5
let one_point_five = Float::parse("1.5".to_string()).unwrap().as_hex();
let one_point_five_no_prefix = one_point_five.trim_start_matches("0x").to_string();

assert!(context.add_value(&one_point_five_no_prefix).is_ok()); // 1.5
let result_hex = context.get_total_as_hex().unwrap();
let result_decimal = Float::from_hex(&result_hex).unwrap().format().unwrap();
assert_eq!(result_decimal, "1.5");

assert!(context
.add_value("fffffffe000000000000000000000000000000000000000000000000000000e1")
.is_ok()); // 2.25
let two_point_two_five = Float::parse("2.25".to_string()).unwrap().as_hex();
let two_point_two_five_no_prefix = two_point_two_five.trim_start_matches("0x").to_string();

assert!(context.add_value(&two_point_two_five_no_prefix).is_ok()); // 2.25
let result_hex = context.get_total_as_hex().unwrap();
let result_decimal = Float::from_hex(&result_hex).unwrap().format().unwrap();
assert_eq!(result_decimal, "3.75"); // 1.5 + 2.25 = 3.75
Expand All @@ -205,12 +207,17 @@ mod tests {
fn test_float_sum_context_add_uppercase_hex() {
let mut context = FloatSumContext::new();

assert!(context
.add_value("0XFFFFFFFB0000000000000000000000000000000000000000000000000004CB2F")
.is_err()); // Should fail - uppercase 0X not supported
assert!(context
.add_value("0XFFFFFFFF00000000000000000000000000000000000000000000000000000069")
.is_err()); // Should fail - uppercase 0X not supported
let upper_case_bad = Float::parse("-12345.6789".to_string())
.unwrap()
.as_hex()
.replacen("0x", "0X", 1);
assert!(context.add_value(&upper_case_bad).is_err()); // Should fail - uppercase 0X not supported

let another_upper = Float::parse("1024.125".to_string())
.unwrap()
.as_hex()
.replacen("0x", "0X", 1);
assert!(context.add_value(&another_upper).is_err()); // Should fail - uppercase 0X not supported
}

#[wasm_bindgen_test]
Expand Down
24 changes: 22 additions & 2 deletions packages/sqlite-web-core/src/database_functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ use std::str::FromStr;

// Import the individual function modules
mod bigint_sum;
mod float_negate;
mod float_sum;
mod rain_math;

use bigint_sum::*;
use float_negate::*;
use float_sum::*;

pub use rain_math::*;
Expand Down Expand Up @@ -78,6 +80,26 @@ pub fn register_custom_functions(db: *mut sqlite3) -> Result<(), String> {
return Err("Failed to register FLOAT_SUM function".to_string());
}

// Register FLOAT_NEGATE scalar function
let float_negate_name = CString::new("FLOAT_NEGATE").unwrap();
let ret = unsafe {
sqlite3_create_function_v2(
db,
float_negate_name.as_ptr(),
1, // 1 argument
SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS,
std::ptr::null_mut(),
Some(float_negate), // xFunc for scalar
None, // No xStep
None, // No xFinal
None, // No destructor
)
};

if ret != SQLITE_OK {
return Err("Failed to register FLOAT_NEGATE function".to_string());
}

Ok(())
}

Expand All @@ -86,8 +108,6 @@ mod tests {
use super::*;
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn test_cstring_conversion() {
let test_string = "test string with spaces and symbols!@#$%";
Expand Down
8 changes: 4 additions & 4 deletions svelte-test/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion svelte-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@
"type": "module",
"dependencies": {
"@rainlanguage/float": "^0.0.0-alpha.22",
"@rainlanguage/sqlite-web": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.0.tgz"
"@rainlanguage/sqlite-web": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.1.tgz"
}
}
Loading