use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use limbo::Value as LimboValue;
use oxisql_core::Value as CoreValue;
use crate::error::SqliteCompatError;
const UNIX_EPOCH_DATE: fn() -> NaiveDate =
|| NaiveDate::from_ymd_opt(1970, 1, 1).expect("epoch date is valid");
pub fn limbo_to_core(val: LimboValue) -> Result<CoreValue, SqliteCompatError> {
limbo_to_core_typed(val, None)
}
pub fn limbo_to_core_typed(
val: LimboValue,
decl_type: Option<&str>,
) -> Result<CoreValue, SqliteCompatError> {
let dt_upper: Option<String> = decl_type.map(|s| s.to_ascii_uppercase());
let dt = dt_upper.as_deref();
let v = match val {
LimboValue::Null => CoreValue::Null,
LimboValue::Real(f) => CoreValue::F64(f),
LimboValue::Integer(n) => {
if let Some(dt) = dt {
if is_datetime_type(dt) {
return Ok(CoreValue::Timestamp(n));
} else if is_date_type(dt) {
let days = i32::try_from(n).unwrap_or(n as i32);
return Ok(CoreValue::Date(days));
} else if is_time_type(dt) {
return Ok(CoreValue::Time(n));
}
}
CoreValue::I64(n)
}
LimboValue::Text(s) => {
if let Some(dt) = dt {
if is_datetime_type(dt) {
if let Some(ts) = parse_text_as_timestamp(&s) {
return Ok(CoreValue::Timestamp(ts));
}
} else if is_date_type(dt) {
if let Some(days) = parse_text_as_date(&s) {
return Ok(CoreValue::Date(days));
}
} else if is_time_type(dt) {
if let Some(us) = parse_text_as_time(&s) {
return Ok(CoreValue::Time(us));
}
} else if is_uuid_type(dt) {
if let Some(u) = parse_text_as_uuid(&s) {
return Ok(CoreValue::Uuid(u));
}
}
}
CoreValue::Text(s)
}
LimboValue::Blob(b) => {
if let Some(dt) = dt {
if is_uuid_type(dt) && b.len() == 16 {
let mut arr = [0u8; 16];
arr.copy_from_slice(&b);
let u = u128::from_be_bytes(arr);
return Ok(CoreValue::Uuid(u));
}
}
CoreValue::Blob(b)
}
};
Ok(v)
}
#[inline]
fn is_datetime_type(dt: &str) -> bool {
dt.starts_with("DATETIME") || dt.starts_with("TIMESTAMP")
}
#[inline]
fn is_date_type(dt: &str) -> bool {
dt.starts_with("DATE")
}
#[inline]
fn is_time_type(dt: &str) -> bool {
dt.starts_with("TIME")
}
#[inline]
fn is_uuid_type(dt: &str) -> bool {
dt.starts_with("UUID")
}
fn parse_text_as_timestamp(s: &str) -> Option<i64> {
let fmt_t = "%Y-%m-%dT%H:%M:%S%.f";
let fmt_sp = "%Y-%m-%d %H:%M:%S%.f";
let fmt_t_no_frac = "%Y-%m-%dT%H:%M:%S";
let fmt_sp_no_frac = "%Y-%m-%d %H:%M:%S";
let dt: Option<NaiveDateTime> = NaiveDateTime::parse_from_str(s, fmt_t)
.or_else(|_| NaiveDateTime::parse_from_str(s, fmt_sp))
.or_else(|_| NaiveDateTime::parse_from_str(s, fmt_t_no_frac))
.or_else(|_| NaiveDateTime::parse_from_str(s, fmt_sp_no_frac))
.ok();
dt.map(|d| {
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.expect("epoch datetime is valid");
let dur = d.signed_duration_since(epoch);
dur.num_microseconds()
.unwrap_or(dur.num_milliseconds() * 1_000)
})
}
fn parse_text_as_date(s: &str) -> Option<i32> {
let d = NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()?;
let epoch = UNIX_EPOCH_DATE();
let days = d.signed_duration_since(epoch).num_days();
i32::try_from(days).ok()
}
fn parse_text_as_time(s: &str) -> Option<i64> {
let t: Option<NaiveTime> = NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
.or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
.ok();
t.map(|t| {
let midnight = NaiveTime::from_hms_opt(0, 0, 0).expect("midnight is valid");
let dur = t.signed_duration_since(midnight);
dur.num_microseconds()
.unwrap_or(dur.num_milliseconds() * 1_000)
})
}
fn parse_text_as_uuid(s: &str) -> Option<u128> {
if s.len() != 36 {
return None;
}
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 5 {
return None;
}
let expected_lens = [8usize, 4, 4, 4, 12];
for (part, &expected) in parts.iter().zip(expected_lens.iter()) {
if part.len() != expected {
return None;
}
}
let hex: String = parts.concat();
u128::from_str_radix(&hex, 16).ok()
}
pub fn core_to_limbo(val: &CoreValue) -> Result<LimboValue, SqliteCompatError> {
let v = match val {
CoreValue::Null => LimboValue::Null,
CoreValue::Bool(b) => LimboValue::Integer(i64::from(*b)),
CoreValue::I64(n) => LimboValue::Integer(*n),
CoreValue::F64(f) => LimboValue::Real(*f),
CoreValue::Text(s) => LimboValue::Text(s.clone()),
CoreValue::Blob(b) => LimboValue::Blob(b.clone()),
CoreValue::Timestamp(us) => LimboValue::Integer(*us),
CoreValue::Date(days) => LimboValue::Integer(i64::from(*days)),
CoreValue::Time(us) => LimboValue::Integer(*us),
CoreValue::Uuid(u) => {
let hi = (u >> 64) as u64;
let lo = *u as u64;
let raw: [u8; 16] = {
let mut buf = [0u8; 16];
buf[..8].copy_from_slice(&hi.to_be_bytes());
buf[8..].copy_from_slice(&lo.to_be_bytes());
buf
};
let s = format!(
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
u32::from_be_bytes(
raw[0..4]
.try_into()
.map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
),
u16::from_be_bytes(
raw[4..6]
.try_into()
.map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
),
u16::from_be_bytes(
raw[6..8]
.try_into()
.map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
),
u16::from_be_bytes(
raw[8..10]
.try_into()
.map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
),
{
let b = &raw[10..16];
((b[0] as u64) << 40)
| ((b[1] as u64) << 32)
| ((b[2] as u64) << 24)
| ((b[3] as u64) << 16)
| ((b[4] as u64) << 8)
| (b[5] as u64)
}
);
LimboValue::Text(s)
}
CoreValue::Decimal(s) | CoreValue::Json(s) => LimboValue::Text(s.clone()),
CoreValue::Array(arr) => LimboValue::Text(format!("{arr:?}")),
CoreValue::TypedArray { values: arr, .. } => LimboValue::Text(format!("{arr:?}")),
};
Ok(v)
}
pub(crate) fn split_statements(sql: &str) -> Vec<&str> {
let mut stmts = Vec::new();
let bytes = sql.as_bytes();
let len = bytes.len();
let mut i = 0usize;
let mut stmt_start = 0usize;
while i < len {
match bytes[i] {
b'\'' => {
i += 1;
while i < len {
if bytes[i] == b'\'' {
if i + 1 < len && bytes[i + 1] == b'\'' {
i += 2; } else {
i += 1; break;
}
} else {
i += 1;
}
}
}
b'"' => {
i += 1;
while i < len && bytes[i] != b'"' {
i += 1;
}
if i < len {
i += 1; }
}
b'`' => {
i += 1;
while i < len && bytes[i] != b'`' {
i += 1;
}
if i < len {
i += 1; }
}
b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
while i < len && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
i += 2;
while i + 1 < len {
if bytes[i] == b'*' && bytes[i + 1] == b'/' {
i += 2;
break;
}
i += 1;
}
}
b';' => {
let stmt = sql[stmt_start..i].trim();
if !stmt.is_empty() {
stmts.push(stmt);
}
i += 1;
stmt_start = i;
}
_ => {
i += 1;
}
}
}
let tail = sql[stmt_start..].trim();
if !tail.is_empty() {
stmts.push(tail);
}
stmts
}
pub fn rewrite_params(
sql: &str,
params: &[&dyn oxisql_core::ToSqlValue],
) -> Result<(String, Vec<LimboValue>), SqliteCompatError> {
let mut out = String::with_capacity(sql.len());
let mut ordered: Vec<LimboValue> = Vec::new();
let chars: Vec<char> = sql.chars().collect();
let n = chars.len();
let mut i = 0;
while i < n {
match chars[i] {
'\'' => {
out.push('\'');
i += 1;
while i < n {
let c = chars[i];
out.push(c);
i += 1;
if c == '\'' {
if i < n && chars[i] == '\'' {
out.push('\'');
i += 1;
} else {
break;
}
}
}
}
'"' => {
out.push('"');
i += 1;
while i < n && chars[i] != '"' {
out.push(chars[i]);
i += 1;
}
if i < n {
out.push('"');
i += 1;
}
}
'$' => {
i += 1;
let start = i;
while i < n && chars[i].is_ascii_digit() {
i += 1;
}
if i > start {
let idx_str: String = chars[start..i].iter().collect();
let idx: usize = idx_str.parse::<usize>().map_err(|_| {
SqliteCompatError::TypeMap(format!(
"invalid parameter placeholder: ${idx_str}"
))
})?;
if idx == 0 || idx > params.len() {
return Err(SqliteCompatError::TypeMap(format!(
"parameter ${idx} is out of range (have {} params)",
params.len()
)));
}
let limbo_val = core_to_limbo(¶ms[idx - 1].to_value())?;
ordered.push(limbo_val);
out.push('?');
} else {
out.push('$');
}
}
c => {
out.push(c);
i += 1;
}
}
}
Ok((out, ordered))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_basic() {
let stmts = split_statements("SELECT 1; SELECT 2");
assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
}
#[test]
fn test_split_trailing_semicolon() {
let stmts = split_statements("SELECT 1; SELECT 2;");
assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
}
#[test]
fn test_split_single_statement_no_semicolon() {
let stmts = split_statements("SELECT 1");
assert_eq!(stmts, vec!["SELECT 1"]);
}
#[test]
fn test_split_empty_statements() {
let stmts = split_statements(";;;");
assert!(stmts.is_empty(), "expected 0 stmts, got {stmts:?}");
}
#[test]
fn test_split_whitespace_only() {
let stmts = split_statements(" \n ");
assert!(stmts.is_empty());
}
#[test]
fn test_split_semicolon_in_single_quoted_string() {
let stmts = split_statements("INSERT INTO t VALUES ('a;b')");
assert_eq!(stmts, vec!["INSERT INTO t VALUES ('a;b')"]);
}
#[test]
fn test_split_escaped_single_quotes() {
let stmts = split_statements("INSERT INTO t VALUES ('it''s ok;really')");
assert_eq!(stmts, vec!["INSERT INTO t VALUES ('it''s ok;really')"]);
}
#[test]
fn test_split_double_quoted_identifier() {
let stmts = split_statements(r#"SELECT "col;name" FROM t"#);
assert_eq!(stmts, vec![r#"SELECT "col;name" FROM t"#]);
}
#[test]
fn test_split_backtick_quoted_identifier() {
let stmts = split_statements("SELECT `col;name` FROM t");
assert_eq!(stmts, vec!["SELECT `col;name` FROM t"]);
}
#[test]
fn test_split_line_comment() {
let stmts = split_statements("SELECT 1 -- ; this is a comment\n");
assert_eq!(stmts, vec!["SELECT 1 -- ; this is a comment"]);
}
#[test]
fn test_split_line_comment_between_stmts() {
let sql = "SELECT 1; -- comment with ; semicolon\nSELECT 2";
let stmts = split_statements(sql);
assert_eq!(stmts.len(), 2, "got {stmts:?}");
assert_eq!(stmts[0], "SELECT 1");
assert_eq!(stmts[1], "-- comment with ; semicolon\nSELECT 2");
}
#[test]
fn test_split_block_comment() {
let stmts = split_statements("SELECT /* ; */ 1");
assert_eq!(stmts, vec!["SELECT /* ; */ 1"]);
}
#[test]
fn test_split_block_comment_spanning_stmts() {
let sql = "SELECT 1; /* comment; with semicolons */ SELECT 2";
let stmts = split_statements(sql);
assert_eq!(stmts.len(), 2, "got {stmts:?}");
assert_eq!(stmts[0], "SELECT 1");
assert_eq!(stmts[1], "/* comment; with semicolons */ SELECT 2");
}
#[test]
fn test_split_multiple_with_trailing_no_semicolon() {
let sql = "CREATE TABLE t (id INT);\nINSERT INTO t VALUES (1)";
let stmts = split_statements(sql);
assert_eq!(stmts.len(), 2, "got {stmts:?}");
assert_eq!(stmts[0], "CREATE TABLE t (id INT)");
assert_eq!(stmts[1], "INSERT INTO t VALUES (1)");
}
#[test]
fn test_split_trims_whitespace() {
let stmts = split_statements(" SELECT 1 ; SELECT 2 ");
assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
}
#[test]
fn test_limbo_to_core_all_types() {
assert_eq!(limbo_to_core(LimboValue::Null).unwrap(), CoreValue::Null);
assert_eq!(
limbo_to_core(LimboValue::Integer(42)).unwrap(),
CoreValue::I64(42)
);
assert_eq!(
limbo_to_core(LimboValue::Real(1.5)).unwrap(),
CoreValue::F64(1.5)
);
assert_eq!(
limbo_to_core(LimboValue::Text("hello".into())).unwrap(),
CoreValue::Text("hello".into())
);
assert_eq!(
limbo_to_core(LimboValue::Blob(vec![1, 2, 3])).unwrap(),
CoreValue::Blob(vec![1, 2, 3])
);
}
#[test]
fn test_core_to_limbo_basic() {
assert_eq!(core_to_limbo(&CoreValue::Null).unwrap(), LimboValue::Null);
assert_eq!(
core_to_limbo(&CoreValue::I64(7)).unwrap(),
LimboValue::Integer(7)
);
assert_eq!(
core_to_limbo(&CoreValue::F64(1.5)).unwrap(),
LimboValue::Real(1.5)
);
assert_eq!(
core_to_limbo(&CoreValue::Bool(true)).unwrap(),
LimboValue::Integer(1)
);
}
#[test]
fn test_rewrite_params_basic() {
let params: Vec<&dyn oxisql_core::ToSqlValue> = vec![&42i64, &"hello"];
let (sql, vals) = rewrite_params("SELECT $1, $2", ¶ms).unwrap();
assert_eq!(sql, "SELECT ?, ?");
assert_eq!(vals.len(), 2);
assert_eq!(vals[0], LimboValue::Integer(42));
assert_eq!(vals[1], LimboValue::Text("hello".into()));
}
#[test]
fn test_rewrite_params_skips_string_literals() {
let params: Vec<&dyn oxisql_core::ToSqlValue> = vec![&99i64];
let (sql, vals) = rewrite_params("SELECT '$1' WHERE id = $1", ¶ms).unwrap();
assert_eq!(sql, "SELECT '$1' WHERE id = ?");
assert_eq!(vals.len(), 1);
assert_eq!(vals[0], LimboValue::Integer(99));
}
#[test]
fn test_rewrite_params_out_of_range() {
let params: Vec<&dyn oxisql_core::ToSqlValue> = vec![&1i64];
assert!(rewrite_params("SELECT $2", ¶ms).is_err());
}
#[test]
fn test_rewrite_params_no_params() {
let params: &[&dyn oxisql_core::ToSqlValue] = &[];
let (sql, vals) = rewrite_params("SELECT 1", params).unwrap();
assert_eq!(sql, "SELECT 1");
assert!(vals.is_empty());
}
}