1use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
23use limbo::Value as LimboValue;
24use oxisql_core::Value as CoreValue;
25
26use crate::error::SqliteCompatError;
27
28const UNIX_EPOCH_DATE: fn() -> NaiveDate =
30 || NaiveDate::from_ymd_opt(1970, 1, 1).expect("epoch date is valid");
31
32pub fn limbo_to_core(val: LimboValue) -> Result<CoreValue, SqliteCompatError> {
43 limbo_to_core_typed(val, None)
44}
45
46pub fn limbo_to_core_typed(
68 val: LimboValue,
69 decl_type: Option<&str>,
70) -> Result<CoreValue, SqliteCompatError> {
71 let dt_upper: Option<String> = decl_type.map(|s| s.to_ascii_uppercase());
73 let dt = dt_upper.as_deref();
74
75 let v = match val {
76 LimboValue::Null => CoreValue::Null,
77 LimboValue::Real(f) => CoreValue::F64(f),
78
79 LimboValue::Integer(n) => {
80 if let Some(dt) = dt {
81 if is_datetime_type(dt) {
82 return Ok(CoreValue::Timestamp(n));
83 } else if is_date_type(dt) {
84 let days = i32::try_from(n).unwrap_or(n as i32);
86 return Ok(CoreValue::Date(days));
87 } else if is_time_type(dt) {
88 return Ok(CoreValue::Time(n));
89 }
90 }
91 CoreValue::I64(n)
92 }
93
94 LimboValue::Text(s) => {
95 if let Some(dt) = dt {
96 if is_datetime_type(dt) {
97 if let Some(ts) = parse_text_as_timestamp(&s) {
98 return Ok(CoreValue::Timestamp(ts));
99 }
100 } else if is_date_type(dt) {
101 if let Some(days) = parse_text_as_date(&s) {
102 return Ok(CoreValue::Date(days));
103 }
104 } else if is_time_type(dt) {
105 if let Some(us) = parse_text_as_time(&s) {
106 return Ok(CoreValue::Time(us));
107 }
108 } else if is_uuid_type(dt) {
109 if let Some(u) = parse_text_as_uuid(&s) {
110 return Ok(CoreValue::Uuid(u));
111 }
112 }
113 }
114 CoreValue::Text(s)
115 }
116
117 LimboValue::Blob(b) => {
118 if let Some(dt) = dt {
120 if is_uuid_type(dt) && b.len() == 16 {
121 let mut arr = [0u8; 16];
122 arr.copy_from_slice(&b);
123 let u = u128::from_be_bytes(arr);
124 return Ok(CoreValue::Uuid(u));
125 }
126 }
127 CoreValue::Blob(b)
128 }
129 };
130 Ok(v)
131}
132
133#[inline]
140fn is_datetime_type(dt: &str) -> bool {
141 dt.starts_with("DATETIME") || dt.starts_with("TIMESTAMP")
142}
143
144#[inline]
148fn is_date_type(dt: &str) -> bool {
149 dt.starts_with("DATE")
150}
151
152#[inline]
156fn is_time_type(dt: &str) -> bool {
157 dt.starts_with("TIME")
158}
159
160#[inline]
162fn is_uuid_type(dt: &str) -> bool {
163 dt.starts_with("UUID")
164}
165
166fn parse_text_as_timestamp(s: &str) -> Option<i64> {
173 let fmt_t = "%Y-%m-%dT%H:%M:%S%.f";
175 let fmt_sp = "%Y-%m-%d %H:%M:%S%.f";
176 let fmt_t_no_frac = "%Y-%m-%dT%H:%M:%S";
177 let fmt_sp_no_frac = "%Y-%m-%d %H:%M:%S";
178
179 let dt: Option<NaiveDateTime> = NaiveDateTime::parse_from_str(s, fmt_t)
180 .or_else(|_| NaiveDateTime::parse_from_str(s, fmt_sp))
181 .or_else(|_| NaiveDateTime::parse_from_str(s, fmt_t_no_frac))
182 .or_else(|_| NaiveDateTime::parse_from_str(s, fmt_sp_no_frac))
183 .ok();
184
185 dt.map(|d| {
186 let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)
187 .and_then(|d| d.and_hms_opt(0, 0, 0))
188 .expect("epoch datetime is valid");
189 let dur = d.signed_duration_since(epoch);
190 dur.num_microseconds()
191 .unwrap_or(dur.num_milliseconds() * 1_000)
192 })
193}
194
195fn parse_text_as_date(s: &str) -> Option<i32> {
199 let d = NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()?;
200 let epoch = UNIX_EPOCH_DATE();
201 let days = d.signed_duration_since(epoch).num_days();
204 i32::try_from(days).ok()
205}
206
207fn parse_text_as_time(s: &str) -> Option<i64> {
212 let t: Option<NaiveTime> = NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
213 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
214 .ok();
215 t.map(|t| {
216 let midnight = NaiveTime::from_hms_opt(0, 0, 0).expect("midnight is valid");
217 let dur = t.signed_duration_since(midnight);
218 dur.num_microseconds()
219 .unwrap_or(dur.num_milliseconds() * 1_000)
220 })
221}
222
223fn parse_text_as_uuid(s: &str) -> Option<u128> {
226 if s.len() != 36 {
228 return None;
229 }
230 let parts: Vec<&str> = s.split('-').collect();
231 if parts.len() != 5 {
232 return None;
233 }
234 let expected_lens = [8usize, 4, 4, 4, 12];
235 for (part, &expected) in parts.iter().zip(expected_lens.iter()) {
236 if part.len() != expected {
237 return None;
238 }
239 }
240
241 let hex: String = parts.concat();
243 u128::from_str_radix(&hex, 16).ok()
244}
245
246pub fn core_to_limbo(val: &CoreValue) -> Result<LimboValue, SqliteCompatError> {
263 let v = match val {
264 CoreValue::Null => LimboValue::Null,
265 CoreValue::Bool(b) => LimboValue::Integer(i64::from(*b)),
266 CoreValue::I64(n) => LimboValue::Integer(*n),
267 CoreValue::F64(f) => LimboValue::Real(*f),
268 CoreValue::Text(s) => LimboValue::Text(s.clone()),
269 CoreValue::Blob(b) => LimboValue::Blob(b.clone()),
270 CoreValue::Timestamp(us) => LimboValue::Integer(*us),
271 CoreValue::Date(days) => LimboValue::Integer(i64::from(*days)),
272 CoreValue::Time(us) => LimboValue::Integer(*us),
273 CoreValue::Uuid(u) => {
274 let hi = (u >> 64) as u64;
276 let lo = *u as u64;
277 let raw: [u8; 16] = {
278 let mut buf = [0u8; 16];
279 buf[..8].copy_from_slice(&hi.to_be_bytes());
280 buf[8..].copy_from_slice(&lo.to_be_bytes());
281 buf
282 };
283 let s = format!(
284 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
285 u32::from_be_bytes(
286 raw[0..4]
287 .try_into()
288 .map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
289 ),
290 u16::from_be_bytes(
291 raw[4..6]
292 .try_into()
293 .map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
294 ),
295 u16::from_be_bytes(
296 raw[6..8]
297 .try_into()
298 .map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
299 ),
300 u16::from_be_bytes(
301 raw[8..10]
302 .try_into()
303 .map_err(|_| SqliteCompatError::TypeMap("uuid slice error".into()))?
304 ),
305 {
306 let b = &raw[10..16];
307 ((b[0] as u64) << 40)
308 | ((b[1] as u64) << 32)
309 | ((b[2] as u64) << 24)
310 | ((b[3] as u64) << 16)
311 | ((b[4] as u64) << 8)
312 | (b[5] as u64)
313 }
314 );
315 LimboValue::Text(s)
316 }
317 CoreValue::Decimal(s) | CoreValue::Json(s) => LimboValue::Text(s.clone()),
318 CoreValue::Array(arr) => LimboValue::Text(format!("{arr:?}")),
319 CoreValue::TypedArray { values: arr, .. } => LimboValue::Text(format!("{arr:?}")),
320 };
321 Ok(v)
322}
323
324pub(crate) fn split_statements(sql: &str) -> Vec<&str> {
342 let mut stmts = Vec::new();
343 let bytes = sql.as_bytes();
344 let len = bytes.len();
345 let mut i = 0usize;
346 let mut stmt_start = 0usize;
347
348 while i < len {
349 match bytes[i] {
350 b'\'' => {
351 i += 1;
354 while i < len {
355 if bytes[i] == b'\'' {
356 if i + 1 < len && bytes[i + 1] == b'\'' {
357 i += 2; } else {
359 i += 1; break;
361 }
362 } else {
363 i += 1;
364 }
365 }
366 }
367 b'"' => {
368 i += 1;
370 while i < len && bytes[i] != b'"' {
371 i += 1;
372 }
373 if i < len {
374 i += 1; }
376 }
377 b'`' => {
378 i += 1;
380 while i < len && bytes[i] != b'`' {
381 i += 1;
382 }
383 if i < len {
384 i += 1; }
386 }
387 b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
388 while i < len && bytes[i] != b'\n' {
390 i += 1;
391 }
392 }
393 b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
394 i += 2;
396 while i + 1 < len {
397 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
398 i += 2;
399 break;
400 }
401 i += 1;
402 }
403 }
404 b';' => {
405 let stmt = sql[stmt_start..i].trim();
406 if !stmt.is_empty() {
407 stmts.push(stmt);
408 }
409 i += 1;
410 stmt_start = i;
411 }
412 _ => {
413 i += 1;
414 }
415 }
416 }
417
418 let tail = sql[stmt_start..].trim();
420 if !tail.is_empty() {
421 stmts.push(tail);
422 }
423
424 stmts
425}
426
427pub fn rewrite_params(
443 sql: &str,
444 params: &[&dyn oxisql_core::ToSqlValue],
445) -> Result<(String, Vec<LimboValue>), SqliteCompatError> {
446 let mut out = String::with_capacity(sql.len());
447 let mut ordered: Vec<LimboValue> = Vec::new();
448 let chars: Vec<char> = sql.chars().collect();
449 let n = chars.len();
450 let mut i = 0;
451
452 while i < n {
453 match chars[i] {
454 '\'' => {
456 out.push('\'');
457 i += 1;
458 while i < n {
459 let c = chars[i];
460 out.push(c);
461 i += 1;
462 if c == '\'' {
463 if i < n && chars[i] == '\'' {
465 out.push('\'');
466 i += 1;
467 } else {
468 break;
469 }
470 }
471 }
472 }
473 '"' => {
475 out.push('"');
476 i += 1;
477 while i < n && chars[i] != '"' {
478 out.push(chars[i]);
479 i += 1;
480 }
481 if i < n {
482 out.push('"');
483 i += 1;
484 }
485 }
486 '$' => {
488 i += 1;
489 let start = i;
491 while i < n && chars[i].is_ascii_digit() {
492 i += 1;
493 }
494 if i > start {
495 let idx_str: String = chars[start..i].iter().collect();
496 let idx: usize = idx_str.parse::<usize>().map_err(|_| {
497 SqliteCompatError::TypeMap(format!(
498 "invalid parameter placeholder: ${idx_str}"
499 ))
500 })?;
501 if idx == 0 || idx > params.len() {
502 return Err(SqliteCompatError::TypeMap(format!(
503 "parameter ${idx} is out of range (have {} params)",
504 params.len()
505 )));
506 }
507 let limbo_val = core_to_limbo(¶ms[idx - 1].to_value())?;
508 ordered.push(limbo_val);
509 out.push('?');
510 } else {
511 out.push('$');
513 }
514 }
515 c => {
516 out.push(c);
517 i += 1;
518 }
519 }
520 }
521
522 Ok((out, ordered))
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
532 fn test_split_basic() {
533 let stmts = split_statements("SELECT 1; SELECT 2");
534 assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
535 }
536
537 #[test]
538 fn test_split_trailing_semicolon() {
539 let stmts = split_statements("SELECT 1; SELECT 2;");
540 assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
541 }
542
543 #[test]
544 fn test_split_single_statement_no_semicolon() {
545 let stmts = split_statements("SELECT 1");
546 assert_eq!(stmts, vec!["SELECT 1"]);
547 }
548
549 #[test]
550 fn test_split_empty_statements() {
551 let stmts = split_statements(";;;");
552 assert!(stmts.is_empty(), "expected 0 stmts, got {stmts:?}");
553 }
554
555 #[test]
556 fn test_split_whitespace_only() {
557 let stmts = split_statements(" \n ");
558 assert!(stmts.is_empty());
559 }
560
561 #[test]
562 fn test_split_semicolon_in_single_quoted_string() {
563 let stmts = split_statements("INSERT INTO t VALUES ('a;b')");
564 assert_eq!(stmts, vec!["INSERT INTO t VALUES ('a;b')"]);
565 }
566
567 #[test]
568 fn test_split_escaped_single_quotes() {
569 let stmts = split_statements("INSERT INTO t VALUES ('it''s ok;really')");
571 assert_eq!(stmts, vec!["INSERT INTO t VALUES ('it''s ok;really')"]);
572 }
573
574 #[test]
575 fn test_split_double_quoted_identifier() {
576 let stmts = split_statements(r#"SELECT "col;name" FROM t"#);
577 assert_eq!(stmts, vec![r#"SELECT "col;name" FROM t"#]);
578 }
579
580 #[test]
581 fn test_split_backtick_quoted_identifier() {
582 let stmts = split_statements("SELECT `col;name` FROM t");
583 assert_eq!(stmts, vec!["SELECT `col;name` FROM t"]);
584 }
585
586 #[test]
587 fn test_split_line_comment() {
588 let stmts = split_statements("SELECT 1 -- ; this is a comment\n");
590 assert_eq!(stmts, vec!["SELECT 1 -- ; this is a comment"]);
591 }
592
593 #[test]
594 fn test_split_line_comment_between_stmts() {
595 let sql = "SELECT 1; -- comment with ; semicolon\nSELECT 2";
596 let stmts = split_statements(sql);
597 assert_eq!(stmts.len(), 2, "got {stmts:?}");
598 assert_eq!(stmts[0], "SELECT 1");
599 assert_eq!(stmts[1], "-- comment with ; semicolon\nSELECT 2");
600 }
601
602 #[test]
603 fn test_split_block_comment() {
604 let stmts = split_statements("SELECT /* ; */ 1");
605 assert_eq!(stmts, vec!["SELECT /* ; */ 1"]);
606 }
607
608 #[test]
609 fn test_split_block_comment_spanning_stmts() {
610 let sql = "SELECT 1; /* comment; with semicolons */ SELECT 2";
611 let stmts = split_statements(sql);
612 assert_eq!(stmts.len(), 2, "got {stmts:?}");
613 assert_eq!(stmts[0], "SELECT 1");
614 assert_eq!(stmts[1], "/* comment; with semicolons */ SELECT 2");
615 }
616
617 #[test]
618 fn test_split_multiple_with_trailing_no_semicolon() {
619 let sql = "CREATE TABLE t (id INT);\nINSERT INTO t VALUES (1)";
620 let stmts = split_statements(sql);
621 assert_eq!(stmts.len(), 2, "got {stmts:?}");
622 assert_eq!(stmts[0], "CREATE TABLE t (id INT)");
623 assert_eq!(stmts[1], "INSERT INTO t VALUES (1)");
624 }
625
626 #[test]
627 fn test_split_trims_whitespace() {
628 let stmts = split_statements(" SELECT 1 ; SELECT 2 ");
629 assert_eq!(stmts, vec!["SELECT 1", "SELECT 2"]);
630 }
631
632 #[test]
635 fn test_limbo_to_core_all_types() {
636 assert_eq!(limbo_to_core(LimboValue::Null).unwrap(), CoreValue::Null);
637 assert_eq!(
638 limbo_to_core(LimboValue::Integer(42)).unwrap(),
639 CoreValue::I64(42)
640 );
641 assert_eq!(
642 limbo_to_core(LimboValue::Real(1.5)).unwrap(),
643 CoreValue::F64(1.5)
644 );
645 assert_eq!(
646 limbo_to_core(LimboValue::Text("hello".into())).unwrap(),
647 CoreValue::Text("hello".into())
648 );
649 assert_eq!(
650 limbo_to_core(LimboValue::Blob(vec![1, 2, 3])).unwrap(),
651 CoreValue::Blob(vec![1, 2, 3])
652 );
653 }
654
655 #[test]
656 fn test_core_to_limbo_basic() {
657 assert_eq!(core_to_limbo(&CoreValue::Null).unwrap(), LimboValue::Null);
658 assert_eq!(
659 core_to_limbo(&CoreValue::I64(7)).unwrap(),
660 LimboValue::Integer(7)
661 );
662 assert_eq!(
663 core_to_limbo(&CoreValue::F64(1.5)).unwrap(),
664 LimboValue::Real(1.5)
665 );
666 assert_eq!(
667 core_to_limbo(&CoreValue::Bool(true)).unwrap(),
668 LimboValue::Integer(1)
669 );
670 }
671
672 #[test]
673 fn test_rewrite_params_basic() {
674 let params: Vec<&dyn oxisql_core::ToSqlValue> = vec![&42i64, &"hello"];
675 let (sql, vals) = rewrite_params("SELECT $1, $2", ¶ms).unwrap();
676 assert_eq!(sql, "SELECT ?, ?");
677 assert_eq!(vals.len(), 2);
678 assert_eq!(vals[0], LimboValue::Integer(42));
679 assert_eq!(vals[1], LimboValue::Text("hello".into()));
680 }
681
682 #[test]
683 fn test_rewrite_params_skips_string_literals() {
684 let params: Vec<&dyn oxisql_core::ToSqlValue> = vec![&99i64];
685 let (sql, vals) = rewrite_params("SELECT '$1' WHERE id = $1", ¶ms).unwrap();
686 assert_eq!(sql, "SELECT '$1' WHERE id = ?");
687 assert_eq!(vals.len(), 1);
688 assert_eq!(vals[0], LimboValue::Integer(99));
689 }
690
691 #[test]
692 fn test_rewrite_params_out_of_range() {
693 let params: Vec<&dyn oxisql_core::ToSqlValue> = vec![&1i64];
694 assert!(rewrite_params("SELECT $2", ¶ms).is_err());
695 }
696
697 #[test]
698 fn test_rewrite_params_no_params() {
699 let params: &[&dyn oxisql_core::ToSqlValue] = &[];
700 let (sql, vals) = rewrite_params("SELECT 1", params).unwrap();
701 assert_eq!(sql, "SELECT 1");
702 assert!(vals.is_empty());
703 }
704}