mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-18 17:14:20 +01:00
printf: this commit adds support for https://github.com/tursodatabase/limbo/issues/885 tracking printf functionality
this commit introduces basic support for printf functionality and doesn't include advanced modifiers like width etc.
This commit is contained in:
@@ -227,7 +227,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
|
|||||||
| min(X,Y,...) | Yes | |
|
| min(X,Y,...) | Yes | |
|
||||||
| nullif(X,Y) | Yes | |
|
| nullif(X,Y) | Yes | |
|
||||||
| octet_length(X) | Yes | |
|
| octet_length(X) | Yes | |
|
||||||
| printf(FORMAT,...) | No | |
|
| printf(FORMAT,...) | Yes | Still need support additional modifiers |
|
||||||
| quote(X) | Yes | |
|
| quote(X) | Yes | |
|
||||||
| random() | Yes | |
|
| random() | Yes | |
|
||||||
| randomblob(N) | Yes | |
|
| randomblob(N) | Yes | |
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ pub enum LimboError {
|
|||||||
InvalidTime(String),
|
InvalidTime(String),
|
||||||
#[error("Modifier parsing error: {0}")]
|
#[error("Modifier parsing error: {0}")]
|
||||||
InvalidModifier(String),
|
InvalidModifier(String),
|
||||||
|
#[error("Invalid argument supplied: {0}")]
|
||||||
|
InvalidArgument(String),
|
||||||
|
#[error("Invalid formatter supplied: {0}")]
|
||||||
|
InvalidFormatter(String),
|
||||||
#[error("Runtime error: {0}")]
|
#[error("Runtime error: {0}")]
|
||||||
Constraint(String),
|
Constraint(String),
|
||||||
#[error("Extension error: {0}")]
|
#[error("Extension error: {0}")]
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ pub enum ScalarFunc {
|
|||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
LoadExtension,
|
LoadExtension,
|
||||||
StrfTime,
|
StrfTime,
|
||||||
|
Printf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ScalarFunc {
|
impl Display for ScalarFunc {
|
||||||
@@ -274,6 +275,7 @@ impl Display for ScalarFunc {
|
|||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
Self::LoadExtension => "load_extension".to_string(),
|
Self::LoadExtension => "load_extension".to_string(),
|
||||||
Self::StrfTime => "strftime".to_string(),
|
Self::StrfTime => "strftime".to_string(),
|
||||||
|
Self::Printf => "printf".to_string(),
|
||||||
};
|
};
|
||||||
write!(f, "{}", str)
|
write!(f, "{}", str)
|
||||||
}
|
}
|
||||||
@@ -572,6 +574,7 @@ impl Func {
|
|||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
"load_extension" => Ok(Self::Scalar(ScalarFunc::LoadExtension)),
|
"load_extension" => Ok(Self::Scalar(ScalarFunc::LoadExtension)),
|
||||||
"strftime" => Ok(Self::Scalar(ScalarFunc::StrfTime)),
|
"strftime" => Ok(Self::Scalar(ScalarFunc::StrfTime)),
|
||||||
|
"printf" => Ok(Self::Scalar(ScalarFunc::Printf)),
|
||||||
_ => crate::bail_parse_error!("no such function: {}", name),
|
_ => crate::bail_parse_error!("no such function: {}", name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1752,6 +1752,14 @@ pub fn translate_expr(
|
|||||||
});
|
});
|
||||||
Ok(target_register)
|
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() {
|
Func::Math(math_func) => match math_func.arity() {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ mod datetime;
|
|||||||
pub mod explain;
|
pub mod explain;
|
||||||
pub mod insn;
|
pub mod insn;
|
||||||
pub mod likeop;
|
pub mod likeop;
|
||||||
|
mod printf;
|
||||||
pub mod sorter;
|
pub mod sorter;
|
||||||
mod strftime;
|
mod strftime;
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ use insn::{
|
|||||||
exec_subtract,
|
exec_subtract,
|
||||||
};
|
};
|
||||||
use likeop::{construct_like_escape_arg, exec_glob, exec_like_with_escape};
|
use likeop::{construct_like_escape_arg, exec_glob, exec_like_with_escape};
|
||||||
|
use printf::exec_printf;
|
||||||
use rand::distributions::{Distribution, Uniform};
|
use rand::distributions::{Distribution, Uniform};
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use regex::{Regex, RegexBuilder};
|
use regex::{Regex, RegexBuilder};
|
||||||
@@ -2108,6 +2110,12 @@ impl Program {
|
|||||||
);
|
);
|
||||||
state.registers[*dest] = result;
|
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 {
|
crate::function::Func::External(f) => match f.func {
|
||||||
ExtFunc::Scalar(f) => {
|
ExtFunc::Scalar(f) => {
|
||||||
|
|||||||
265
core/vdbe/printf.rs
Normal file
265
core/vdbe/printf.rs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,3 +23,4 @@ source $testdir/compare.test
|
|||||||
source $testdir/changes.test
|
source $testdir/changes.test
|
||||||
source $testdir/total-changes.test
|
source $testdir/total-changes.test
|
||||||
source $testdir/offset.test
|
source $testdir/offset.test
|
||||||
|
source $testdir/scalar-functions-printf.test
|
||||||
22
testing/scalar-functions-printf.test
Normal file
22
testing/scalar-functions-printf.test
Normal file
@@ -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}}
|
||||||
Reference in New Issue
Block a user