Skip to content

Commit

Permalink
printf: this commit adds support for tursodatabase#885 tracking print…
Browse files Browse the repository at this point in the history
…f functionality

this commit introduces basic support for printf functionality and doesn't include advanced modifiers like width etc.
  • Loading branch information
redixhumayun committed Feb 4, 2025
1 parent 20d3399 commit 6a863b3
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 1 deletion.
2 changes: 1 addition & 1 deletion COMPAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
| min(X,Y,...) | Yes | |
| nullif(X,Y) | Yes | |
| octet_length(X) | Yes | |
| printf(FORMAT,...) | No | |
| printf(FORMAT,...) | Yes | Still need support additional modifiers |
| quote(X) | Yes | |
| random() | Yes | |
| randomblob(N) | Yes | |
Expand Down
4 changes: 4 additions & 0 deletions core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub enum LimboError {
InvalidTime(String),
#[error("Modifier parsing error: {0}")]
InvalidModifier(String),
#[error("Invalid argument supplied: {0}")]
InvalidArgument(String),
#[error("Invalid formatter supplied: {0}")]
InvalidFormatter(String),
#[error("Runtime error: {0}")]
Constraint(String),
#[error("Extension error: {0}")]
Expand Down
3 changes: 3 additions & 0 deletions core/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ pub enum ScalarFunc {
#[cfg(not(target_family = "wasm"))]
LoadExtension,
StrfTime,
Printf,
}

impl Display for ScalarFunc {
Expand Down Expand Up @@ -274,6 +275,7 @@ impl Display for ScalarFunc {
#[cfg(not(target_family = "wasm"))]
Self::LoadExtension => "load_extension".to_string(),
Self::StrfTime => "strftime".to_string(),
Self::Printf => "printf".to_string(),
};
write!(f, "{}", str)
}
Expand Down Expand Up @@ -572,6 +574,7 @@ impl Func {
#[cfg(not(target_family = "wasm"))]
"load_extension" => Ok(Self::Scalar(ScalarFunc::LoadExtension)),
"strftime" => Ok(Self::Scalar(ScalarFunc::StrfTime)),
"printf" => Ok(Self::Scalar(ScalarFunc::Printf)),
_ => crate::bail_parse_error!("no such function: {}", name),
}
}
Expand Down
8 changes: 8 additions & 0 deletions core/translate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,14 @@ pub fn translate_expr(
});
Ok(target_register)
}
ScalarFunc::Printf => translate_function(
program,
args.as_deref().unwrap_or(&[]),
referenced_tables,
resolver,
target_register,
func_ctx,
),
}
}
Func::Math(math_func) => match math_func.arity() {
Expand Down
8 changes: 8 additions & 0 deletions core/vdbe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod datetime;
pub mod explain;
pub mod insn;
pub mod likeop;
mod printf;
pub mod sorter;
mod strftime;

Expand Down Expand Up @@ -57,6 +58,7 @@ use insn::{
exec_subtract,
};
use likeop::{construct_like_escape_arg, exec_glob, exec_like_with_escape};
use printf::exec_printf;
use rand::distributions::{Distribution, Uniform};
use rand::{thread_rng, Rng};
use regex::{Regex, RegexBuilder};
Expand Down Expand Up @@ -2108,6 +2110,12 @@ impl Program {
);
state.registers[*dest] = result;
}
ScalarFunc::Printf => {
let result = exec_printf(
&state.registers[*start_reg..*start_reg + arg_count],
)?;
state.registers[*dest] = result;
}
},
crate::function::Func::External(f) => match f.func {
ExtFunc::Scalar(f) => {
Expand Down
265 changes: 265 additions & 0 deletions core/vdbe/printf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
use std::rc::Rc;

use crate::types::OwnedValue;
use crate::LimboError;

#[inline(always)]
pub fn exec_printf(values: &[OwnedValue]) -> crate::Result<OwnedValue> {
if values.is_empty() {
return Ok(OwnedValue::Null);
}
let format_str = match &values[0] {
OwnedValue::Text(t) => &t.value,
_ => return Ok(OwnedValue::Null),
};

let mut result = String::new();
let mut args_index = 1;
let mut chars = format_str.chars().peekable();

while let Some(c) = chars.next() {
if c != '%' {
result.push(c);
continue;
}

match chars.next() {
Some('%') => {
result.push('%');
continue;
}
Some('d') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
match &values[args_index] {
OwnedValue::Integer(i) => result.push_str(&i.to_string()),
OwnedValue::Float(f) => result.push_str(&f.to_string()),
_ => result.push_str("0".into()),
}
args_index += 1;
}
Some('s') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
match &values[args_index] {
OwnedValue::Text(t) => result.push_str(&t.value),
OwnedValue::Null => result.push_str("(null)"),
v => result.push_str(&v.to_string()),
}
args_index += 1;
}
Some('f') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
match &values[args_index] {
OwnedValue::Float(f) => result.push_str(&f.to_string()),
OwnedValue::Integer(i) => result.push_str(&(*i as f64).to_string()),
_ => result.push_str("0.0".into()),
}
args_index += 1;
}
None => {
return Err(LimboError::InvalidArgument(
"incomplete format specifier".into(),
))
}
_ => {
return Err(LimboError::InvalidFormatter(
"this formatter is not supported".into(),
));
}
}
}
Ok(OwnedValue::build_text(Rc::new(result)))
}

#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;

fn text(value: &str) -> OwnedValue {
OwnedValue::build_text(Rc::new(value.to_string()))
}

fn integer(value: i64) -> OwnedValue {
OwnedValue::Integer(value)
}

fn float(value: f64) -> OwnedValue {
OwnedValue::Float(value)
}

#[test]
fn test_printf_no_args() {
assert_eq!(exec_printf(&[]).unwrap(), OwnedValue::Null);
}

#[test]
fn test_printf_basic_string() {
assert_eq!(
exec_printf(&[text("Hello World")]).unwrap(),
text("Hello World")
);
}

#[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!"), OwnedValue::Null],
text("Hello, (null)!"),
),
// 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);
}
}

#[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"),
),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), output)
}
}

#[test]
fn test_printf_float_formatting() {
let test_cases = vec![
// Basic float formatting
(vec![text("Number: %f"), float(42.5)], text("Number: 42.5")),
// Negative float
(
vec![text("Number: %f"), float(-42.5)],
text("Number: -42.5"),
),
// Integer as float
(vec![text("Number: %f"), integer(42)], text("Number: 42")),
// Multiple floats
(
vec![text("%f + %f = %f"), float(2.5), float(3.5), float(6.0)],
text("2.5 + 3.5 = 6"),
),
// Non-numeric value defaults to 0.0
(
vec![text("Number: %f"), text("not a number")],
text("Number: 0.0"),
),
];

for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), expected);
}
}

#[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.5%)"),
),
// Complex format
(
vec![
text("Name: %s, ID: %d, Score: %f"),
text("John"),
integer(123),
float(95.5),
],
text("Name: John, ID: 123, Score: 95.5"),
),
];

for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), expected);
}
}

#[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);
}
}
}
1 change: 1 addition & 0 deletions testing/all.test
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ source $testdir/compare.test
source $testdir/changes.test
source $testdir/total-changes.test
source $testdir/offset.test
source $testdir/scalar-functions-printf.test
22 changes: 22 additions & 0 deletions testing/scalar-functions-printf.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env tclsh

set testdir [file dirname $argv0]
source $testdir/tester.tcl

# Basic string formatting
do_execsql_test printf-basic-string {
SELECT printf('Hello World!');
} {{Hello World!}}

do_execsql_test printf-string-replacement {
SELECT printf('Hello, %s', 'Alice');
} {{Hello, Alice}}

do_execsql_test printf-numeric-replacement {
SELECT printf('My number is: %d', 42);
} {{My number is: 42}}

# Multiple consecutive format specifiers
do_execsql_test printf-consecutive-formats {
SELECT printf('%d%s%f', 1, 'test', 2.5);
} {{1test2.500000}}

0 comments on commit 6a863b3

Please sign in to comment.