From 1a4e399721690d2a82f7e4f02c6157ddb284daa2 Mon Sep 17 00:00:00 2001 From: Matthew Badger Date: Sat, 9 Aug 2025 15:32:35 +0100 Subject: [PATCH 1/2] WIP: enable time fields with non-standard names in derive macro --- influxdb/tests/derive_integration_tests.rs | 33 ++++++++- influxdb_derive/src/writeable.rs | 79 ++++++++++++++++------ 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/influxdb/tests/derive_integration_tests.rs b/influxdb/tests/derive_integration_tests.rs index 1601ce7..a2e64e3 100644 --- a/influxdb/tests/derive_integration_tests.rs +++ b/influxdb/tests/derive_integration_tests.rs @@ -4,7 +4,7 @@ mod utilities; #[cfg(feature = "derive")] use influxdb::InfluxDbWriteable; -use chrono::{DateTime, Utc}; +use chrono::{Date, DateTime, Utc}; use influxdb::{Query, ReadQuery, Timestamp}; #[cfg(feature = "serde")] @@ -23,6 +23,20 @@ struct WeatherReading { wind_strength: Option, } +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "derive", derive(InfluxDbWriteable))] +struct WeatherReadingWithNonstandardTime { + #[influxdb(time)] + reading_time: DateTime, + #[influxdb(ignore)] + time: DateTime, + #[influxdb(ignore)] + humidity: i32, + pressure: i32, + #[influxdb(tag)] + wind_strength: Option, +} + #[derive(Debug)] #[cfg_attr(feature = "serde", derive(Deserialize))] struct WeatherReadingWithoutIgnored { @@ -47,6 +61,23 @@ fn test_build_query() { ); } +#[test] +fn test_build_nonstandard_query() { + let weather_reading = WeatherReadingWithNonstandardTime { + reading_time: Timestamp::Hours(1).into(), + time: Timestamp::Hours(1).into(), + humidity: 30, + pressure: 100, + wind_strength: Some(5), + }; + let query = weather_reading.into_query("weather_reading"); + let query = query.build().unwrap(); + assert_eq!( + query.get(), + "weather_reading,wind_strength=5 pressure=100i 3600000000000" + ); +} + #[cfg(feature = "derive")] /// INTEGRATION TEST /// diff --git a/influxdb_derive/src/writeable.rs b/influxdb_derive/src/writeable.rs index 870f947..101f0b2 100644 --- a/influxdb_derive/src/writeable.rs +++ b/influxdb_derive/src/writeable.rs @@ -10,6 +10,7 @@ use syn::{ #[derive(Debug)] struct WriteableField { ident: Ident, + is_time: bool, is_tag: bool, is_ignore: bool, } @@ -17,11 +18,13 @@ struct WriteableField { mod kw { use syn::custom_keyword; + custom_keyword!(time); custom_keyword!(tag); custom_keyword!(ignore); } enum FieldAttr { + Time(kw::time), Tag(kw::tag), Ignore(kw::ignore), } @@ -29,7 +32,9 @@ enum FieldAttr { impl Parse for FieldAttr { fn parse(input: ParseStream<'_>) -> syn::Result { let lookahead = input.lookahead1(); - if lookahead.peek(kw::tag) { + if lookahead.peek(kw::time) { + Ok(Self::Time(input.parse()?)) + } else if lookahead.peek(kw::tag) { Ok(Self::Tag(input.parse()?)) } else if lookahead.peek(kw::ignore) { Ok(Self::Ignore(input.parse()?)) @@ -52,6 +57,7 @@ impl TryFrom for WriteableField { fn try_from(field: Field) -> syn::Result { let ident = field.ident.expect("fields without ident are not supported"); + let mut has_time_attr = false; let mut is_tag = false; let mut is_ignore = false; @@ -60,6 +66,7 @@ impl TryFrom for WriteableField { Meta::List(list) if list.path.is_ident("influxdb") => { for attr in syn::parse2::(list.tokens)?.0 { match attr { + FieldAttr::Time(_) => has_time_attr = true, FieldAttr::Tag(_) => is_tag = true, FieldAttr::Ignore(_) => is_ignore = true, } @@ -69,8 +76,23 @@ impl TryFrom for WriteableField { } } + if [has_time_attr, is_tag, is_ignore] + .iter() + .filter(|&&b| b) + .count() + > 1 + { + panic!("only one of time, tag, or ignore can be used"); + } + + // A field is considered a time field if: + // 1. It has the #[influxdb(time)] attribute, OR + // 2. It's named "time" and doesn't have #[influxdb(ignore)] + let is_time = has_time_attr || (ident == "time" && !is_ignore); + Ok(WriteableField { ident, + is_time, is_tag, is_ignore, }) @@ -97,39 +119,52 @@ pub fn expand_writeable(input: DeriveInput) -> syn::Result { } }; - let time_field = format_ident!("time"); - let time_field_str = time_field.to_string(); - #[allow(clippy::cmp_owned)] // that's not how idents work clippy - let fields = match fields { + let writeable_fields: Vec = match fields { Fields::Named(fields) => fields .named .into_iter() - .filter_map(|f| { - WriteableField::try_from(f) - .map(|wf| { - if !wf.is_ignore && wf.ident.to_string() != time_field_str { - let ident = wf.ident; - Some(match wf.is_tag { - true => quote!(query.add_tag(stringify!(#ident), self.#ident)), - false => quote!(query.add_field(stringify!(#ident), self.#ident)), - }) - } else { - None - } - }) - .transpose() - }) + .map(WriteableField::try_from) .collect::>>()?, - _ => panic!("a struct without named fields is not supported"), + _ => panic!("A struct without named fields is not supported!"), }; + // Find the time field + let mut time_field = None; + for wf in &writeable_fields { + if wf.is_time { + if time_field.is_some() { + panic!("multiple time fields found!"); + } + time_field = Some(wf.ident.clone()); + } + } + + // There must be exactly one time field + let time_field = time_field.expect("no time field found"); + + // Generate field assignments (excluding time and ignored fields) + let field_assignments = writeable_fields + .into_iter() + .filter_map(|wf| { + if wf.is_ignore || wf.is_time { + None + } else { + let ident = wf.ident; + Some(match wf.is_tag { + true => quote!(query.add_tag(stringify!(#ident), self.#ident)), + false => quote!(query.add_field(stringify!(#ident), self.#ident)), + }) + } + }) + .collect::>(); + Ok(quote! { impl #impl_generics ::influxdb::InfluxDbWriteable for #ident #ty_generics #where_clause { fn into_query>(self, name: I) -> ::influxdb::WriteQuery { let timestamp: ::influxdb::Timestamp = self.#time_field.into(); let mut query = timestamp.into_query(name); #( - query = #fields; + query = #field_assignments; )* query } From 922c8c3b0575d0bdef02283c55b3505852cebdd5 Mon Sep 17 00:00:00 2001 From: Matthew Badger Date: Sat, 9 Aug 2025 19:11:24 +0100 Subject: [PATCH 2/2] Enable serializing DateTime --- influxdb/src/query/write_query.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/influxdb/src/query/write_query.rs b/influxdb/src/query/write_query.rs index 75d8b02..b55fee9 100644 --- a/influxdb/src/query/write_query.rs +++ b/influxdb/src/query/write_query.rs @@ -5,6 +5,7 @@ use crate::query::line_proto_term::LineProtoTerm; use crate::query::{QueryType, ValidQuery}; use crate::{Error, Query, Timestamp}; +use chrono::{DateTime, TimeZone}; use std::fmt::{Display, Formatter}; pub trait WriteType { @@ -152,6 +153,20 @@ impl From<&str> for Type { Type::Text(b.into()) } } + +impl From> for Type { + fn from(dt: DateTime) -> Self { + match dt.timestamp_nanos_opt() { + Some(nanos) => Type::SignedInteger(nanos), + None => { + // For dates before 1677-09-21, or after + // 2262-04-11, we're just going to return 0. + Type::SignedInteger(0) + } + } + } +} + impl From<&T> for Type where T: Copy + Into,