@@ -311,7 +311,8 @@ fn brief_value(v: *mut Value) -> String {
311311 Value::Float(n) => format!("Float({})", n),
312312 Value::Text(s) => {
313313 if s.len() > 30 {
314− format!("Text(\"{}...\")", &s[..27])
314+ let truncated: String = s.chars().take(27).collect();
315+ format!("Text(\"{}...\")", truncated)
315316 } else {
316317 format!("Text(\"{}\")", s)
317318 }
@@ -1014,7 +1015,7 @@ fn sql_relations_equal(conn: &Connection, a: &[*mut Value], b: &[*mut Value]) ->
10141015
10151016 // Check symmetric difference: (a EXCEPT b) UNION ALL (b EXCEPT a) should be empty
10161017 let sql = format!(
1017− "SELECT 1 FROM (SELECT * FROM {} EXCEPT SELECT * FROM {} UNION ALL SELECT * FROM {} EXCEPT SELECT * FROM {}) LIMIT 1",
1018+ "SELECT 1 FROM (SELECT * FROM {} EXCEPT SELECT * FROM {} UNION ALL (SELECT * FROM {} EXCEPT SELECT * FROM {})) LIMIT 1",
10181019 quote_ident(&t1), quote_ident(&t2), quote_ident(&t2), quote_ident(&t1)
10191020 );
10201021 debug_sql(&sql);
@@ -1472,17 +1473,23 @@ fn values_equal(a: *mut Value, b: *mut Value) -> bool {
14721473 if ra.len() != rb.len() {
14731474 return false;
14741475 }
1475− let set_a: HashSet<Vec<u8>> = ra.iter().map(|r| {
1476+ // Use a multiset (count occurrences) to correctly handle duplicates
1477+ let mut counts: HashMap<Vec<u8>, usize> = HashMap::new();
1478+ for r in ra.iter() {
14761479 let mut buf = Vec::new();
14771480 value_to_hash_bytes(*r, &mut buf);
1478− buf
1479− }).collect();
1481+ *counts.entry(buf).or_insert(0) += 1;
1482+ }
14801483 let mut buf = Vec::new();
1481− rb.iter().all(|r| {
1484+ for r in rb.iter() {
14821485 buf.clear();
14831486 value_to_hash_bytes(*r, &mut buf);
1484− set_a.contains(buf.as_slice())
1485− })
1487+ match counts.get_mut(buf.as_slice()) {
1488+ Some(c) if *c > 0 => *c -= 1,
1489+ _ => return false,
1490+ }
1491+ }
1492+ true
14861493 }
14871494 _ => false,
14881495 }
@@ -2067,6 +2074,34 @@ fn deep_clone_value(val: *mut Value) -> *mut Value {
20672074 Box::into_raw(Box::new(cloned))
20682075}
20692076
2077+/// Recursively free a value tree allocated by `deep_clone_value`.
2078+/// SAFETY: Every node in the tree must have been allocated by `Box::into_raw`.
2079+/// Do NOT call this on arena-allocated values.
2080+unsafe fn deep_drop_value(val: *mut Value) {
2081+ if val.is_null() {
2082+ return;
2083+ }
2084+ unsafe {
2085+ match &*val {
2086+ Value::Record(fields) => {
2087+ for f in fields {
2088+ deep_drop_value(f.value);
2089+ }
2090+ }
2091+ Value::Relation(rows) => {
2092+ for r in rows {
2093+ deep_drop_value(*r);
2094+ }
2095+ }
2096+ Value::Constructor(_, inner) => deep_drop_value(*inner),
2097+ Value::Function(_, env, _) => deep_drop_value(*env),
2098+ Value::IO(_, env) => deep_drop_value(*env),
2099+ _ => {}
2100+ }
2101+ drop(Box::from_raw(val));
2102+ }
2103+}
2104+
20702105/// Fork an IO action onto a new OS thread.
20712106/// Takes an IO value, returns an IO thunk that spawns the thread.
20722107#[unsafe(no_mangle)]
@@ -2089,6 +2124,7 @@ pub extern "C" fn knot_fork_io(io_val: *mut Value) -> *mut Value {
20892124 knot_io_run(db, io);
20902125
20912126 // Close DB and clean up cloned values
2127+ unsafe { deep_drop_value(io); }
20922128 knot_db_close(db);
20932129 });
20942130
@@ -2912,7 +2948,22 @@ fn parse_json_string(s: &str) -> (*mut Value, &str) {
29122948 Some('u') => {
29132949 let hex: String = chars.by_ref().take(4).collect();
29142950 if let Ok(cp) = u32::from_str_radix(&hex, 16) {
2915− if let Some(c) = char::from_u32(cp) {
2951+ if (0xD800..=0xDBFF).contains(&cp) {
2952+ // High surrogate — expect \uXXXX low surrogate
2953+ let mut low_cp = 0u32;
2954+ if chars.next() == Some('\\') && chars.next() == Some('u') {
2955+ let hex2: String = chars.by_ref().take(4).collect();
2956+ if let Ok(lc) = u32::from_str_radix(&hex2, 16) {
2957+ low_cp = lc;
2958+ }
2959+ }
2960+ if (0xDC00..=0xDFFF).contains(&low_cp) {
2961+ let combined = 0x10000 + ((cp - 0xD800) << 10) + (low_cp - 0xDC00);
2962+ if let Some(c) = char::from_u32(combined) {
2963+ result.push(c);
2964+ }
2965+ }
2966+ } else if let Some(c) = char::from_u32(cp) {
29162967 result.push(c);
29172968 }
29182969 }
@@ -3003,6 +3054,7 @@ fn parse_json_obj(s: &str) -> (*mut Value, &str) {
30033054 break;
30043055 }
30053056 }
3057+ fields.sort_by(|a, b| a.name.cmp(&b.name));
30063058 (alloc(Value::Record(fields)), rest)
30073059}
30083060
@@ -3288,7 +3340,7 @@ pub extern "C" fn knot_source_migrate(
32883340
32893341 db_ref
32903342 .conn
3291− .execute_batch("BEGIN IMMEDIATE;")
3343+ .execute_batch("SAVEPOINT knot_migrate;")
32923344 .expect("knot runtime: failed to begin migration transaction");
32933345
32943346 let drop_sql = format!("DROP TABLE IF EXISTS {};", table);
@@ -3376,7 +3428,7 @@ pub extern "C" fn knot_source_migrate(
33763428
33773429 db_ref
33783430 .conn
3379− .execute_batch("COMMIT;")
3431+ .execute_batch("RELEASE SAVEPOINT knot_migrate;")
33803432 .expect("knot runtime: failed to commit migration");
33813433
33823434 eprintln!("Migrated source '{}': {} rows", name, old_rows.len());
@@ -4830,7 +4882,7 @@ pub extern "C" fn knot_source_write(
48304882 // Delete all existing rows and insert new ones in a transaction
48314883 db_ref
48324884 .conn
4833− .execute_batch("BEGIN;")
4885+ .execute_batch("SAVEPOINT knot_full_set;")
48344886 .expect("knot runtime: failed to begin transaction");
48354887
48364888 let table_name = format!("_knot_{}", name);
@@ -4883,7 +4935,7 @@ pub extern "C" fn knot_source_write(
48834935
48844936 db_ref
48854937 .conn
4886− .execute_batch("COMMIT;")
4938+ .execute_batch("RELEASE SAVEPOINT knot_full_set;")
48874939 .expect("knot runtime: failed to commit transaction");
48884940 notify_relation_changed();
48894941}
@@ -4919,7 +4971,7 @@ pub extern "C" fn knot_source_append(
49194971
49204972 db_ref
49214973 .conn
4922− .execute_batch("BEGIN;")
4974+ .execute_batch("SAVEPOINT knot_set;")
49234975 .expect("knot runtime: failed to begin transaction");
49244976
49254977 if is_adt_schema(schema) {
@@ -4959,7 +5011,7 @@ pub extern "C" fn knot_source_append(
49595011
49605012 db_ref
49615013 .conn
4962− .execute_batch("COMMIT;")
5014+ .execute_batch("RELEASE SAVEPOINT knot_set;")
49635015 .expect("knot runtime: failed to commit transaction");
49645016 notify_relation_changed();
49655017}
@@ -4992,7 +5044,7 @@ pub extern "C" fn knot_source_diff_write(
49925044
49935045 db_ref
49945046 .conn
4995− .execute_batch("BEGIN;")
5047+ .execute_batch("SAVEPOINT knot_diff_write;")
49965048 .expect("knot runtime: failed to begin transaction");
49975049
49985050 if is_adt_schema(schema) {
@@ -5206,7 +5258,7 @@ pub extern "C" fn knot_source_diff_write(
52065258
52075259 db_ref
52085260 .conn
5209− .execute_batch("COMMIT;")
5261+ .execute_batch("RELEASE SAVEPOINT knot_diff_write;")
52105262 .expect("knot runtime: failed to commit transaction");
52115263 notify_relation_changed();
52125264}
@@ -5719,9 +5771,13 @@ pub extern "C" fn knot_atomic_commit(db: *mut c_void) {
57195771pub extern "C" fn knot_atomic_rollback(db: *mut c_void) {
57205772 let db_ref = unsafe { &*(db as *mut KnotDb) };
57215773 let depth = db_ref.atomic_depth.get();
5774+ // ROLLBACK TO undoes changes but keeps the savepoint alive.
5775+ // RELEASE then removes it so the next begin creates a clean one.
57225776 db_ref
57235777 .conn
5724− .execute_batch(&format!("ROLLBACK TO SAVEPOINT knot_atomic_{depth};"))
5778+ .execute_batch(&format!(
5779+ "ROLLBACK TO SAVEPOINT knot_atomic_{depth}; RELEASE SAVEPOINT knot_atomic_{depth};"
5780+ ))
57255781 .expect("knot runtime: failed to rollback atomic");
57265782 db_ref.atomic_depth.set(depth - 1);
57275783}
@@ -6108,7 +6164,7 @@ pub extern "C" fn knot_view_write(
61086164
61096165 db_ref
61106166 .conn
6111− .execute_batch("BEGIN;")
6167+ .execute_batch("SAVEPOINT knot_view_write;")
61126168 .expect("knot runtime: view_write begin failed");
61136169
61146170 // 1. Delete rows matching the view's constant filter
@@ -6190,7 +6246,7 @@ pub extern "C" fn knot_view_write(
61906246
61916247 db_ref
61926248 .conn
6193− .execute_batch("COMMIT;")
6249+ .execute_batch("RELEASE SAVEPOINT knot_view_write;")
61946250 .expect("knot runtime: view_write commit failed");
61956251}
61966252
@@ -6469,21 +6525,20 @@ fn parse_query_string(qs: &str) -> HashMap<String, String> {
64696525}
64706526
64716527fn url_decode(s: &str) -> String {
6472− let mut result = String::with_capacity(s.len());
6473− let mut chars = s.bytes();
6474− while let Some(b) = chars.next() {
6528+ let mut bytes = Vec::with_capacity(s.len());
6529+ let mut iter = s.bytes();
6530+ while let Some(b) = iter.next() {
64756531 if b == b'%' {
6476− let h = chars.next().unwrap_or(b'0');
6477− let l = chars.next().unwrap_or(b'0');
6478− let val = hex_val(h) * 16 + hex_val(l);
6479− result.push(val as char);
6532+ let h = iter.next().unwrap_or(b'0');
6533+ let l = iter.next().unwrap_or(b'0');
6534+ bytes.push(hex_val(h) * 16 + hex_val(l));
64806535 } else if b == b'+' {
6481− result.push(' ');
6536+ bytes.push(b' ');
64826537 } else {
6483− result.push(b as char);
6538+ bytes.push(b);
64846539 }
64856540 }
6486− result
6541+ String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
64876542}
64886543
64896544fn hex_val(b: u8) -> u8 {
@@ -6547,7 +6602,15 @@ fn value_to_json(v: *mut Value) -> String {
65476602 match unsafe { as_ref(v) } {
65486603 Value::Int(n) => n.to_string(),
65496604 Value::Float(n) => n.to_string(),
6550− Value::Text(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
6605+ Value::Text(s) => {
6606+ let escaped = s
6607+ .replace('\\', "\\\\")
6608+ .replace('"', "\\\"")
6609+ .replace('\n', "\\n")
6610+ .replace('\r', "\\r")
6611+ .replace('\t', "\\t");
6612+ format!("\"{}\"", escaped)
6613+ }
65516614 Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
65526615 Value::Bytes(b) => {
65536616 format!("\"{}\"", base64_encode(b))
@@ -6556,7 +6619,10 @@ fn value_to_json(v: *mut Value) -> String {
65566619 Value::Record(fields) => {
65576620 let parts: Vec<String> = fields
65586621 .iter()
6559− .map(|f| format!("\"{}\":{}", f.name, value_to_json(f.value)))
6622+ .map(|f| {
6623+ let escaped_name = f.name.replace('\\', "\\\\").replace('"', "\\\"");
6624+ format!("\"{}\":{}", escaped_name, value_to_json(f.value))
6625+ })
65606626 .collect();
65616627 format!("{{{}}}", parts.join(","))
65626628 }
@@ -6566,7 +6632,10 @@ fn value_to_json(v: *mut Value) -> String {
65666632 }
65676633 Value::Constructor(tag, payload) => {
65686634 let p = value_to_json(*payload);
6569− format!("{{\"tag\":\"{}\",\"value\":{}}}", tag, p)
6635+ let escaped_tag = tag
6636+ .replace('\\', "\\\\")
6637+ .replace('"', "\\\"");
6638+ format!("{{\"tag\":\"{}\",\"value\":{}}}", escaped_tag, p)
65706639 }
65716640 Value::Function(_, _, src) => format!("\"<function: {}>\"", src),
65726641 Value::IO(_, _) => "\"<<IO>>\"".to_string(),
@@ -6643,7 +6712,7 @@ pub extern "C" fn knot_http_listen(
66436712 Value::Int(n) => n.to_u16().expect("knot runtime: port number out of range"),
66446713 _ => panic!("knot runtime: listen expects Int port, got {}", type_name(port_val)),
66456714 };
6646− let table = Arc::new(unsafe { std::ptr::read(route_table as *const RouteTable) });
6715+ let table = Arc::new(unsafe { *Box::from_raw(route_table as *mut RouteTable) });
66476716 let addr = format!("0.0.0.0:{}", port);
66486717 let server = Arc::new(tiny_http::Server::http(&addr)
66496718 .unwrap_or_else(|e| panic!("knot runtime: failed to start HTTP server on {}: {}", addr, e)));
@@ -6827,6 +6896,7 @@ pub extern "C" fn knot_http_listen(
68276896 });
68286897 }
68296898
6899+ fields.sort_by(|a, b| a.name.cmp(&b.name));
68306900 let record = alloc(Value::Record(fields));
68316901 let ctor_val = alloc(Value::Constructor(entry_constructor, record));
68326902
@@ -6876,6 +6946,8 @@ pub extern "C" fn knot_http_listen(
68766946 }
68776947
68786948 knot_db_close(db);
6949+ // Free the deep-cloned handler to avoid per-request memory leak
6950+ unsafe { deep_drop_value(handler); }
68796951 });
68806952 }
68816953 None => {
@@ -7163,15 +7235,17 @@ fn fetch_build_url(base: &str, path_pattern: &str, payload: *mut Value) -> Strin
71637235/// Build a JSON body string from a field descriptor and record payload.
71647236fn fetch_build_body(body_desc: &str, payload: *mut Value) -> String {
71657237 let mut json = String::from("{");
7166− for (i, field_desc) in body_desc.split(',').enumerate() {
7238+ let mut first = true;
7239+ for field_desc in body_desc.split(',') {
71677240 if field_desc.is_empty() {
71687241 continue;
71697242 }
71707243 let (name, _ty) = field_desc.split_once(':').unwrap_or((field_desc, "text"));
71717244 let field_val = knot_record_field(payload, name.as_ptr(), name.len());
7172− if i > 0 {
7245+ if !first {
71737246 json.push(',');
71747247 }
7248+ first = false;
71757249 json.push('"');
71767250 json.push_str(name);
71777251 json.push_str("\":");