Skip to content

support format() function#4062

Merged
jussisaurio merged 5 commits intotursodatabase:mainfrom
fahdfady:feat-format-function
Dec 30, 2025
Merged

support format() function#4062
jussisaurio merged 5 commits intotursodatabase:mainfrom
fahdfady:feat-format-function

Conversation

@fahdfady
Copy link
Contributor

resolves #4061
implements the scalar function format() from sqlite https://sqlite.org/lang_corefunc.html#format

turso> select format('%s %d', 'hello world', 120);
┌──────────────────────────────────────┐
│ format ('%s %d', 'hello world', 120) │
├──────────────────────────────────────┤
│ hello world 120                      │
└──────────────────────────────────────┘

@fahdfady
Copy link
Contributor Author

Right now the feature is just a copy-paste from the printf() implementation. I know there is a better way to implement this (since format is just an interface for printf) but i'm not familiar with the codebase yet so i didn't want to mess a lot with the code yet.

@Pavan-Nambi
Copy link
Contributor

Pavan-Nambi commented Nov 30, 2025

if they are same thing, u can just add them as alias. just soemthing like

format | printf => func()

this would be better imo, as we won't have to fix bugs twice..

EDIT: like this

"if" | "iif" => Ok(Self::Scalar(ScalarFunc::Iif)),

also from sqlite's changelog:
https://www.sqlite.org/releaselog/3_38_0.html

Rename the printf() SQL function to format() for better compatibility. The original printf() name is retained as an alias for backwards compatibility.

@fahdfady
Copy link
Contributor Author

@Pavan-Nambi Thanks Pavan! That'll do it i guess?

@jussisaurio
Copy link
Collaborator

Thanks! Could you add some tests?

@fahdfady
Copy link
Contributor Author

fahdfady commented Dec 4, 2025

Thanks! Could you add some tests?

Thanks @jussisaurio! but wouldn't the tests in

#[cfg(test)]
mod tests {
use super::*;
fn text(value: &str) -> Register {
Register::Value(Value::build_text(value.to_string()))
}
fn integer(value: i64) -> Register {
Register::Value(Value::Integer(value))
}
fn float(value: f64) -> Register {
Register::Value(Value::Float(value))
}
#[test]
fn test_printf_no_args() {
assert_eq!(exec_printf(&[]).unwrap(), Value::Null);
}
#[test]
fn test_printf_basic_string() {
assert_eq!(
exec_printf(&[text("Hello World")]).unwrap(),
*text("Hello World").get_value()
);
}
#[test]
fn test_printf_string_formatting() {
let test_cases = vec![
// Simple string substitution
(
vec![text("Hello, %s!"), text("World")],
text("Hello, World!"),
),
// Multiple string substitutions
(
vec![text("%s %s!"), text("Hello"), text("World")],
text("Hello World!"),
),
// String with null value
(
vec![text("Hello, %s!"), Register::Value(Value::Null)],
text("Hello, !"),
),
// String with number conversion
(vec![text("Value: %s"), integer(42)], text("Value: 42")),
// Escaping percent sign
(vec![text("100%% complete")], text("100% complete")),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *output.get_value());
}
}
#[test]
fn test_printf_integer_formatting() {
let test_cases = vec![
// Basic integer formatting
(vec![text("Number: %d"), integer(42)], text("Number: 42")),
// Negative integer
(vec![text("Number: %d"), integer(-42)], text("Number: -42")),
// Multiple integers
(
vec![text("%d + %d = %d"), integer(2), integer(3), integer(5)],
text("2 + 3 = 5"),
),
// Non-numeric value defaults to 0
(
vec![text("Number: %d"), text("not a number")],
text("Number: 0"),
),
(
vec![text("Truncated float: %d"), float(3.9)],
text("Truncated float: 3"),
),
(vec![text("Number: %i"), integer(42)], text("Number: 42")),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *output.get_value())
}
}
#[test]
fn test_printf_unsigned_integer_formatting() {
let test_cases = vec![
// Basic
(vec![text("Number: %u"), integer(42)], text("Number: 42")),
// Multiple numbers
(
vec![text("%u + %u = %u"), integer(2), integer(3), integer(5)],
text("2 + 3 = 5"),
),
// Negative number should be represented as its uint representation
(
vec![text("Negative: %u"), integer(-1)],
text("Negative: 18446744073709551615"),
),
// Non-numeric value defaults to 0
(vec![text("NaN: %u"), text("not a number")], text("NaN: 0")),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *output.get_value())
}
}
#[test]
fn test_printf_float_formatting() {
let test_cases = vec![
// Basic float formatting
(
vec![text("Number: %f"), float(42.5)],
text("Number: 42.500000"),
),
// Negative float
(
vec![text("Number: %f"), float(-42.5)],
text("Number: -42.500000"),
),
// Integer as float
(
vec![text("Number: %f"), integer(42)],
text("Number: 42.000000"),
),
// Multiple floats
(
vec![text("%f + %f = %f"), float(2.5), float(3.5), float(6.0)],
text("2.500000 + 3.500000 = 6.000000"),
),
// Non-numeric value defaults to 0.0
(
vec![text("Number: %f"), text("not a number")],
text("Number: 0.000000"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_character_formatting() {
let test_cases = vec![
// Simple character
(vec![text("character: %c"), text("a")], text("character: a")),
// Character with string
(
vec![text("character: %c"), text("this is a test")],
text("character: t"),
),
// Character with empty
(vec![text("character: %c"), text("")], text("character: ")),
// Character with integer
(
vec![text("character: %c"), integer(123)],
text("character: 1"),
),
// Character with float
(
vec![text("character: %c"), float(42.5)],
text("character: 4"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_exponential_formatting() {
let test_cases = vec![
// Simple number
(
vec![text("Exp: %e"), float(23000000.0)],
text("Exp: 2.300000e+07"),
),
// Negative number
(
vec![text("Exp: %e"), float(-23000000.0)],
text("Exp: -2.300000e+07"),
),
// Non integer float
(
vec![text("Exp: %e"), float(250.375)],
text("Exp: 2.503750e+02"),
),
// Positive, but smaller than zero
(
vec![text("Exp: %e"), float(0.0003235)],
text("Exp: 3.235000e-04"),
),
// Zero
(vec![text("Exp: %e"), float(0.0)], text("Exp: 0.000000e+00")),
// Uppercase "e"
(
vec![text("Exp: %e"), float(0.0003235)],
text("Exp: 3.235000e-04"),
),
// String with integer number
(
vec![text("Exp: %e"), text("123")],
text("Exp: 1.230000e+02"),
),
// String with floating point number
(
vec![text("Exp: %e"), text("123.45")],
text("Exp: 1.234500e+02"),
),
// String with number with leftmost zeroes
(
vec![text("Exp: %e"), text("00123")],
text("Exp: 1.230000e+02"),
),
// String with text
(
vec![text("Exp: %e"), text("test")],
text("Exp: 0.000000e+00"),
),
// String starting with number, but with text on the end
(
vec![text("Exp: %e"), text("123ab")],
text("Exp: 1.230000e+02"),
),
// String starting with text, but with number on the end
(
vec![text("Exp: %e"), text("ab123")],
text("Exp: 0.000000e+00"),
),
// String with exponential representation
(
vec![text("Exp: %e"), text("1.230000e+02")],
text("Exp: 1.230000e+02"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_hexadecimal_formatting() {
let test_cases = vec![
// Simple number
(vec![text("hex: %x"), integer(4)], text("hex: 4")),
// Bigger Number
(
vec![text("hex: %x"), integer(15565303546)],
text("hex: 39fc3aefa"),
),
// Uppercase letters
(
vec![text("hex: %X"), integer(15565303546)],
text("hex: 39FC3AEFA"),
),
// Negative
(
vec![text("hex: %x"), integer(-15565303546)],
text("hex: fffffffc603c5106"),
),
// Float
(vec![text("hex: %x"), float(42.5)], text("hex: 2a")),
// Negative Float
(
vec![text("hex: %x"), float(-42.5)],
text("hex: ffffffffffffffd6"),
),
// Text
(vec![text("hex: %x"), text("42")], text("hex: 2a")),
// Empty Text
(vec![text("hex: %x"), text("")], text("hex: 0")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_octal_formatting() {
let test_cases = vec![
// Simple number
(vec![text("octal: %o"), integer(4)], text("octal: 4")),
// Bigger Number
(
vec![text("octal: %o"), integer(15565303546)],
text("octal: 163760727372"),
),
// Negative
(
vec![text("octal: %o"), integer(-15565303546)],
text("octal: 1777777777614017050406"),
),
// Float
(vec![text("octal: %o"), float(42.5)], text("octal: 52")),
// Negative Float
(
vec![text("octal: %o"), float(-42.5)],
text("octal: 1777777777777777777726"),
),
// Text
(vec![text("octal: %o"), text("42")], text("octal: 52")),
// Empty Text
(vec![text("octal: %o"), text("")], text("octal: 0")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_mixed_formatting() {
let test_cases = vec![
// Mix of string and integer
(
vec![text("%s: %d"), text("Count"), integer(42)],
text("Count: 42"),
),
// Mix of all types
(
vec![
text("%s: %d (%f%%)"),
text("Progress"),
integer(75),
float(75.5),
],
text("Progress: 75 (75.500000%)"),
),
// Complex format
(
vec![
text("Name: %s, ID: %d, Score: %f"),
text("John"),
integer(123),
float(95.5),
],
text("Name: John, ID: 123, Score: 95.500000"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_error_cases() {
let error_cases = vec![
// Not enough arguments
vec![text("%d %d"), integer(42)],
// Invalid format string
vec![text("%z"), integer(42)],
// Incomplete format specifier
vec![text("incomplete %")],
];
for case in error_cases {
assert!(exec_printf(&case).is_err());
}
}
#[test]
fn test_printf_edge_cases() {
let test_cases = vec![
// Empty format string
(vec![text("")], text("")),
// Only percent signs
(vec![text("%%%%")], text("%%")),
// String with no format specifiers
(vec![text("No substitutions")], text("No substitutions")),
// Multiple consecutive format specifiers
(
vec![text("%d%d%d"), integer(1), integer(2), integer(3)],
text("123"),
),
// Format string with special characters
(
vec![text("Special chars: %s"), text("\n\t\r")],
text("Special chars: \n\t\r"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
}
be enough? I just added an alias for the same function.
or do you mean to add TCL tests like those here:
https://github.com/tursodatabase/turso/blob/71715c22619cf132d55a6cfdb526c9b95b160fa3/testing/scalar-functions-printf.test
let me know, and i'm fine with adding some TCL tests it doesn't seem hard :)

@jussisaurio
Copy link
Collaborator

jussisaurio commented Dec 10, 2025

@fahdfady although format just aliases printf some TCL tests would still guard the fact that it continues working as expected. Maybe there's a clean way to make the TCL tests run the same logic against both printf and format with the expectation that they work identically

@fahdfady
Copy link
Contributor Author

something i might need to clarify. SQLite now supports format() function as a rename of printf() .. and printf now is considered an alias for backward compatibility. not the other way around.

Rename the printf() SQL function to format() for better compatibility. The original printf() name is retained as an alias for backwards compatibility.

@Pavan-Nambi already mentioned that before. thanks!

@fahdfady
Copy link
Contributor Author

@jussisaurio a bit late follow-up. I added TCL tests, inspired by the printf function tcl tests, and added some tests at the end to compare the output of format() against printf()
Ran those test & apparently they work

@jussisaurio jussisaurio merged commit d015850 into tursodatabase:main Dec 30, 2025
56 of 58 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

format() function support

3 participants