From 1a4e399721690d2a82f7e4f02c6157ddb284daa2 Mon Sep 17 00:00:00 2001 From: Matthew Badger Date: Sat, 9 Aug 2025 15:32:35 +0100 Subject: [PATCH 1/6] 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/6] 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, From 0bddfe67ff41057da1ca06a19a0d918236658840 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 22 Aug 2025 16:58:49 +0200 Subject: [PATCH 3/6] Place chrono-dependent code behind feature flag --- influxdb/src/query/write_query.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/influxdb/src/query/write_query.rs b/influxdb/src/query/write_query.rs index 76e066f..c188f6a 100644 --- a/influxdb/src/query/write_query.rs +++ b/influxdb/src/query/write_query.rs @@ -5,7 +5,6 @@ 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 { @@ -154,8 +153,9 @@ impl From<&str> for Type { } } -impl From> for Type { - fn from(dt: DateTime) -> Self { +#[cfg(feature = "chrono")] +impl From> for Type { + fn from(dt: chrono::DateTime) -> Self { match dt.timestamp_nanos_opt() { Some(nanos) => Type::SignedInteger(nanos), None => { From 2de663c1588f833834531ffabcef04a474307ef2 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 22 Aug 2025 17:01:25 +0200 Subject: [PATCH 4/6] Add impl for the time-crate counterpart --- influxdb/src/query/write_query.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/influxdb/src/query/write_query.rs b/influxdb/src/query/write_query.rs index c188f6a..148f107 100644 --- a/influxdb/src/query/write_query.rs +++ b/influxdb/src/query/write_query.rs @@ -167,6 +167,13 @@ impl From> for Type { } } +#[cfg(feature = "time")] +impl From for Type { + fn from(dt: time::UtcDateTime) -> Self { + Type::SignedInteger(dt.unix_timestamp_nanos().try_into().unwrap_or(0)) + } +} + impl From<&T> for Type where T: Copy + Into, From 230d47ca57a79c086090da54515b69862bb3df4f Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 22 Aug 2025 17:06:07 +0200 Subject: [PATCH 5/6] remove unused import --- influxdb_derive/src/writeable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/influxdb_derive/src/writeable.rs b/influxdb_derive/src/writeable.rs index 1185c1d..d66018a 100644 --- a/influxdb_derive/src/writeable.rs +++ b/influxdb_derive/src/writeable.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, From 16faa7e34364ea3dfe26f15bbcf0f952534b750c Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 22 Aug 2025 20:02:56 +0200 Subject: [PATCH 6/6] remove unused import --- influxdb/tests/derive_integration_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/influxdb/tests/derive_integration_tests.rs b/influxdb/tests/derive_integration_tests.rs index 2bcb173..751899a 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::{Date, DateTime, Utc}; +use chrono::{DateTime, Utc}; use influxdb::{Query, ReadQuery, Timestamp}; #[cfg(feature = "serde")]