Merge 'sql_generation: Fix implementation of LTValue and GTValue for Text types' from Jussi Saurio

## Background
Simulator wants to create predicates that it knows will be Greater or
Less than some known value. It uses `LTValue` and `GTValue` for
generating these.
## Problem
Current implementation simply decrements or increments a random char by
1, and can thus generate strings with control characters like null
terminators that result in parse errors, as seen in e.g. this CI run htt
ps://github.com/tursodatabase/turso/actions/runs/18459131141/job/5258630
5749?pr=3702 of PR #3702
EDIT: I realized the _actual_ problem is in `GTValue` when it decides to
make the string longer, it uses a random char value from `0..255` which
can include null terminators etc. Fixed that too. I think in general
this PR's approach is a bit more predictable so let's keep it.
## Solution
Restrict string mutations to ascii string characters so that the
mutation always results in another ascii string character.

Closes #3708
This commit is contained in:
Jussi Saurio
2025-10-15 09:25:17 +03:00
committed by GitHub

View File

@@ -23,18 +23,7 @@ impl ArbitraryFrom<(&SimValue, ColumnType)> for LTValue {
t.pop();
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.random_range(0..t.len());
t[index] -= 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
.map(|c| char::from_u32(c).unwrap_or('z'))
.collect::<String>();
Value::build_text(t)
Value::build_text(mutate_string(&t, rng, MutationType::Decrement))
}
}
Value::Blob(b) => {
@@ -75,21 +64,14 @@ impl ArbitraryFrom<(&SimValue, ColumnType)> for GTValue {
// Either lengthen the string, or make at least one character smaller and mutate the rest
let mut t = value.to_string();
if rng.random_bool(0.01) {
t.push(rng.random_range(0..=255) as u8 as char);
if rng.random_bool(0.5) {
t.push(rng.random_range(UPPERCASE_A..=UPPERCASE_Z) as u8 as char);
} else {
t.push(rng.random_range(LOWERCASE_A..=LOWERCASE_Z) as u8 as char);
}
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.random_range(0..t.len());
t[index] += 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
.map(|c| char::from_u32(c).unwrap_or('a'))
.collect::<String>();
Value::build_text(t)
Value::build_text(mutate_string(&t, rng, MutationType::Increment))
}
}
Value::Blob(b) => {
@@ -116,3 +98,86 @@ impl ArbitraryFrom<(&SimValue, ColumnType)> for GTValue {
Self(SimValue(new_value))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum MutationType {
Decrement,
Increment,
}
const UPPERCASE_A: u32 = 'A' as u32;
const UPPERCASE_Z: u32 = 'Z' as u32;
const LOWERCASE_A: u32 = 'a' as u32;
const LOWERCASE_Z: u32 = 'z' as u32;
fn mutate_string<R: rand::Rng + ?Sized>(
t: &str,
rng: &mut R,
mutation_type: MutationType,
) -> String {
let mut chars = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let mut index;
let mut max_loops = 100;
loop {
index = rng.random_range(0..chars.len());
if chars[index] > UPPERCASE_A && chars[index] < UPPERCASE_Z
|| chars[index] > LOWERCASE_A && chars[index] < LOWERCASE_Z
{
break;
}
max_loops -= 1;
if max_loops == 0 {
panic!("Failed to find a printable character to decrement");
}
}
if mutation_type == MutationType::Decrement {
chars[index] -= 1;
} else {
chars[index] += 1;
}
// Mutate the rest of the string with printable ASCII characters
for val in chars.iter_mut().skip(index + 1) {
if rng.random_bool(0.5) {
*val = rng.random_range(UPPERCASE_A..=UPPERCASE_Z);
} else {
*val = rng.random_range(LOWERCASE_A..=LOWERCASE_Z);
}
}
chars
.into_iter()
.map(|c| char::from_u32(c).unwrap())
.collect::<String>()
}
#[cfg(test)]
mod tests {
use anarchist_readable_name_generator_lib::readable_name;
use super::*;
#[test]
fn test_mutate_string_fuzz() {
let mut rng = rand::rng();
for _ in 0..1000 {
let mut t = readable_name();
while !t.is_ascii() {
t = readable_name();
}
let t2 = mutate_string(&t, &mut rng, MutationType::Decrement);
assert!(t2.is_ascii(), "{}", t);
assert!(t2 < t);
}
for _ in 0..1000 {
let mut t = readable_name();
while !t.is_ascii() {
t = readable_name();
}
let t2 = mutate_string(&t, &mut rng, MutationType::Increment);
assert!(t2.is_ascii(), "{}", t);
assert!(t2 > t);
}
}
}