Skip to content

Commit a4e41c3

Browse files
committed
Use chrono ToSql/FromSql in sqlite reader
1 parent 853c98f commit a4e41c3

2 files changed

Lines changed: 27 additions & 27 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ polars-ops = { version = "0.52", features = ["pivot"] }
3737
duckdb = { version = "1.4", features = ["bundled", "vtab-arrow"] }
3838
arrow = { version = "56", default-features = false, features = ["ipc"] }
3939
postgres = "0.19"
40-
rusqlite = { version = "0.38", features = ["bundled", "functions", "window", "series"] }
40+
rusqlite = { version = "0.38", features = ["bundled", "chrono", "functions", "window", "series"] }
4141

4242
# Writers
4343
plotters = "0.3"

src/reader/sqlite.rs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -139,33 +139,34 @@ fn anyvalue_to_sqlite(value: AnyValue, _dtype: &DataType) -> rusqlite::types::Va
139139
AnyValue::Float64(v) => Value::Real(v),
140140
AnyValue::String(s) => Value::Text(s.to_string()),
141141
AnyValue::StringOwned(s) => Value::Text(s.to_string()),
142-
AnyValue::Date(days) => {
143-
// Convert days since epoch to ISO-8601 date string
144-
match chrono::NaiveDate::from_num_days_from_ce_opt(days + 719_163) {
145-
Some(d) => Value::Text(d.format("%Y-%m-%d").to_string()),
146-
None => Value::Null,
147-
}
148-
}
149-
AnyValue::Datetime(us, _, _) => {
150-
// Convert microseconds since epoch to ISO-8601 datetime string
151-
match chrono::DateTime::from_timestamp_micros(us) {
152-
Some(d) => Value::Text(d.format("%Y-%m-%dT%H:%M:%S%.f").to_string()),
153-
None => Value::Null,
154-
}
155-
}
142+
AnyValue::Date(days) => chrono::NaiveDate::from_num_days_from_ce_opt(days + 719_163)
143+
.and_then(|d| to_sql_value(&d))
144+
.unwrap_or(Value::Null),
145+
AnyValue::Datetime(us, _, _) => chrono::DateTime::from_timestamp_micros(us)
146+
.map(|d| d.naive_utc())
147+
.and_then(|d| to_sql_value(&d))
148+
.unwrap_or(Value::Null),
156149
AnyValue::Time(ns) => {
157-
// Convert nanoseconds since midnight to time string
158150
let secs = (ns / 1_000_000_000) as u32;
159151
let nanos = (ns % 1_000_000_000) as u32;
160-
match chrono::NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos) {
161-
Some(t) => Value::Text(t.format("%H:%M:%S%.f").to_string()),
162-
None => Value::Null,
163-
}
152+
chrono::NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos)
153+
.and_then(|t| to_sql_value(&t))
154+
.unwrap_or(Value::Null)
164155
}
165156
_ => Value::Text(format!("{}", value)),
166157
}
167158
}
168159

160+
/// Use rusqlite's `ToSql` to convert a value into a `rusqlite::types::Value`.
161+
fn to_sql_value(v: &dyn rusqlite::types::ToSql) -> Option<rusqlite::types::Value> {
162+
use rusqlite::types::ToSqlOutput;
163+
match v.to_sql().ok()? {
164+
ToSqlOutput::Borrowed(vref) => Some(vref.into()),
165+
ToSqlOutput::Owned(val) => Some(val),
166+
_ => None,
167+
}
168+
}
169+
169170
impl Reader for SqliteReader {
170171
fn execute_sql(&self, sql: &str) -> Result<DataFrame> {
171172
// Handle ggsql:name namespaced identifiers (builtin datasets)
@@ -399,7 +400,7 @@ impl Reader for SqliteReader {
399400
/// Try to parse all non-null TEXT values as ISO-8601 dates (YYYY-MM-DD).
400401
/// Returns a Date series if all non-null values parse, None otherwise.
401402
fn try_parse_as_date(name: &str, values: &[rusqlite::types::Value]) -> Option<Series> {
402-
use rusqlite::types::Value;
403+
use rusqlite::types::{FromSql, Value, ValueRef};
403404

404405
// Days between 0001-01-01 (CE day 1) and 1970-01-01 (Unix epoch)
405406
const EPOCH_DAYS_FROM_CE: i32 = 719_163;
@@ -410,7 +411,8 @@ fn try_parse_as_date(name: &str, values: &[rusqlite::types::Value]) -> Option<Se
410411
match v {
411412
Value::Null => parsed.push(None),
412413
Value::Text(s) => {
413-
let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()?;
414+
let vref = ValueRef::Text(s.as_bytes());
415+
let date: chrono::NaiveDate = FromSql::column_result(vref).ok()?;
414416
parsed.push(Some(date.num_days_from_ce() - EPOCH_DAYS_FROM_CE));
415417
}
416418
_ => return None,
@@ -425,7 +427,7 @@ fn try_parse_as_date(name: &str, values: &[rusqlite::types::Value]) -> Option<Se
425427
/// Supports both "T" and space separators (e.g. "2024-01-15T10:30:00" or "2024-01-15 10:30:00").
426428
/// Returns a Datetime series if all non-null values parse, None otherwise.
427429
fn try_parse_as_datetime(name: &str, values: &[rusqlite::types::Value]) -> Option<Series> {
428-
use rusqlite::types::Value;
430+
use rusqlite::types::{FromSql, Value, ValueRef};
429431

430432
let mut parsed: Vec<Option<i64>> = Vec::with_capacity(values.len());
431433

@@ -437,10 +439,8 @@ fn try_parse_as_datetime(name: &str, values: &[rusqlite::types::Value]) -> Optio
437439
if !s.contains('T') && !s.contains(' ') {
438440
return None;
439441
}
440-
let dt = s
441-
.parse::<chrono::NaiveDateTime>()
442-
.or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
443-
.ok()?;
442+
let vref = ValueRef::Text(s.as_bytes());
443+
let dt: chrono::NaiveDateTime = FromSql::column_result(vref).ok()?;
444444
parsed.push(Some(dt.and_utc().timestamp_millis()));
445445
}
446446
_ => return None,

0 commit comments

Comments
 (0)