diff --git a/runtimes/core/src/api/pvalue.rs b/runtimes/core/src/api/pvalue.rs index fc36a386e8..5f5a33b55c 100644 --- a/runtimes/core/src/api/pvalue.rs +++ b/runtimes/core/src/api/pvalue.rs @@ -100,3 +100,18 @@ impl Display for PValue { } } } + +impl From for PValue { + fn from(value: serde_json::Value) -> Self { + match value { + serde_json::Value::Null => PValue::Null, + serde_json::Value::Bool(b) => PValue::Bool(b), + serde_json::Value::Number(n) => PValue::Number(n), + serde_json::Value::String(s) => PValue::String(s), + serde_json::Value::Array(a) => PValue::Array(a.into_iter().map(PValue::from).collect()), + serde_json::Value::Object(o) => { + PValue::Object(o.into_iter().map(|(k, v)| (k, PValue::from(v))).collect()) + } + } + } +} diff --git a/runtimes/core/src/sqldb/val.rs b/runtimes/core/src/sqldb/val.rs index 9a16b70712..303a50ec92 100644 --- a/runtimes/core/src/sqldb/val.rs +++ b/runtimes/core/src/sqldb/val.rs @@ -1,12 +1,15 @@ use anyhow::Context; -use bytes::BytesMut; +use bytes::{BufMut, BytesMut}; +use serde::Serialize; use std::error::Error; -use tokio_postgres::types::{FromSql, IsNull, Kind, ToSql, Type}; +use tokio_postgres::types::{accepts, to_sql_checked, FromSql, IsNull, Kind, ToSql, Type}; use uuid::Uuid; +use crate::api::{DateTime, PValue}; + #[derive(Debug)] pub enum RowValue { - Json(serde_json::Value), + PVal(PValue), Bytes(Vec), Uuid(uuid::Uuid), } @@ -24,27 +27,25 @@ impl ToSql for RowValue { _ => Err(format!("uuid not supported for column of type {}", ty).into()), }, - Self::Json(val) => match *ty { + Self::PVal(val) => match *ty { Type::JSON | Type::JSONB => val.to_sql(ty, out), _ => match val { - serde_json::Value::Null => Ok(IsNull::Yes), - serde_json::Value::Bool(bool) => match *ty { + PValue::Null => Ok(IsNull::Yes), + PValue::Bool(bool) => match *ty { Type::BOOL => bool.to_sql(ty, out), Type::TEXT | Type::VARCHAR => bool.to_string().to_sql(ty, out), _ => Err(format!("bool not supported for column of type {}", ty).into()), }, - serde_json::Value::String(str) => match *ty { + PValue::String(str) => match *ty { Type::TEXT | Type::VARCHAR => str.to_sql(ty, out), Type::BYTEA => { let val = str.as_bytes(); val.to_sql(ty, out) } Type::TIMESTAMP => { - let val = - chrono::NaiveDateTime::parse_from_str(str, "%Y-%m-%d %H:%M:%S") - .map_err(Box::new)?; + let val = str.parse::().map_err(Box::new)?; val.to_sql(ty, out) } Type::TIMESTAMPTZ => { @@ -69,7 +70,7 @@ impl ToSql for RowValue { _ => Err(format!("string not supported for column of type {}", ty).into()), }, - serde_json::Value::Number(num) => match *ty { + PValue::Number(num) => match *ty { Type::INT2 => { let val: Result = if num.is_i64() { num.as_i64().unwrap().try_into() @@ -179,11 +180,11 @@ impl ToSql for RowValue { } } }, - - serde_json::Value::Array(_) => { + PValue::DateTime(dt) => dt.to_sql(ty, out), + PValue::Array(_) => { Err(format!("array not supported for column of type {}", ty).into()) } - serde_json::Value::Object(_) => { + PValue::Object(_) => { Err(format!("object not supported for column of type {}", ty).into()) } }, @@ -230,7 +231,7 @@ impl<'a> FromSql<'a> for RowValue { Ok(match *ty { Type::BOOL => { let val: bool = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::Bool(val)) + Self::PVal(PValue::Bool(val)) } Type::BYTEA => { let val: Vec = FromSql::from_sql(ty, raw)?; @@ -238,32 +239,32 @@ impl<'a> FromSql<'a> for RowValue { } Type::TEXT | Type::VARCHAR => { let val: String = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::String(val)) + Self::PVal(PValue::String(val)) } Type::INT2 => { let val: i16 = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::Number(serde_json::Number::from(val))) + Self::PVal(PValue::Number(serde_json::Number::from(val))) } Type::INT4 => { let val: i32 = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::Number(serde_json::Number::from(val))) + Self::PVal(PValue::Number(serde_json::Number::from(val))) } Type::INT8 => { let val: i64 = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::Number(serde_json::Number::from(val))) + Self::PVal(PValue::Number(serde_json::Number::from(val))) } Type::OID => { let val: u32 = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::Number(serde_json::Number::from(val))) + Self::PVal(PValue::Number(serde_json::Number::from(val))) } Type::JSON | Type::JSONB => { - let val: serde_json::Value = FromSql::from_sql(ty, raw)?; - Self::Json(val) + let val: PValue = FromSql::from_sql(ty, raw)?; + Self::PVal(val) } Type::JSON_ARRAY | Type::JSONB_ARRAY => { - let val: Vec = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::Array(val)) + let val: Vec = FromSql::from_sql(ty, raw)?; + Self::PVal(PValue::Array(val)) } Type::UUID => { let val: uuid::Uuid = FromSql::from_sql(ty, raw)?; @@ -273,40 +274,40 @@ impl<'a> FromSql<'a> for RowValue { Type::FLOAT4 => { let val: f32 = FromSql::from_sql(ty, raw)?; match serde_json::Number::from_f64(val as f64) { - Some(num) => Self::Json(serde_json::Value::Number(num)), - None => Self::Json(serde_json::Value::Null), + Some(num) => Self::PVal(PValue::Number(num)), + None => Self::PVal(PValue::Null), } } Type::FLOAT8 => { let val: f64 = FromSql::from_sql(ty, raw)?; match serde_json::Number::from_f64(val) { - Some(num) => Self::Json(serde_json::Value::Number(num)), - None => Self::Json(serde_json::Value::Null), + Some(num) => Self::PVal(PValue::Number(num)), + None => Self::PVal(PValue::Null), } } Type::TIMESTAMP => { - let val: chrono::NaiveDateTime = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::String(val.to_string())) + let val: DateTime = FromSql::from_sql(ty, raw)?; + Self::PVal(PValue::DateTime(val)) } Type::TIMESTAMPTZ => { - let val: chrono::DateTime = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::String(val.to_string())) + let val: DateTime = FromSql::from_sql(ty, raw)?; + Self::PVal(PValue::DateTime(val)) } Type::DATE => { let val: chrono::NaiveDate = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::String(val.to_string())) + Self::PVal(PValue::String(val.to_string())) } Type::TIME => { let val: chrono::NaiveTime = FromSql::from_sql(ty, raw)?; - Self::Json(serde_json::Value::String(val.to_string())) + Self::PVal(PValue::String(val.to_string())) } _ => { if let Kind::Enum(_) = ty.kind() { let val = std::str::from_utf8(raw)?; - Self::Json(serde_json::Value::String(val.to_string())) + Self::PVal(PValue::String(val.to_string())) } else { return Err(format!("unsupported type: {:?}", ty).into()); } @@ -315,7 +316,7 @@ impl<'a> FromSql<'a> for RowValue { } fn from_sql_null(_: &Type) -> Result> { - Ok(Self::Json(serde_json::Value::Null)) + Ok(Self::PVal(PValue::Null)) } fn accepts(ty: &Type) -> bool { @@ -341,3 +342,31 @@ impl<'a> FromSql<'a> for RowValue { } } } + +impl<'a> FromSql<'a> for PValue { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + let val: serde_json::Value = FromSql::from_sql(ty, raw)?; + Ok(val.into()) + } + + accepts!(JSON, JSONB); +} + +impl ToSql for PValue { + fn to_sql( + &self, + ty: &Type, + out: &mut BytesMut, + ) -> Result> { + if *ty == Type::JSONB { + out.put_u8(1); + } + + let mut ser = serde_json::ser::Serializer::new(out.writer()); + self.serialize(&mut ser)?; + Ok(IsNull::No) + } + + accepts!(JSON, JSONB); + to_sql_checked!(); +} diff --git a/runtimes/js/encore.dev/storage/sqldb/database.ts b/runtimes/js/encore.dev/storage/sqldb/database.ts index e9abccd084..2b3f2223b6 100644 --- a/runtimes/js/encore.dev/storage/sqldb/database.ts +++ b/runtimes/js/encore.dev/storage/sqldb/database.ts @@ -19,7 +19,7 @@ const driverName = "node-pg"; export type Row = Record; /** Represents a type that can be used in query template literals */ -export type Primitive = string | number | boolean | Buffer | null; +export type Primitive = string | number | boolean | Buffer | Date | null; /** * Constructing a new database object will result in Encore provisioning a database with diff --git a/runtimes/js/src/sqldb.rs b/runtimes/js/src/sqldb.rs index 6db4190162..5c74e833c0 100644 --- a/runtimes/js/src/sqldb.rs +++ b/runtimes/js/src/sqldb.rs @@ -1,4 +1,5 @@ use crate::api::Request; +use crate::pvalue::{parse_pvalue, pvalue_to_js}; use encore_runtime_core::sqldb; use mappable_rc::Marc; use napi::{Env, JsUnknown}; @@ -21,76 +22,26 @@ pub struct QueryArgs { #[napi] impl QueryArgs { #[napi(constructor)] - pub fn new(env: Env, params: Vec) -> napi::Result { - let values = convert_row_values(env, params)?; + pub fn new(params: Vec) -> napi::Result { + let values = convert_row_values(params)?; Ok(Self { values: std::sync::Mutex::new(values), }) } } -fn convert_row_values(env: Env, params: Vec) -> napi::Result> { - use napi::{JsBuffer, ValueType}; +fn convert_row_values(params: Vec) -> napi::Result> { + use napi::JsBuffer; params .into_iter() .map(|val| -> napi::Result { - Ok(match val.get_type()? { - ValueType::Null => sqldb::RowValue::Json(serde_json::Value::Null), - ValueType::Number => { - let float = val.coerce_to_number()?.get_double()?; - let int = float as i64; - if float == int as f64 { - sqldb::RowValue::Json(serde_json::Value::Number(int.into())) - } else { - match serde_json::Number::from_f64(float) { - Some(n) => sqldb::RowValue::Json(serde_json::Value::Number(n)), - None => { - return Err(napi::Error::new( - napi::Status::GenericFailure, - "failed to convert float to json number".to_string(), - )); - } - } - } - } - ValueType::Boolean => { - let b = val.coerce_to_bool()?.get_value()?; - sqldb::RowValue::Json(serde_json::Value::Bool(b)) - } - ValueType::String => { - let s = val.coerce_to_string()?.into_utf8()?.into_owned()?; - sqldb::RowValue::Json(serde_json::Value::String(s)) - } - ValueType::Object => { - // Is this a buffer? - if val.is_buffer()? { - let buf: JsBuffer = val.try_into()?; - let buf = buf.into_value()?; - sqldb::RowValue::Bytes(buf.to_vec()) - } else { - let val: serde_json::Value = env.from_js_value(val)?; - sqldb::RowValue::Json(val) - } - } - ValueType::Unknown => { - return Err(napi::Error::new( - napi::Status::GenericFailure, - "unknown not yet supported".to_string(), - )); - } - ValueType::BigInt => { - return Err(napi::Error::new( - napi::Status::GenericFailure, - "unsupported value type".to_string(), - )); - } - _ => { - return Err(napi::Error::new( - napi::Status::GenericFailure, - "unsupported value type".to_string(), - )); - } - }) + if val.is_buffer()? { + let buf: JsBuffer = val.try_into()?; + let buf = buf.into_value()?; + return Ok(sqldb::RowValue::Bytes(buf.to_vec())); + } + let pval = parse_pvalue(val)?; + Ok(sqldb::RowValue::PVal(pval)) }) .collect() } @@ -187,7 +138,7 @@ impl Row { let mut map = HashMap::with_capacity(vals.len()); for (key, val) in vals { let val: JsUnknown = match val { - sqldb::RowValue::Json(val) => env.to_js_value(&val)?, + sqldb::RowValue::PVal(val) => pvalue_to_js(env, &val)?, sqldb::RowValue::Bytes(val) => { env.create_arraybuffer_with_data(val)?.into_unknown() }