diff --git a/Cargo.lock b/Cargo.lock index 63e5d18c1..e4e0aeb83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,6 +177,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "bytecount" version = "0.6.3" @@ -629,6 +638,30 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "half" version = "1.8.2" @@ -718,6 +751,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indenter" version = "0.3.3" @@ -1064,9 +1115,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69486e2b8c2d2aeb9762db7b4e00b0331156393555cff467f4163ff06821eef8" +checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a" dependencies = [ "thiserror", "ucd-trie", @@ -1074,9 +1125,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13570633aff33c6d22ce47dd566b10a3b9122c2fe9d8e7501895905be532b91" +checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63" dependencies = [ "pest", "pest_generator", @@ -1084,9 +1135,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c567e5702efdc79fb18859ea74c3eb36e14c43da7b8c1f098a4ed6514ec7a0" +checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1" dependencies = [ "pest", "pest_meta", @@ -1097,13 +1148,13 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb32be5ee3bbdafa8c7a18b0a8a8d962b66cfa2ceee4037f49267a50ee821fe" +checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662" dependencies = [ "once_cell", "pest", - "sha-1", + "sha1", ] [[package]] @@ -1688,10 +1739,10 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549" dependencies = [ "cfg-if", "cpufeatures", @@ -1895,6 +1946,22 @@ dependencies = [ "serde", ] +[[package]] +name = "tera" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df578c295f9ec044ff1c829daf31bb7581d5b3c2a7a3d87419afe1f2531438c" +dependencies = [ + "globwalk", + "lazy_static", + "pest", + "pest_derive", + "regex", + "serde", + "serde_json", + "unic-segment", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -1925,18 +1992,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -2012,6 +2079,7 @@ dependencies = [ "statrs", "stats_agg", "tdigest", + "tera", "time_weighted_average", "tspoint", "twofloat", @@ -2208,9 +2276,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uddsketch" @@ -2229,6 +2297,56 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.6.0" diff --git a/extension/Cargo.toml b/extension/Cargo.toml index bfebfd420..b8cb0aed1 100644 --- a/extension/Cargo.toml +++ b/extension/Cargo.toml @@ -43,6 +43,7 @@ rand = { version = "0.8.3", features = ["getrandom", "small_rng"] } rand_distr = "0.4.0" rand_chacha = "0.3.0" ron="0.6.0" +tera = { version = "1.17.0", default-features = false } twofloat = { version = "0.6.0", features = ["serde"] } num-traits = "0.2.15" diff --git a/extension/src/time_vector.rs b/extension/src/time_vector.rs index 459253387..912082719 100644 --- a/extension/src/time_vector.rs +++ b/extension/src/time_vector.rs @@ -1,6 +1,10 @@ #![allow(clippy::identity_op)] // clippy gets confused by pg_type! enums +use crate::pg_sys::timestamptz_to_str; +use core::str::Utf8Error; use pgx::{iter::TableIterator, *}; +use std::ffi::CStr; +use tera::{Context, Tera}; use crate::{ aggregate_utils::in_aggregate_context, @@ -115,6 +119,48 @@ pub fn unnest<'a>( ) } +/// Util function to convert from *const ::std::os::raw::c_char to String +/// TimestampTz -> *const c_char -> &CStr -> &str -> String +pub fn timestamptz_to_string(time: pg_sys::TimestampTz) -> Result { + let char_ptr = unsafe { timestamptz_to_str(time) }; + let c_str = unsafe { CStr::from_ptr(char_ptr) }; + c_str.to_str().map(|s| s.to_owned()) +} + +#[pg_extern(immutable, schema = "toolkit_experimental", parallel_safe)] +pub fn format_timevector<'a>(series: Timevector_TSTZ_F64<'a>, format_string: String) -> String { + let mut context = Context::new(); + let mut times: Vec = Vec::new(); + let mut values: Vec = Vec::new(); + if series.has_nulls() { + for (i, point) in series.iter().enumerate() { + times.push(timestamptz_to_string(point.ts).unwrap()); + if series.is_null_val(i) { + values.push("null".to_string()) + } else { + match point.val.to_string().as_ref() { + "NaN" | "Inf" => panic!(), + x => values.push(x.to_string()), + } + } + } + } else { + // optimized path if series does not have any nulls + for point in series { + times.push(timestamptz_to_string(point.ts).unwrap()); + values.push(point.val.to_string()); + } + } + context.insert("TIMES", ×); + context.insert("VALUES", &values); + match format_string.as_ref() { + "plotly" => Tera::one_off("{\"times\": {{ TIMES | json_encode(pretty=true) | safe }}, \"vals\": {{ VALUES | json_encode(pretty=true) | safe }}}", &context, true).expect("Failed to create plotly template"), + "paired" => { + Tera::one_off( "[{% for x in TIMES %}{\"time\": \"{{ x }}\", \"val\": {{ VALUES[loop.index0] }}}, {% endfor %}]", &context, true).expect("Failed to create paired template")}, + format_string => Tera::one_off(&format_string, &context, true).expect("Failed to create template with Tera") + } +} + #[pg_operator(immutable, parallel_safe)] #[opname(->)] pub fn arrow_timevector_unnest<'a>( @@ -424,6 +470,52 @@ mod tests { }) } + #[pg_test] + pub fn test_format_timevector() { + Spi::execute(|client| { + client.select("SET timezone TO 'UTC'", None, None); + client.select( + "CREATE TABLE data(time TIMESTAMPTZ, value DOUBLE PRECISION)", + None, + None, + ); + client.select( + r#"INSERT INTO data VALUES + ('2020-1-1', 30.0), + ('2020-1-2', 45.0), + ('2020-1-3', NULL), + ('2020-1-4', 55.5), + ('2020-1-5', 10.0)"#, + None, + None, + ); + + let test_plotly_template = client.select( + // "SELECT toolkit_experimental.format_timevector(timevector(time, value),'{\"times\": {{ TIMES }}, \"vals\": {{ VALUES }}}') FROM data", + "SELECT toolkit_experimental.format_timevector(timevector(time, value),'plotly') FROM data", + None, + None, + ).first() + .get_one::() + .unwrap(); + + assert_eq!(test_plotly_template,"{\"times\": [\n \"2020-01-01 00:00:00+00\",\n \"2020-01-02 00:00:00+00\",\n \"2020-01-03 00:00:00+00\",\n \"2020-01-04 00:00:00+00\",\n \"2020-01-05 00:00:00+00\"\n], \"vals\": [\n \"30\",\n \"45\",\n \"null\",\n \"55.5\",\n \"10\"\n]}" + ); + + let test_paired_template = client.select( + "SELECT toolkit_experimental.format_timevector(timevector(time, value),'paired') FROM data", + None, + None, + ).first() + .get_one::() + .unwrap(); + + assert_eq!( + test_paired_template, + "[{\"time\": \"2020-01-01 00:00:00+00\", \"val\": 30}, {\"time\": \"2020-01-02 00:00:00+00\", \"val\": 45}, {\"time\": \"2020-01-03 00:00:00+00\", \"val\": null}, {\"time\": \"2020-01-04 00:00:00+00\", \"val\": 55.5}, {\"time\": \"2020-01-05 00:00:00+00\", \"val\": 10}, ]" + ); + }) + } #[pg_test] pub fn timevector_io() { Spi::execute(|client| {