Skip to content

Commit c8ddae4

Browse files
badgeMatthew Badgermsrd0
authored
Renameable time field (#162)
* WIP: enable time fields with non-standard names in derive macro * Enable serializing DateTime<Utc> * Place chrono-dependent code behind feature flag * Add impl for the time-crate counterpart * remove unused import * remove unused import --------- Co-authored-by: Matthew Badger <[email protected]> Co-authored-by: Dominic <[email protected]>
1 parent a1c5f7c commit c8ddae4

File tree

3 files changed

+111
-23
lines changed

3 files changed

+111
-23
lines changed

influxdb/src/query/write_query.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,28 @@ impl From<&str> for Type {
152152
Type::Text(b.into())
153153
}
154154
}
155+
156+
#[cfg(feature = "chrono")]
157+
impl<Tz: chrono::TimeZone> From<chrono::DateTime<Tz>> for Type {
158+
fn from(dt: chrono::DateTime<Tz>) -> Self {
159+
match dt.timestamp_nanos_opt() {
160+
Some(nanos) => Type::SignedInteger(nanos),
161+
None => {
162+
// For dates before 1677-09-21, or after
163+
// 2262-04-11, we're just going to return 0.
164+
Type::SignedInteger(0)
165+
}
166+
}
167+
}
168+
}
169+
170+
#[cfg(feature = "time")]
171+
impl From<time::UtcDateTime> for Type {
172+
fn from(dt: time::UtcDateTime) -> Self {
173+
Type::SignedInteger(dt.unix_timestamp_nanos().try_into().unwrap_or(0))
174+
}
175+
}
176+
155177
impl<T> From<&T> for Type
156178
where
157179
T: Copy + Into<Type>,

influxdb/tests/derive_integration_tests.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ struct WeatherReading {
2323
wind_strength: Option<u64>,
2424
}
2525

26+
#[derive(Debug, PartialEq)]
27+
#[cfg_attr(feature = "derive", derive(InfluxDbWriteable))]
28+
struct WeatherReadingWithNonstandardTime {
29+
#[influxdb(time)]
30+
reading_time: DateTime<Utc>,
31+
#[influxdb(ignore)]
32+
time: DateTime<Utc>,
33+
#[influxdb(ignore)]
34+
humidity: i32,
35+
pressure: i32,
36+
#[influxdb(tag)]
37+
wind_strength: Option<u64>,
38+
}
39+
2640
#[derive(Debug)]
2741
#[cfg_attr(feature = "serde", derive(Deserialize))]
2842
struct WeatherReadingWithoutIgnored {
@@ -47,6 +61,23 @@ fn test_build_query() {
4761
);
4862
}
4963

64+
#[test]
65+
fn test_build_nonstandard_query() {
66+
let weather_reading = WeatherReadingWithNonstandardTime {
67+
reading_time: Timestamp::Hours(1).into(),
68+
time: Timestamp::Hours(1).into(),
69+
humidity: 30,
70+
pressure: 100,
71+
wind_strength: Some(5),
72+
};
73+
let query = weather_reading.into_query("weather_reading");
74+
let query = query.build().unwrap();
75+
assert_eq!(
76+
query.get(),
77+
"weather_reading,wind_strength=5 pressure=100i 3600000000000"
78+
);
79+
}
80+
5081
#[cfg(feature = "derive")]
5182
/// INTEGRATION TEST
5283
///

influxdb_derive/src/writeable.rs

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use proc_macro2::TokenStream;
2-
use quote::{format_ident, quote};
2+
use quote::quote;
33
use syn::{
44
parse::{Parse, ParseStream},
55
punctuated::Punctuated,
@@ -9,27 +9,32 @@ use syn::{
99
#[derive(Debug)]
1010
struct WriteableField {
1111
ident: Ident,
12+
is_time: bool,
1213
is_tag: bool,
1314
is_ignore: bool,
1415
}
1516

1617
mod kw {
1718
use syn::custom_keyword;
1819

20+
custom_keyword!(time);
1921
custom_keyword!(tag);
2022
custom_keyword!(ignore);
2123
}
2224

2325
#[allow(dead_code)] // TODO do we need to store the keywords?
2426
enum FieldAttr {
27+
Time(kw::time),
2528
Tag(kw::tag),
2629
Ignore(kw::ignore),
2730
}
2831

2932
impl Parse for FieldAttr {
3033
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
3134
let lookahead = input.lookahead1();
32-
if lookahead.peek(kw::tag) {
35+
if lookahead.peek(kw::time) {
36+
Ok(Self::Time(input.parse()?))
37+
} else if lookahead.peek(kw::tag) {
3338
Ok(Self::Tag(input.parse()?))
3439
} else if lookahead.peek(kw::ignore) {
3540
Ok(Self::Ignore(input.parse()?))
@@ -52,6 +57,7 @@ impl TryFrom<Field> for WriteableField {
5257

5358
fn try_from(field: Field) -> syn::Result<WriteableField> {
5459
let ident = field.ident.expect("fields without ident are not supported");
60+
let mut has_time_attr = false;
5561
let mut is_tag = false;
5662
let mut is_ignore = false;
5763

@@ -60,6 +66,7 @@ impl TryFrom<Field> for WriteableField {
6066
Meta::List(list) if list.path.is_ident("influxdb") => {
6167
for attr in syn::parse2::<FieldAttrs>(list.tokens)?.0 {
6268
match attr {
69+
FieldAttr::Time(_) => has_time_attr = true,
6370
FieldAttr::Tag(_) => is_tag = true,
6471
FieldAttr::Ignore(_) => is_ignore = true,
6572
}
@@ -69,8 +76,23 @@ impl TryFrom<Field> for WriteableField {
6976
}
7077
}
7178

79+
if [has_time_attr, is_tag, is_ignore]
80+
.iter()
81+
.filter(|&&b| b)
82+
.count()
83+
> 1
84+
{
85+
panic!("only one of time, tag, or ignore can be used");
86+
}
87+
88+
// A field is considered a time field if:
89+
// 1. It has the #[influxdb(time)] attribute, OR
90+
// 2. It's named "time" and doesn't have #[influxdb(ignore)]
91+
let is_time = has_time_attr || (ident == "time" && !is_ignore);
92+
7293
Ok(WriteableField {
7394
ident,
95+
is_time,
7496
is_tag,
7597
is_ignore,
7698
})
@@ -97,39 +119,52 @@ pub fn expand_writeable(input: DeriveInput) -> syn::Result<TokenStream> {
97119
}
98120
};
99121

100-
let time_field = format_ident!("time");
101-
let time_field_str = time_field.to_string();
102-
#[allow(clippy::cmp_owned)] // that's not how idents work clippy
103-
let fields = match fields {
122+
let writeable_fields: Vec<WriteableField> = match fields {
104123
Fields::Named(fields) => fields
105124
.named
106125
.into_iter()
107-
.filter_map(|f| {
108-
WriteableField::try_from(f)
109-
.map(|wf| {
110-
if !wf.is_ignore && wf.ident.to_string() != time_field_str {
111-
let ident = wf.ident;
112-
Some(match wf.is_tag {
113-
true => quote!(query.add_tag(stringify!(#ident), self.#ident)),
114-
false => quote!(query.add_field(stringify!(#ident), self.#ident)),
115-
})
116-
} else {
117-
None
118-
}
119-
})
120-
.transpose()
121-
})
126+
.map(WriteableField::try_from)
122127
.collect::<syn::Result<Vec<_>>>()?,
123-
_ => panic!("a struct without named fields is not supported"),
128+
_ => panic!("A struct without named fields is not supported!"),
124129
};
125130

131+
// Find the time field
132+
let mut time_field = None;
133+
for wf in &writeable_fields {
134+
if wf.is_time {
135+
if time_field.is_some() {
136+
panic!("multiple time fields found!");
137+
}
138+
time_field = Some(wf.ident.clone());
139+
}
140+
}
141+
142+
// There must be exactly one time field
143+
let time_field = time_field.expect("no time field found");
144+
145+
// Generate field assignments (excluding time and ignored fields)
146+
let field_assignments = writeable_fields
147+
.into_iter()
148+
.filter_map(|wf| {
149+
if wf.is_ignore || wf.is_time {
150+
None
151+
} else {
152+
let ident = wf.ident;
153+
Some(match wf.is_tag {
154+
true => quote!(query.add_tag(stringify!(#ident), self.#ident)),
155+
false => quote!(query.add_field(stringify!(#ident), self.#ident)),
156+
})
157+
}
158+
})
159+
.collect::<Vec<_>>();
160+
126161
Ok(quote! {
127162
impl #impl_generics ::influxdb::InfluxDbWriteable for #ident #ty_generics #where_clause {
128163
fn into_query<I: Into<String>>(self, name: I) -> ::influxdb::WriteQuery {
129164
let timestamp: ::influxdb::Timestamp = self.#time_field.into();
130165
let mut query = timestamp.into_query(name);
131166
#(
132-
query = #fields;
167+
query = #field_assignments;
133168
)*
134169
query
135170
}

0 commit comments

Comments
 (0)