mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-19 06:55:18 +01:00
feature: implement strftime function
This commit is contained in:
24
COMPAT.md
24
COMPAT.md
@@ -4,16 +4,24 @@ This document describes the compatibility of Limbo with SQLite.
|
||||
|
||||
## Table of contents:
|
||||
|
||||
- [Features](#features)
|
||||
- [SQLite query language](#sqlite-query-language)
|
||||
- [Compatibility with SQLite](#compatibility-with-sqlite)
|
||||
- [Table of contents:](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [SQLite query language](#sqlite-query-language)
|
||||
- [Statements](#statements)
|
||||
- [PRAGMA Statements](#pragma)
|
||||
- [PRAGMA](#pragma)
|
||||
- [Expressions](#expressions)
|
||||
- [Functions](#functions)
|
||||
- [SQLite C API](#sqlite-c-api)
|
||||
- [SQLite VDBE opcodes](#sqlite-vdbe-opcodes)
|
||||
- [Extensions](#extensions)
|
||||
- [SQL functions](#sql-functions)
|
||||
- [Scalar functions](#scalar-functions)
|
||||
- [Mathematical functions](#mathematical-functions)
|
||||
- [Aggregate functions](#aggregate-functions)
|
||||
- [Date and time functions](#date-and-time-functions)
|
||||
- [JSON functions](#json-functions)
|
||||
- [SQLite C API](#sqlite-c-api)
|
||||
- [SQLite VDBE opcodes](#sqlite-vdbe-opcodes)
|
||||
- [Extensions](#extensions)
|
||||
- [UUID](#uuid)
|
||||
- [regexp](#regexp)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -308,7 +316,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
|
||||
| datetime() | Yes | partially supports modifiers |
|
||||
| julianday() | Partial | does not support modifiers |
|
||||
| unixepoch() | Partial | does not support modifiers |
|
||||
| strftime() | No | |
|
||||
| strftime() | Yes | partially supports modifiers |
|
||||
| timediff() | No | |
|
||||
|
||||
Modifiers:
|
||||
|
||||
@@ -213,6 +213,7 @@ pub enum ScalarFunc {
|
||||
Replace,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
LoadExtension,
|
||||
StrfTime,
|
||||
}
|
||||
|
||||
impl Display for ScalarFunc {
|
||||
@@ -264,6 +265,7 @@ impl Display for ScalarFunc {
|
||||
Self::DateTime => "datetime".to_string(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
Self::LoadExtension => "load_extension".to_string(),
|
||||
Self::StrfTime => "strftime".to_string(),
|
||||
};
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
@@ -554,6 +556,7 @@ impl Func {
|
||||
"trunc" => Ok(Self::Math(MathFunc::Trunc)),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
"load_extension" => Ok(Self::Scalar(ScalarFunc::LoadExtension)),
|
||||
"strftime" => Ok(Self::Scalar(ScalarFunc::StrfTime)),
|
||||
_ => crate::bail_parse_error!("no such function: {}", name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1517,6 +1517,26 @@ pub fn translate_expr(
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::StrfTime => {
|
||||
if let Some(args) = args {
|
||||
for arg in args.iter() {
|
||||
// register containing result of each argument expression
|
||||
let _ = translate_and_mark(
|
||||
program,
|
||||
referenced_tables,
|
||||
arg,
|
||||
resolver,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
program.emit_insn(Insn::Function {
|
||||
constant_mask: 0,
|
||||
start_reg: target_register + 1,
|
||||
dest: target_register,
|
||||
func: func_ctx,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
}
|
||||
}
|
||||
Func::Math(math_func) => match math_func.arity() {
|
||||
|
||||
@@ -22,24 +22,44 @@ pub fn exec_datetime_full(values: &[OwnedValue]) -> OwnedValue {
|
||||
exec_datetime(values, DateTimeOutput::DateTime)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn exec_strftime(values: &[OwnedValue]) -> OwnedValue {
|
||||
if values.is_empty() {
|
||||
return OwnedValue::Null;
|
||||
}
|
||||
|
||||
let format_str = match &values[0] {
|
||||
OwnedValue::Text(text) => text.value.to_string(),
|
||||
OwnedValue::Integer(num) => num.to_string(),
|
||||
OwnedValue::Float(num) => format!("{:.14}", num),
|
||||
_ => return OwnedValue::Null,
|
||||
};
|
||||
|
||||
exec_datetime(&values[1..], DateTimeOutput::StrfTime(format_str))
|
||||
}
|
||||
|
||||
enum DateTimeOutput {
|
||||
Date,
|
||||
Time,
|
||||
DateTime,
|
||||
// Holds the format string
|
||||
StrfTime(String),
|
||||
}
|
||||
|
||||
fn exec_datetime(values: &[OwnedValue], output_type: DateTimeOutput) -> OwnedValue {
|
||||
if values.is_empty() {
|
||||
return OwnedValue::build_text(Rc::new(
|
||||
parse_naive_date_time(&OwnedValue::build_text(Rc::new("now".to_string())))
|
||||
.unwrap()
|
||||
.format(match output_type {
|
||||
DateTimeOutput::DateTime => "%Y-%m-%d %H:%M:%S",
|
||||
DateTimeOutput::Time => "%H:%M:%S",
|
||||
DateTimeOutput::Date => "%Y-%m-%d",
|
||||
})
|
||||
.to_string(),
|
||||
));
|
||||
let now =
|
||||
parse_naive_date_time(&OwnedValue::build_text(Rc::new("now".to_string()))).unwrap();
|
||||
|
||||
let formatted_str = match output_type {
|
||||
DateTimeOutput::DateTime => now.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
DateTimeOutput::Time => now.format("%H:%M:%S").to_string(),
|
||||
DateTimeOutput::Date => now.format("%Y-%m-%d").to_string(),
|
||||
DateTimeOutput::StrfTime(ref format_str) => strftime_format(&now, format_str),
|
||||
};
|
||||
|
||||
// Parse here
|
||||
return OwnedValue::build_text(Rc::new(formatted_str));
|
||||
}
|
||||
if let Some(mut dt) = parse_naive_date_time(&values[0]) {
|
||||
// if successful, treat subsequent entries as modifiers
|
||||
@@ -95,6 +115,31 @@ fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> St
|
||||
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
}
|
||||
DateTimeOutput::StrfTime(format_str) => strftime_format(&dt, &format_str),
|
||||
}
|
||||
}
|
||||
|
||||
// Not as fast as if the formatting was native to chrono, but a good enough
|
||||
// for now, just to have the feature implemented
|
||||
fn strftime_format(dt: &NaiveDateTime, format_str: &str) -> String {
|
||||
use std::fmt::Write;
|
||||
// Necessary to remove %f and %J that are exclusive formatters to sqlite
|
||||
// Chrono does not support them, so it is necessary to replace the modifiers manually
|
||||
|
||||
// Sqlite uses 9 decimal places for julianday in strftime
|
||||
let copy_format = format_str
|
||||
.to_string()
|
||||
.replace("%J", &format!("{:.9}", to_julian_day_exact(dt)));
|
||||
// Just change the formatting here to have fractional seconds using chrono builtin modifier
|
||||
let copy_format = copy_format.replace("%f", "%S.%3f");
|
||||
|
||||
// The write! macro is used here as chrono's format can panic if the formatting string contains
|
||||
// unknown specifiers. By using a writer, we can catch the panic and handle the error
|
||||
let mut formatted = String::new();
|
||||
match write!(formatted, "{}", dt.format(©_format)) {
|
||||
Ok(_) => formatted,
|
||||
// On sqlite when the formatting fails nothing is printed
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1729,4 +1774,7 @@ mod tests {
|
||||
.naive_utc();
|
||||
assert!(is_leap_second(&dt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strftime() {}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ use crate::{
|
||||
json::json_extract, json::json_object, json::json_type,
|
||||
};
|
||||
use crate::{resolve_ext_path, Connection, Result, Rows, TransactionState, DATABASE_VERSION};
|
||||
use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch};
|
||||
use datetime::{
|
||||
exec_date, exec_datetime_full, exec_julianday, exec_strftime, exec_time, exec_unixepoch,
|
||||
};
|
||||
use insn::{
|
||||
exec_add, exec_bit_and, exec_bit_not, exec_bit_or, exec_boolean_not, exec_concat, exec_divide,
|
||||
exec_multiply, exec_remainder, exec_shift_left, exec_shift_right, exec_subtract,
|
||||
@@ -2005,6 +2007,12 @@ impl Program {
|
||||
conn.load_extension(ext)?;
|
||||
}
|
||||
}
|
||||
ScalarFunc::StrfTime => {
|
||||
let result = exec_strftime(
|
||||
&state.registers[*start_reg..*start_reg + arg_count],
|
||||
);
|
||||
state.registers[*dest] = result;
|
||||
}
|
||||
},
|
||||
crate::function::Func::External(f) => match f.func {
|
||||
ExtFunc::Scalar(f) => {
|
||||
|
||||
@@ -443,3 +443,140 @@ do_execsql_test julianday-time-only {
|
||||
# SELECT julianday('2023-05-18');
|
||||
#} {2460082.5}
|
||||
|
||||
|
||||
|
||||
|
||||
# Strftime tests
|
||||
|
||||
|
||||
set FMT {%d,%e,%f,%F,%G,%g,%H,%I,%j,%J,%k,%l,%i,%m,%M,%p,%P,%R,%s,%S,%T,%U,%u,%V,%w,%W,%Y,%%}
|
||||
|
||||
do_execsql_test strftime-day {
|
||||
SELECT strftime('%d', '2025-01-23T13:10:30.567');
|
||||
} {23}
|
||||
|
||||
do_execsql_test strftime-day-without-leading-zero-1 {
|
||||
SELECT strftime('%e', '2025-01-23T13:10:30.567');
|
||||
} {23}
|
||||
|
||||
do_execsql_test strftime-day-without-leading-zero-2 {
|
||||
SELECT strftime('%e', '2025-01-02T13:10:30.567');
|
||||
} {" 2"}
|
||||
# TODO not a typo in sqlite there is also a space
|
||||
|
||||
do_execsql_test strftime-fractional-seconds {
|
||||
SELECT strftime('%f', '2025-01-02T13:10:30.567');
|
||||
} {30.567}
|
||||
|
||||
do_execsql_test strftime-iso-8601-date {
|
||||
SELECT strftime('%F', '2025-01-23T13:10:30.567');
|
||||
} {2025-01-23}
|
||||
|
||||
do_execsql_test strftime-iso-8601-year {
|
||||
SELECT strftime('%G', '2025-01-23T13:10:30.567');
|
||||
} {2025}
|
||||
|
||||
do_execsql_test strftime-iso-8601-year-2_digit {
|
||||
SELECT strftime('%g', '2025-01-23T13:10:30.567');
|
||||
} {25}
|
||||
|
||||
do_execsql_test strftime-hour {
|
||||
SELECT strftime('%H', '2025-01-23T13:10:30.567');
|
||||
} {13}
|
||||
|
||||
do_execsql_test strftime-hour-12-hour-clock {
|
||||
SELECT strftime('%I', '2025-01-23T13:10:30.567');
|
||||
} {01}
|
||||
|
||||
do_execsql_test strftime-day-of-year {
|
||||
SELECT strftime('%j', '2025-01-23T13:10:30.567');
|
||||
} {023}
|
||||
|
||||
do_execsql_test strftime-julianday {
|
||||
SELECT strftime('%J', '2025-01-23T13:10:30.567');
|
||||
} {2460699.048964896}
|
||||
|
||||
do_execsql_test strftime-hour-without-leading-zero-1 {
|
||||
SELECT strftime('%k', '2025-01-23T13:10:30.567');
|
||||
} {13}
|
||||
|
||||
do_execsql_test strftime-hour-without-leading-zero-2 {
|
||||
SELECT strftime('%k', '2025-01-23T02:10:30.567');
|
||||
} {" 2"}
|
||||
|
||||
do_execsql_test strftime-hour-12-hour-clock-without-leading-zero-2 {
|
||||
SELECT strftime('%l', '2025-01-23T13:10:30.567');
|
||||
} {" 1"}
|
||||
|
||||
do_execsql_test strftime-month {
|
||||
SELECT strftime('%m', '2025-01-23T13:10:30.567');
|
||||
} {01}
|
||||
|
||||
do_execsql_test strftime-minute {
|
||||
SELECT strftime('%M', '2025-01-23T13:14:30.567');
|
||||
} {14}
|
||||
|
||||
do_execsql_test strftime-am-pm=1 {
|
||||
SELECT strftime('%p', '2025-01-23T11:14:30.567');
|
||||
} {AM}
|
||||
|
||||
do_execsql_test strftime-am-pm-2 {
|
||||
SELECT strftime('%p', '2025-01-23T13:14:30.567');
|
||||
} {PM}
|
||||
|
||||
do_execsql_test strftime-am-pm-lower-1 {
|
||||
SELECT strftime('%P', '2025-01-23T11:14:30.567');
|
||||
} {am}
|
||||
|
||||
do_execsql_test strftime-am-pm-lower-2 {
|
||||
SELECT strftime('%P', '2025-01-23T13:14:30.567');
|
||||
} {pm}
|
||||
|
||||
do_execsql_test strftime-iso8601-time {
|
||||
SELECT strftime('%R', '2025-01-23T13:14:30.567');
|
||||
} {13:14}
|
||||
|
||||
do_execsql_test strftime-seconds-since-epoch {
|
||||
SELECT strftime('%s', '2025-01-23T13:14:30.567');
|
||||
} {1737638070}
|
||||
|
||||
do_execsql_test strftime-seconds {
|
||||
SELECT strftime('%S', '2025-01-23T13:14:30.567');
|
||||
} {30}
|
||||
|
||||
do_execsql_test strftime-iso8601-with-seconds {
|
||||
SELECT strftime('%T', '2025-01-23T13:14:30.567');
|
||||
} {13:14:30}
|
||||
|
||||
do_execsql_test strftime-week-year-start-sunday {
|
||||
SELECT strftime('%U', '2025-01-23T13:14:30.567');
|
||||
} {03}
|
||||
|
||||
do_execsql_test strftime-day-week-start-monday {
|
||||
SELECT strftime('%u', '2025-01-23T13:14:30.567');
|
||||
} {4}
|
||||
|
||||
do_execsql_test strftime-iso8601-week-year {
|
||||
SELECT strftime('%V', '2025-01-23T13:14:30.567');
|
||||
} {04}
|
||||
|
||||
do_execsql_test strftime-day-week-start-sunday {
|
||||
SELECT strftime('%w', '2025-01-23T13:14:30.567');
|
||||
} {4}
|
||||
|
||||
do_execsql_test strftime-day-week-start-sunday {
|
||||
SELECT strftime('%w', '2025-01-23T13:14:30.567');
|
||||
} {4}
|
||||
|
||||
do_execsql_test strftime-week-year-start-sunday {
|
||||
SELECT strftime('%W', '2025-01-23T13:14:30.567');
|
||||
} {03}
|
||||
|
||||
do_execsql_test strftime-year {
|
||||
SELECT strftime('%Y', '2025-01-23T13:14:30.567');
|
||||
} {2025}
|
||||
|
||||
do_execsql_test strftime-percent {
|
||||
SELECT strftime('%%', '2025-01-23T13:14:30.567');
|
||||
} {%}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user