use crate::error::Result;
use crate::types::{ColumnType, Row, SqlRow, TableSchema, Value};
pub fn row_to_sql_row(row: &Row, schema: &TableSchema) -> Result<SqlRow> {
let mut sql_row = SqlRow::with_capacity(schema.columns.len());
for (i, col_def) in schema.columns.iter().enumerate() {
let value = row.get(i).cloned().unwrap_or(Value::Null);
let coerced = match (&col_def.col_type, &value) {
(ColumnType::Timestamp, Value::Integer(i)) => {
Value::Timestamp(crate::types::Timestamp::from_micros(*i))
}
_ => value,
};
sql_row.insert(col_def.name.clone(), coerced);
}
Ok(sql_row)
}
pub fn sql_row_to_row(sql_row: &SqlRow, schema: &TableSchema) -> Result<Row> {
let mut row = Vec::with_capacity(schema.columns.len());
for col_def in &schema.columns {
let value = sql_row.get(&col_def.name).cloned().unwrap_or(Value::Null);
if !col_def.nullable && !col_def.auto_increment && matches!(value, Value::Null) {
return Err(crate::error::MoteDBError::InvalidArgument(format!(
"Column '{}' cannot be null",
col_def.name
)));
}
let coerced_value = match (&col_def.col_type, &value) {
(ColumnType::Timestamp, Value::Integer(i)) => {
use crate::types::Timestamp;
Value::Timestamp(Timestamp::from_micros(*i))
}
(ColumnType::Float, Value::Integer(i)) => Value::Float(*i as f64),
(ColumnType::Integer, Value::Float(f))
if f.is_finite()
&& f.fract() == 0.0
&& *f < 9223372036854775808.0
&& *f > -9223372036854775809.0 =>
{
Value::Integer(*f as i64)
}
(ColumnType::Timestamp, Value::Float(f))
if f.is_finite()
&& f.fract() == 0.0
&& *f < 9223372036854775808.0
&& *f > -9223372036854775809.0 =>
{
use crate::types::Timestamp;
Value::Timestamp(Timestamp::from_micros(*f as i64))
}
_ => value,
};
row.push(coerced_value);
}
Ok(row)
}
pub fn rows_to_sql_rows(rows: Vec<(u64, Row)>, schema: &TableSchema) -> Result<Vec<(u64, SqlRow)>> {
let col_names: Vec<&String> = schema.columns.iter().map(|c| &c.name).collect();
rows.into_iter()
.map(|(row_id, row)| {
let mut sql_row = SqlRow::with_capacity(col_names.len());
for (i, name) in col_names.iter().enumerate() {
let value = row.get(i).cloned().unwrap_or(Value::Null);
sql_row.insert((*name).clone(), value);
}
Ok((row_id, sql_row))
})
.collect()
}
pub fn values_to_row_by_columns(
values: &[Value],
columns: &[String],
schema: &TableSchema,
) -> Result<Row> {
let mut row: Vec<Value> = schema
.columns
.iter()
.map(|c| c.default_value.clone().unwrap_or(Value::Null))
.collect();
for (i, col_name) in columns.iter().enumerate() {
let val = values.get(i).cloned().unwrap_or(Value::Null);
if let Some(col_def) = schema.get_column(col_name) {
if col_def.auto_increment {
continue;
}
if !col_def.nullable && matches!(val, Value::Null) && col_def.default_value.is_none() {
return Err(crate::error::MoteDBError::InvalidArgument(format!(
"Column '{}' cannot be null",
col_name
)));
}
let coerced = match (&col_def.col_type, &val) {
(ColumnType::Timestamp, Value::Integer(ts)) => {
Value::Timestamp(crate::types::Timestamp::from_micros(*ts))
}
(ColumnType::Float, Value::Integer(i)) => Value::Float(*i as f64),
(ColumnType::Integer, Value::Float(f))
if f.is_finite()
&& f.fract() == 0.0
&& *f < 9223372036854775808.0
&& *f > -9223372036854775809.0 =>
{
Value::Integer(*f as i64)
}
(ColumnType::Timestamp, Value::Float(f))
if f.is_finite()
&& f.fract() == 0.0
&& *f < 9223372036854775808.0
&& *f > -9223372036854775809.0 =>
{
Value::Timestamp(crate::types::Timestamp::from_micros(*f as i64))
}
_ => val,
};
row[col_def.position] = coerced;
}
}
Ok(row)
}
pub fn values_to_row_schema_order(values: &[Value], schema: &TableSchema) -> Result<Row> {
let mut row = Vec::with_capacity(schema.columns.len());
for (i, col_def) in schema.columns.iter().enumerate() {
if col_def.auto_increment {
let val = values.get(i).cloned().unwrap_or(Value::Null);
if !matches!(val, Value::Null) {
row.push(val);
} else {
row.push(Value::Null);
}
continue;
}
let val = values.get(i).cloned().unwrap_or(Value::Null);
if !col_def.nullable && matches!(val, Value::Null) {
return Err(crate::error::MoteDBError::InvalidArgument(format!(
"Column '{}' cannot be null",
col_def.name
)));
}
let coerced = match (&col_def.col_type, &val) {
(ColumnType::Timestamp, Value::Integer(ts)) => {
Value::Timestamp(crate::types::Timestamp::from_micros(*ts))
}
(ColumnType::Timestamp, Value::Text(s)) => {
match parse_datetime(s.as_str()) {
Some(ts) => ts,
None => {
return Err(crate::error::MoteDBError::InvalidArgument(format!(
"Cannot parse '{}' as TIMESTAMP (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)", s
)));
}
}
}
(ColumnType::Float, Value::Integer(i)) => Value::Float(*i as f64),
_ => val,
};
row.push(coerced);
}
Ok(row)
}
fn parse_datetime(s: &str) -> Option<Value> {
use crate::types::Timestamp;
if let Ok(micros) = s.parse::<i64>() {
return Some(Value::Timestamp(Timestamp::from_micros(micros)));
}
let (date_part, time_part) = if let Some(idx) = s.find(['T', ' ']) {
(&s[..idx], Some(&s[idx + 1..]))
} else {
(s, None)
};
let dparts: Vec<&str> = date_part.split('-').collect();
if dparts.len() != 3 {
return None;
}
let year: i32 = dparts[0].parse().ok()?;
let month: u32 = dparts[1].parse().ok()?;
let day: u32 = dparts[2].parse().ok()?;
if month < 1 || month > 12 || day < 1 || day > 31 {
return None;
}
let (hour, min, sec) = if let Some(tp) = time_part {
let tparts: Vec<&str> = tp.split(':').collect();
let h: u32 = tparts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let m: u32 = tparts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let s: u32 = tparts
.get(2)
.and_then(|s| s.split('.').next().and_then(|n| n.parse().ok()))
.unwrap_or(0);
(h, m, s)
} else {
(0, 0, 0)
};
let days = days_from_civil(year, month, day)?;
let micros = days as i64 * 86_400_000_000
+ hour as i64 * 3_600_000_000
+ min as i64 * 60_000_000
+ sec as i64 * 1_000_000;
Some(Value::Timestamp(Timestamp::from_micros(micros)))
}
fn days_from_civil(y: i32, m: u32, d: u32) -> Option<i64> {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let m = m as u64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d as u64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
Some(era as i64 * 146097 + doe as i64 - 719468)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ArcString, ColumnDef, ColumnType};
use std::sync::Arc;
#[test]
fn test_row_to_sql_row() {
let schema = TableSchema::new(
"users".to_string(),
vec![
ColumnDef::new("id".to_string(), ColumnType::Integer, 0),
ColumnDef::new("name".to_string(), ColumnType::Text, 1),
],
);
let row = vec![
Value::Integer(1),
Value::Text(ArcString(Arc::from("Alice"))),
];
let sql_row = row_to_sql_row(&row, &schema).unwrap();
assert_eq!(sql_row.get("id"), Some(&Value::Integer(1)));
assert_eq!(
sql_row.get("name"),
Some(&Value::Text(ArcString(Arc::from("Alice"))))
);
}
#[test]
fn test_sql_row_to_row() {
let schema = TableSchema::new(
"users".to_string(),
vec![
ColumnDef::new("id".to_string(), ColumnType::Integer, 0),
ColumnDef::new("name".to_string(), ColumnType::Text, 1),
],
);
let mut sql_row = SqlRow::new();
sql_row.insert("id".to_string(), Value::Integer(1));
sql_row.insert(
"name".to_string(),
Value::Text(ArcString(Arc::from("Alice"))),
);
let row = sql_row_to_row(&sql_row, &schema).unwrap();
assert_eq!(row.len(), 2);
assert_eq!(row[0], Value::Integer(1));
assert_eq!(row[1], Value::Text(ArcString(Arc::from("Alice"))));
}
#[test]
fn test_round_trip() {
let schema = TableSchema::new(
"users".to_string(),
vec![
ColumnDef::new("id".to_string(), ColumnType::Integer, 0),
ColumnDef::new("name".to_string(), ColumnType::Text, 1),
ColumnDef::new("age".to_string(), ColumnType::Integer, 2),
],
);
let original_row = vec![
Value::Integer(42),
Value::Text(ArcString(Arc::from("Bob"))),
Value::Integer(30),
];
let sql_row = row_to_sql_row(&original_row, &schema).unwrap();
let converted_row = sql_row_to_row(&sql_row, &schema).unwrap();
assert_eq!(original_row, converted_row);
}
}