diff --git a/Cargo.lock b/Cargo.lock index 89edf68..1da1393 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1586,6 +1586,7 @@ name = "rust-outbound-pg-v3" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "http 1.0.0", "spin-sdk", ] diff --git a/examples/postgres-v3/Cargo.toml b/examples/postgres-v3/Cargo.toml index df486c9..03f3145 100644 --- a/examples/postgres-v3/Cargo.toml +++ b/examples/postgres-v3/Cargo.toml @@ -13,4 +13,5 @@ anyhow = "1" http = "1.0.0" # The Spin SDK. spin-sdk = { path = "../.." } - +# For handling date/time types +chrono = "0.4.38" diff --git a/examples/postgres-v3/README.md b/examples/postgres-v3/README.md index cca7179..ac96316 100644 --- a/examples/postgres-v3/README.md +++ b/examples/postgres-v3/README.md @@ -55,3 +55,15 @@ date: Sun, 25 Sep 2022 15:46:22 GMT Count: 3 ``` + +Curl the write_datetime_info route to experiment with date time types: +``` +$ curl -i localhost:3000/write_datetime_info +HTTP/1.1 200 OK +content-length: 9 +date: Sun, 25 Sep 2022 15:46:22 GMT + +Count: 4 +``` + +Read endpoint should now also show a row with publisheddate, publishedtime, publisheddatetime and readtime values. diff --git a/examples/postgres-v3/db/testdata.sql b/examples/postgres-v3/db/testdata.sql index 2c8b667..d9d30ee 100644 --- a/examples/postgres-v3/db/testdata.sql +++ b/examples/postgres-v3/db/testdata.sql @@ -3,11 +3,14 @@ CREATE TABLE articletest ( title varchar(40) NOT NULL, content text NOT NULL, authorname varchar(40) NOT NULL , - published date NOT NULL, + publisheddate date NOT NULL, + publishedtime time, + publisheddatetime timestamp, + readtime bigint, coauthor text ); -INSERT INTO articletest (title, content, authorname, published) VALUES +INSERT INTO articletest (title, content, authorname, publisheddate) VALUES ( 'My Life as a Goat', 'I went to Nepal to live as a goat, and it was much better than being a butler.', diff --git a/examples/postgres-v3/src/lib.rs b/examples/postgres-v3/src/lib.rs index 0f9cb87..ae8c6f4 100644 --- a/examples/postgres-v3/src/lib.rs +++ b/examples/postgres-v3/src/lib.rs @@ -1,10 +1,7 @@ #![allow(dead_code)] use anyhow::Result; use http::{Request, Response}; -use spin_sdk::{ - http_component, pg3, - pg3::{Date, Decode}, -}; +use spin_sdk::{http_component, pg3, pg3::Decode}; // The environment variable set in `spin.toml` that points to the // address of the Pg server that the component will write to @@ -16,7 +13,10 @@ struct Article { title: String, content: String, authorname: String, - published: Date, + published_date: chrono::NaiveDate, + published_time: Option, + published_datetime: Option, + read_time: Option, coauthor: Option, } @@ -28,15 +28,21 @@ impl TryFrom<&pg3::Row> for Article { let title = String::decode(&row[1])?; let content = String::decode(&row[2])?; let authorname = String::decode(&row[3])?; - let published = Date::decode(&row[4])?; - let coauthor = Option::::decode(&row[5])?; + let published_date = chrono::NaiveDate::decode(&row[4])?; + let published_time = Option::::decode(&row[5])?; + let published_datetime = Option::::decode(&row[6])?; + let read_time = Option::::decode(&row[7])?; + let coauthor = Option::::decode(&row[8])?; Ok(Self { id, title, content, authorname, - published, + published_date, + published_time, + published_datetime, + read_time, coauthor, }) } @@ -47,6 +53,7 @@ fn process(req: Request<()>) -> Result> { match req.uri().path() { "/read" => read(req), "/write" => write(req), + "/write_datetime_info" => write_datetime_info(req), "/pg_backend_pid" => pg_backend_pid(req), _ => Ok(http::Response::builder() .status(404) @@ -58,7 +65,7 @@ fn read(_req: Request<()>) -> Result> { let address = std::env::var(DB_URL_ENV)?; let conn = pg3::Connection::open(&address)?; - let sql = "SELECT id, title, content, authorname, published, coauthor FROM articletest"; + let sql = "SELECT id, title, content, authorname, publisheddate, publishedtime, publisheddatetime, readtime, coauthor FROM articletest"; let rowset = conn.query(sql, &[])?; let column_summary = rowset @@ -89,6 +96,31 @@ fn read(_req: Request<()>) -> Result> { Ok(http::Response::builder().status(200).body(response)?) } +fn write_datetime_info(_req: Request<()>) -> Result> { + let address = std::env::var(DB_URL_ENV)?; + let conn = pg3::Connection::open(&address)?; + + let date: chrono::NaiveDate = chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(); + let time: chrono::NaiveTime = chrono::NaiveTime::from_hms_nano_opt(12, 34, 56, 1).unwrap(); + let datetime: chrono::NaiveDateTime = chrono::NaiveDateTime::new(date, time); + let readtime = 123i64; + + let nrow_executed = conn.execute( + "INSERT INTO articletest(title, content, authorname, publisheddate, publishedtime, publisheddatetime, readtime) VALUES ($1, $2, $3, $4, $5, $6, $7)", + &[ "aaa".to_string().into(), "bbb".to_string().into(), "ccc".to_string().into(), date.into(), time.into(), datetime.into(), readtime.into() ], + ); + + println!("nrow_executed: {:?}", nrow_executed); + + let sql = "SELECT COUNT(id) FROM articletest"; + let rowset = conn.query(sql, &[])?; + let row = &rowset.rows[0]; + let count = i64::decode(&row[0])?; + let response = format!("Count: {}\n", count); + + Ok(http::Response::builder().status(200).body(response)?) +} + fn write(_req: Request<()>) -> Result> { let address = std::env::var(DB_URL_ENV)?; let conn = pg3::Connection::open(&address)?; diff --git a/src/pg3.rs b/src/pg3.rs index a416488..ef27df2 100644 --- a/src/pg3.rs +++ b/src/pg3.rs @@ -2,24 +2,26 @@ //! //! # Types //! -//! | Rust type | WIT (db-value) | Postgres type(s) | -//! |------------|-----------------------------------------------|----------------------------- | -//! | `bool` | boolean(bool) | BOOL | -//! | `i16` | int16(s16) | SMALLINT, SMALLSERIAL, INT2 | -//! | `i32` | int32(s32) | INT, SERIAL, INT4 | -//! | `i64` | int64(s64) | BIGINT, BIGSERIAL, INT8 | -//! | `f32` | floating32(float32) | REAL, FLOAT4 | -//! | `f64` | floating64(float64) | DOUBLE PRECISION, FLOAT8 | -//! | `String` | str(string) | VARCHAR, CHAR(N), TEXT | -//! | `Vec` | binary(list\) | BYTEA | -//! | `Date` | date(tuple) | DATE | -//! | `Time` | time(tuple) | TIME | -//! | `Datetime` | datetime(tuple) | TIMESTAMP | -//! | `Timestamp`| timestamp(s64) | BIGINT | +//! | Rust type | WIT (db-value) | Postgres type(s) | +//! |-------------------------|-----------------------------------------------|----------------------------- | +//! | `bool` | boolean(bool) | BOOL | +//! | `i16` | int16(s16) | SMALLINT, SMALLSERIAL, INT2 | +//! | `i32` | int32(s32) | INT, SERIAL, INT4 | +//! | `i64` | int64(s64) | BIGINT, BIGSERIAL, INT8 | +//! | `f32` | floating32(float32) | REAL, FLOAT4 | +//! | `f64` | floating64(float64) | DOUBLE PRECISION, FLOAT8 | +//! | `String` | str(string) | VARCHAR, CHAR(N), TEXT | +//! | `Vec` | binary(list\) | BYTEA | +//! | `chrono::NaiveDate` | date(tuple) | DATE | +//! | `chrono::NaiveTime` | time(tuple) | TIME | +//! | `chrono::NaiveDateTime` | datetime(tuple) | TIMESTAMP | +//! | `chrono::Duration` | timestamp(s64) | BIGINT | #[doc(inline)] pub use super::wit::pg3::{Error as PgError, *}; +use chrono::{Datelike, Timelike}; + /// A pg error #[derive(Debug, thiserror::Error)] pub enum Error { @@ -124,11 +126,7 @@ impl Decode for String { } } -/// Native representation of the WIT postgres Date value. -#[derive(Clone, Debug, PartialEq)] -pub struct Date(pub chrono::NaiveDate); - -impl Decode for Date { +impl Decode for chrono::NaiveDate { fn decode(value: &DbValue) -> Result { match value { DbValue::Date((year, month, day)) => { @@ -140,18 +138,14 @@ impl Decode for Date { year, month, day )) })?; - Ok(Date(naive_date)) + Ok(naive_date) } _ => Err(Error::Decode(format_decode_err("DATE", value))), } } } -/// Native representation of the WIT postgres Time value. -#[derive(Clone, Debug, PartialEq)] -pub struct Time(pub chrono::NaiveTime); - -impl Decode for Time { +impl Decode for chrono::NaiveTime { fn decode(value: &DbValue) -> Result { match value { DbValue::Time((hour, minute, second, nanosecond)) => { @@ -167,18 +161,14 @@ impl Decode for Time { hour, minute, second, nanosecond )) })?; - Ok(Time(naive_time)) + Ok(naive_time) } _ => Err(Error::Decode(format_decode_err("TIME", value))), } } } -/// Native representation of the WIT postgres DateTime value. -#[derive(Clone, Debug, PartialEq)] -pub struct DateTime(pub chrono::NaiveDateTime); - -impl Decode for DateTime { +impl Decode for chrono::NaiveDateTime { fn decode(value: &DbValue) -> Result { match value { DbValue::Datetime((year, month, day, hour, minute, second, nanosecond)) => { @@ -203,19 +193,100 @@ impl Decode for DateTime { )) })?; let dt = chrono::NaiveDateTime::new(naive_date, naive_time); - Ok(DateTime(dt)) + Ok(dt) } _ => Err(Error::Decode(format_decode_err("DATETIME", value))), } } } +impl Decode for chrono::Duration { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Timestamp(n) => Ok(chrono::Duration::seconds(*n)), + _ => Err(Error::Decode(format_decode_err("BIGINT", value))), + } + } +} + +macro_rules! impl_parameter_value_conversions { + ($($ty:ty => $id:ident),*) => { + $( + impl From<$ty> for ParameterValue { + fn from(v: $ty) -> ParameterValue { + ParameterValue::$id(v) + } + } + )* + }; +} + +impl_parameter_value_conversions! { + i8 => Int8, + i16 => Int16, + i32 => Int32, + i64 => Int64, + f32 => Floating32, + f64 => Floating64, + bool => Boolean, + String => Str, + Vec => Binary +} + +impl From for ParameterValue { + fn from(v: chrono::NaiveDateTime) -> ParameterValue { + ParameterValue::Datetime(( + v.year(), + v.month() as u8, + v.day() as u8, + v.hour() as u8, + v.minute() as u8, + v.second() as u8, + v.nanosecond() as u32, + )) + } +} + +impl From for ParameterValue { + fn from(v: chrono::NaiveTime) -> ParameterValue { + ParameterValue::Time(( + v.hour() as u8, + v.minute() as u8, + v.second() as u8, + v.nanosecond() as u32, + )) + } +} + +impl From for ParameterValue { + fn from(v: chrono::NaiveDate) -> ParameterValue { + ParameterValue::Date((v.year(), v.month() as u8, v.day() as u8)) + } +} + +impl From for ParameterValue { + fn from(v: chrono::TimeDelta) -> ParameterValue { + ParameterValue::Timestamp(v.num_seconds()) + } +} + +impl> From> for ParameterValue { + fn from(o: Option) -> ParameterValue { + match o { + Some(v) => v.into(), + None => ParameterValue::DbNull, + } + } +} + fn format_decode_err(types: &str, value: &DbValue) -> String { format!("Expected {} from the DB but got {:?}", types, value) } #[cfg(test)] mod tests { + use chrono::NaiveDateTime; + use super::*; #[test] @@ -285,27 +356,31 @@ mod tests { #[test] fn date() { assert_eq!( - Date::decode(&DbValue::Date((1, 2, 4))).unwrap(), - Date(chrono::NaiveDate::from_ymd_opt(1, 2, 4).unwrap()) + chrono::NaiveDate::decode(&DbValue::Date((1, 2, 4))).unwrap(), + chrono::NaiveDate::from_ymd_opt(1, 2, 4).unwrap() ); assert_ne!( - Date::decode(&DbValue::Date((1, 2, 4))).unwrap(), - Date(chrono::NaiveDate::from_ymd_opt(1, 2, 5).unwrap()) + chrono::NaiveDate::decode(&DbValue::Date((1, 2, 4))).unwrap(), + chrono::NaiveDate::from_ymd_opt(1, 2, 5).unwrap() ); - assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + assert!(Option::::decode(&DbValue::DbNull) + .unwrap() + .is_none()); } #[test] fn time() { assert_eq!( - Time::decode(&DbValue::Time((1, 2, 3, 4))).unwrap(), - Time(chrono::NaiveTime::from_hms_nano_opt(1, 2, 3, 4).unwrap()) + chrono::NaiveTime::decode(&DbValue::Time((1, 2, 3, 4))).unwrap(), + chrono::NaiveTime::from_hms_nano_opt(1, 2, 3, 4).unwrap() ); assert_ne!( - Time::decode(&DbValue::Time((1, 2, 3, 4))).unwrap(), - Time(chrono::NaiveTime::from_hms_nano_opt(1, 2, 4, 5).unwrap()) + chrono::NaiveTime::decode(&DbValue::Time((1, 2, 3, 4))).unwrap(), + chrono::NaiveTime::from_hms_nano_opt(1, 2, 4, 5).unwrap() ); - assert!(Option::