From 5eec3485feb7f3a5ade0c864f3ebf4db3363e0ba Mon Sep 17 00:00:00 2001 From: Weny Xu Date: Thu, 21 Aug 2025 14:29:59 +0800 Subject: [PATCH] feat: add `IntoRow` and `Schema` derive macros (#6778) * chore: import items Signed-off-by: WenyXu * feat: add `IntoRow` and `Schema` derive macros Signed-off-by: WenyXu * refactor: styling Signed-off-by: WenyXu * chore: apply suggestions Signed-off-by: WenyXu --------- Signed-off-by: WenyXu --- src/common/macro/src/lib.rs | 95 +++++- src/common/macro/src/row.rs | 25 ++ src/common/macro/src/row/attribute.rs | 128 +++++++ src/common/macro/src/row/into_row.rs | 88 +++++ src/common/macro/src/row/schema.rs | 118 +++++++ src/common/macro/src/row/to_row.rs | 88 +++++ src/common/macro/src/row/utils.rs | 313 +++++++++++++++++ src/common/macro/src/to_row.rs | 469 -------------------------- src/common/macro/tests/test_derive.rs | 72 ++-- 9 files changed, 905 insertions(+), 491 deletions(-) create mode 100644 src/common/macro/src/row.rs create mode 100644 src/common/macro/src/row/attribute.rs create mode 100644 src/common/macro/src/row/into_row.rs create mode 100644 src/common/macro/src/row/schema.rs create mode 100644 src/common/macro/src/row/to_row.rs create mode 100644 src/common/macro/src/row/utils.rs delete mode 100644 src/common/macro/src/to_row.rs diff --git a/src/common/macro/src/lib.rs b/src/common/macro/src/lib.rs index 7a8cbb6312..d904df8e8f 100644 --- a/src/common/macro/src/lib.rs +++ b/src/common/macro/src/lib.rs @@ -16,8 +16,8 @@ mod admin_fn; mod aggr_func; mod print_caller; mod range_fn; +mod row; mod stack_trace_debug; -mod to_row; mod utils; use aggr_func::{impl_aggr_func_type_store, impl_as_aggr_func_creator}; @@ -28,7 +28,9 @@ use range_fn::process_range_fn; use syn::{parse_macro_input, Data, DeriveInput, Fields}; use crate::admin_fn::process_admin_fn; -use crate::to_row::derive_to_row_impl; +use crate::row::into_row::derive_into_row_impl; +use crate::row::schema::derive_schema_impl; +use crate::row::to_row::derive_to_row_impl; /// Make struct implemented trait [AggrFuncTypeStore], which is necessary when writing UDAF. /// This derive macro is expect to be used along with attribute macro [macro@as_aggr_func_creator]. @@ -193,6 +195,10 @@ pub fn derive_meta_builder(input: TokenStream) -> TokenStream { /// /// # Example /// ```rust, ignore +/// use api::v1::Row; +/// use api::v1::value::ValueData; +/// use api::v1::Value; +/// /// #[derive(ToRow)] /// struct ToRowTest { /// my_value: i32, @@ -206,10 +212,95 @@ pub fn derive_meta_builder(input: TokenStream) -> TokenStream { /// datatype = "TimestampMillisecond" /// )] /// my_timestamp: i64, +/// #[col(skip)] +/// my_skip: i32, /// } +/// +/// let row = ToRowTest { +/// my_value: 1, +/// my_string: "test".to_string(), +/// my_bool: true, +/// my_float: 1.0, +/// my_timestamp: 1718563200000, +/// my_skip: 1, +/// }.to_row(); +/// ``` #[proc_macro_derive(ToRow, attributes(col))] pub fn derive_to_row(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let output = derive_to_row_impl(input); output.unwrap_or_else(|e| e.to_compile_error()).into() } + +/// Derive macro to convert a struct to a row with move semantics. +/// +/// # Example +/// ```rust, ignore +/// use api::v1::Row; +/// use api::v1::value::ValueData; +/// use api::v1::Value; +/// +/// #[derive(IntoRow)] +/// struct IntoRowTest { +/// my_value: i32, +/// #[col(name = "string_value", datatype = "string", semantic = "tag")] +/// my_string: String, +/// my_bool: bool, +/// my_float: f32, +/// #[col( +/// name = "timestamp_value", +/// semantic = "Timestamp", +/// datatype = "TimestampMillisecond" +/// )] +/// my_timestamp: i64, +/// #[col(skip)] +/// my_skip: i32, +/// } +/// +/// let row = IntoRowTest { +/// my_value: 1, +/// my_string: "test".to_string(), +/// my_bool: true, +/// my_float: 1.0, +/// my_timestamp: 1718563200000, +/// my_skip: 1, +/// }.into_row(); +/// ``` +#[proc_macro_derive(IntoRow, attributes(col))] +pub fn derive_into_row(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let output = derive_into_row_impl(input); + output.unwrap_or_else(|e| e.to_compile_error()).into() +} + +/// Derive macro to convert a struct to a schema. +/// +/// # Example +/// ```rust, ignore +/// use api::v1::ColumnSchema; +/// +/// #[derive(Schema)] +/// struct SchemaTest { +/// my_value: i32, +/// #[col(name = "string_value", datatype = "string", semantic = "tag")] +/// my_string: String, +/// my_bool: bool, +/// my_float: f32, +/// #[col( +/// name = "timestamp_value", +/// semantic = "Timestamp", +/// datatype = "TimestampMillisecond" +/// )] +/// my_timestamp: i64, +/// #[col(skip)] +/// my_skip: i32, +/// } +/// +/// let schema = SchemaTest::schema(); +/// ``` +#[proc_macro_derive(Schema, attributes(col))] +pub fn derive_schema(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let output = derive_schema_impl(input); + output.unwrap_or_else(|e| e.to_compile_error()).into() +} diff --git a/src/common/macro/src/row.rs b/src/common/macro/src/row.rs new file mode 100644 index 0000000000..5a660bb082 --- /dev/null +++ b/src/common/macro/src/row.rs @@ -0,0 +1,25 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) mod attribute; +pub(crate) mod into_row; +pub(crate) mod schema; +pub(crate) mod to_row; +pub(crate) mod utils; + +pub(crate) const META_KEY_COL: &str = "col"; +pub(crate) const META_KEY_NAME: &str = "name"; +pub(crate) const META_KEY_DATATYPE: &str = "datatype"; +pub(crate) const META_KEY_SEMANTIC: &str = "semantic"; +pub(crate) const META_KEY_SKIP: &str = "skip"; diff --git a/src/common/macro/src/row/attribute.rs b/src/common/macro/src/row/attribute.rs new file mode 100644 index 0000000000..f50b6cb462 --- /dev/null +++ b/src/common/macro/src/row/attribute.rs @@ -0,0 +1,128 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use once_cell::sync::Lazy; +use syn::meta::ParseNestedMeta; +use syn::spanned::Spanned; +use syn::{Attribute, LitStr, Meta, Result}; + +use crate::row::utils::{ + column_data_type_from_str, semantic_type_from_str, ColumnDataTypeWithExtension, SemanticType, +}; +use crate::row::{ + META_KEY_COL, META_KEY_DATATYPE, META_KEY_NAME, META_KEY_SEMANTIC, META_KEY_SKIP, +}; + +/// Column attribute. +#[derive(Default)] +pub(crate) struct ColumnAttribute { + /// User-defined name of the column., + pub(crate) name: Option, + /// Data type of the column. + pub(crate) datatype: Option, + /// Semantic type of the column. + pub(crate) semantic_type: SemanticType, + /// Whether to skip the column. + pub(crate) skip: bool, +} + +/// Find the column attribute in the attributes. +pub(crate) fn find_column_attribute(attrs: &[Attribute]) -> Option<&Attribute> { + attrs + .iter() + .find(|attr| matches!(&attr.meta, Meta::List(list) if list.path.is_ident(META_KEY_COL))) +} + +/// Parse the column attribute. +pub(crate) fn parse_column_attribute(attr: &Attribute) -> Result { + match &attr.meta { + Meta::List(list) if list.path.is_ident(META_KEY_COL) => { + let mut attribute = ColumnAttribute::default(); + list.parse_nested_meta(|meta| { + parse_column_attribute_field(&meta, &mut attribute) + })?; + Ok(attribute) + } + _ => Err(syn::Error::new( + attr.span(), + format!( + "expected `{META_KEY_COL}({META_KEY_NAME} = \"...\", {META_KEY_DATATYPE} = \"...\", {META_KEY_SEMANTIC} = \"...\")`" + ), + )), + } +} + +type ParseColumnAttributeField = fn(&ParseNestedMeta, &mut ColumnAttribute) -> Result<()>; + +static PARSE_COLUMN_ATTRIBUTE_FIELDS: Lazy> = + Lazy::new(|| { + HashMap::from([ + (META_KEY_NAME, parse_name_field as _), + (META_KEY_DATATYPE, parse_datatype_field as _), + (META_KEY_SEMANTIC, parse_semantic_field as _), + (META_KEY_SKIP, parse_skip_field as _), + ]) + }); + +fn parse_name_field(meta: &ParseNestedMeta<'_>, attribute: &mut ColumnAttribute) -> Result<()> { + let value = meta.value()?; + let s: LitStr = value.parse()?; + attribute.name = Some(s.value()); + Ok(()) +} + +fn parse_datatype_field(meta: &ParseNestedMeta<'_>, attribute: &mut ColumnAttribute) -> Result<()> { + let value = meta.value()?; + let s: LitStr = value.parse()?; + let ident = s.value(); + let Some(value) = column_data_type_from_str(&ident) else { + return Err(meta.error(format!("unexpected {META_KEY_DATATYPE}: {ident}"))); + }; + attribute.datatype = Some(value); + Ok(()) +} + +fn parse_semantic_field(meta: &ParseNestedMeta<'_>, attribute: &mut ColumnAttribute) -> Result<()> { + let value = meta.value()?; + let s: LitStr = value.parse()?; + let ident = s.value(); + let Some(value) = semantic_type_from_str(&ident) else { + return Err(meta.error(format!("unexpected {META_KEY_SEMANTIC}: {ident}"))); + }; + attribute.semantic_type = value; + Ok(()) +} + +fn parse_skip_field(_: &ParseNestedMeta<'_>, attribute: &mut ColumnAttribute) -> Result<()> { + attribute.skip = true; + Ok(()) +} + +fn parse_column_attribute_field( + meta: &ParseNestedMeta<'_>, + attribute: &mut ColumnAttribute, +) -> Result<()> { + let Some(ident) = meta.path.get_ident() else { + return Err(meta.error(format!("expected `{META_KEY_COL}({META_KEY_NAME} = \"...\", {META_KEY_DATATYPE} = \"...\", {META_KEY_SEMANTIC} = \"...\")`"))); + }; + let Some(parse_column_attribute) = + PARSE_COLUMN_ATTRIBUTE_FIELDS.get(ident.to_string().as_str()) + else { + return Err(meta.error(format!("unexpected attribute: {ident}"))); + }; + + parse_column_attribute(meta, attribute) +} diff --git a/src/common/macro/src/row/into_row.rs b/src/common/macro/src/row/into_row.rs new file mode 100644 index 0000000000..58d0d7eb0e --- /dev/null +++ b/src/common/macro/src/row/into_row.rs @@ -0,0 +1,88 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::spanned::Spanned; +use syn::{DeriveInput, Result}; + +use crate::row::utils::{ + convert_column_data_type_to_value_data_ident, extract_struct_fields, get_column_data_type, + parse_fields_from_fields_named, ParsedField, +}; +use crate::row::{META_KEY_COL, META_KEY_DATATYPE}; + +pub(crate) fn derive_into_row_impl(input: DeriveInput) -> Result { + let Some(fields) = extract_struct_fields(&input.data) else { + return Err(syn::Error::new( + input.span(), + "IntoRow can only be derived for structs", + )); + }; + let ident = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let fields = parse_fields_from_fields_named(fields)?; + + // Implement `into_row` method. + let impl_to_row_method = impl_into_row_method_combined(&fields)?; + Ok(quote! { + impl #impl_generics #ident #ty_generics #where_clause { + #impl_to_row_method + } + }) +} + +fn impl_into_row_method_combined(fields: &[ParsedField<'_>]) -> Result { + let value_exprs = fields + .iter() + .map(|field| { + let ParsedField {ident, field_type, column_data_type, column_attribute} = field; + let Some(column_data_type) = get_column_data_type(column_data_type, column_attribute) + else { + return Err(syn::Error::new( + ident.span(), + format!( + "expected to set data type explicitly via [({META_KEY_COL}({META_KEY_DATATYPE} = \"...\"))]" + ), + )); + }; + let value_data = convert_column_data_type_to_value_data_ident(&column_data_type.data_type); + let expr = if field_type.is_optional() { + quote! { + match self.#ident { + Some(v) => Value { + value_data: Some(ValueData::#value_data(v.into())), + }, + None => Value { value_data: None }, + } + } + } else { + quote! { + Value { + value_data: Some(ValueData::#value_data(self.#ident.into())), + } + } + }; + Ok(expr) + }) + .collect::>>()?; + + Ok(quote! { + pub fn into_row(self) -> Row { + Row { + values: vec![ #( #value_exprs ),* ] + } + } + }) +} diff --git a/src/common/macro/src/row/schema.rs b/src/common/macro/src/row/schema.rs new file mode 100644 index 0000000000..ac4d232f99 --- /dev/null +++ b/src/common/macro/src/row/schema.rs @@ -0,0 +1,118 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use greptime_proto::v1::column_data_type_extension::TypeExt; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::spanned::Spanned; +use syn::{DeriveInput, Result}; + +use crate::row::utils::{ + convert_semantic_type_to_proto_semantic_type, extract_struct_fields, get_column_data_type, + parse_fields_from_fields_named, ColumnDataTypeWithExtension, ParsedField, +}; +use crate::row::{META_KEY_COL, META_KEY_DATATYPE}; + +pub(crate) fn derive_schema_impl(input: DeriveInput) -> Result { + let Some(fields) = extract_struct_fields(&input.data) else { + return Err(syn::Error::new( + input.span(), + "Schema can only be derived for structs", + )); + }; + let ident = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let fields = parse_fields_from_fields_named(fields)?; + + // Implement `schema` method. + let impl_schema_method = impl_schema_method(&fields)?; + Ok(quote! { + impl #impl_generics #ident #ty_generics #where_clause { + #impl_schema_method + } + }) +} + +fn impl_schema_method(fields: &[ParsedField<'_>]) -> Result { + let schemas: Vec = fields + .iter() + .map(|field| { + let ParsedField{ ident, column_data_type, column_attribute, ..} = field; + let Some(ColumnDataTypeWithExtension{data_type, extension}) = get_column_data_type(column_data_type, column_attribute) + else { + return Err(syn::Error::new( + ident.span(), + format!( + "expected to set data type explicitly via [({META_KEY_COL}({META_KEY_DATATYPE} = \"...\"))]" + ), + )); + }; + // Uses user explicit name or field name as column name. + let name = column_attribute + .name + .clone() + .unwrap_or_else(|| ident.to_string()); + let name = syn::LitStr::new(&name, ident.span()); + let column_data_type = + syn::LitInt::new(&(data_type as i32).to_string(), ident.span()); + let semantic_type_val = convert_semantic_type_to_proto_semantic_type(column_attribute.semantic_type) as i32; + let semantic_type = syn::LitInt::new(&semantic_type_val.to_string(), ident.span()); + let extension = match extension { + Some(ext) => { + match ext.type_ext { + Some(TypeExt::DecimalType(ext)) => { + let precision = syn::LitInt::new(&ext.precision.to_string(), ident.span()); + let scale = syn::LitInt::new(&ext.scale.to_string(), ident.span()); + quote! { + Some(ColumnDataTypeExtension { type_ext: Some(TypeExt::DecimalType(DecimalTypeExtension { precision: #precision, scale: #scale })) }) + } + } + Some(TypeExt::JsonType(ext)) => { + let json_type = syn::LitInt::new(&ext.to_string(), ident.span()); + quote! { + Some(ColumnDataTypeExtension { type_ext: Some(TypeExt::JsonType(#json_type)) }) + } + } + Some(TypeExt::VectorType(ext)) => { + let dim = syn::LitInt::new(&ext.dim.to_string(), ident.span()); + quote! { + Some(ColumnDataTypeExtension { type_ext: Some(TypeExt::VectorType(VectorTypeExtension { dim: #dim })) }) + } + } + None => { + quote! { None } + } + } + } + None => quote! { None }, + }; + + Ok(quote! { + ColumnSchema { + column_name: #name.to_string(), + datatype: #column_data_type, + datatype_extension: #extension, + options: None, + semantic_type: #semantic_type, + } + }) + }) + .collect::>()?; + + Ok(quote! { + pub fn schema() -> Vec { + vec![ #(#schemas),* ] + } + }) +} diff --git a/src/common/macro/src/row/to_row.rs b/src/common/macro/src/row/to_row.rs new file mode 100644 index 0000000000..db62e1129f --- /dev/null +++ b/src/common/macro/src/row/to_row.rs @@ -0,0 +1,88 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::spanned::Spanned; +use syn::{DeriveInput, Result}; + +use crate::row::utils::{ + convert_column_data_type_to_value_data_ident, extract_struct_fields, get_column_data_type, + parse_fields_from_fields_named, ParsedField, +}; +use crate::row::{META_KEY_COL, META_KEY_DATATYPE}; + +pub(crate) fn derive_to_row_impl(input: DeriveInput) -> Result { + let Some(fields) = extract_struct_fields(&input.data) else { + return Err(syn::Error::new( + input.span(), + "ToRow can only be derived for structs", + )); + }; + let ident = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let fields = parse_fields_from_fields_named(fields)?; + + // Implement `to_row` method. + let impl_to_row_method = impl_to_row_method_combined(&fields)?; + Ok(quote! { + impl #impl_generics #ident #ty_generics #where_clause { + #impl_to_row_method + } + }) +} + +fn impl_to_row_method_combined(fields: &[ParsedField<'_>]) -> Result { + let value_exprs = fields + .iter() + .map(|field| { + let ParsedField {ident, field_type, column_data_type, column_attribute} = field; + let Some(column_data_type) = get_column_data_type(column_data_type, column_attribute) + else { + return Err(syn::Error::new( + ident.span(), + format!( + "expected to set data type explicitly via [({META_KEY_COL}({META_KEY_DATATYPE} = \"...\"))]" + ), + )); + }; + let value_data = convert_column_data_type_to_value_data_ident(&column_data_type.data_type); + let expr = if field_type.is_optional() { + quote! { + match &self.#ident { + Some(v) => Value { + value_data: Some(ValueData::#value_data(v.clone().into())), + }, + None => Value { value_data: None }, + } + } + } else { + quote! { + Value { + value_data: Some(ValueData::#value_data(self.#ident.clone().into())), + } + } + }; + Ok(expr) + }) + .collect::>>()?; + + Ok(quote! { + pub fn to_row(&self) -> Row { + Row { + values: vec![ #( #value_exprs ),* ] + } + } + }) +} diff --git a/src/common/macro/src/row/utils.rs b/src/common/macro/src/row/utils.rs new file mode 100644 index 0000000000..eb6de00b2c --- /dev/null +++ b/src/common/macro/src/row/utils.rs @@ -0,0 +1,313 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use greptime_proto::v1::column_data_type_extension::TypeExt; +use greptime_proto::v1::{ColumnDataType, ColumnDataTypeExtension, JsonTypeExtension}; +use once_cell::sync::Lazy; +use quote::format_ident; +use syn::{ + AngleBracketedGenericArguments, Data, DataStruct, Fields, FieldsNamed, GenericArgument, Ident, + Path, PathArguments, PathSegment, Result, Type, TypePath, TypeReference, +}; + +use crate::row::attribute::{find_column_attribute, parse_column_attribute, ColumnAttribute}; + +static SEMANTIC_TYPES: Lazy> = Lazy::new(|| { + HashMap::from([ + ("field", SemanticType::Field), + ("tag", SemanticType::Tag), + ("timestamp", SemanticType::Timestamp), + ]) +}); + +static DATATYPE_TO_COLUMN_DATA_TYPE: Lazy> = + Lazy::new(|| { + HashMap::from([ + // Timestamp + ("timestampsecond", ColumnDataType::TimestampSecond.into()), + ( + "timestampmillisecond", + ColumnDataType::TimestampMillisecond.into(), + ), + ( + "timestampmicrosecond", + ColumnDataType::TimestampMicrosecond.into(), + ), + ( + "timestampnanosecond", + ColumnDataType::TimestampNanosecond.into(), + ), + // Date + ("date", ColumnDataType::Date.into()), + ("datetime", ColumnDataType::Datetime.into()), + // Time + ("timesecond", ColumnDataType::TimeSecond.into()), + ("timemillisecond", ColumnDataType::TimeMillisecond.into()), + ("timemicrosecond", ColumnDataType::TimeMicrosecond.into()), + ("timenanosecond", ColumnDataType::TimeNanosecond.into()), + // Others + ("string", ColumnDataType::String.into()), + ("json", ColumnDataTypeWithExtension::json()), + // TODO(weny): support vector and decimal128. + ]) + }); + +static PRIMITIVE_TYPE_TO_COLUMN_DATA_TYPE: Lazy> = + Lazy::new(|| { + HashMap::from([ + ("i8", ColumnDataType::Int8), + ("i16", ColumnDataType::Int16), + ("i32", ColumnDataType::Int32), + ("i64", ColumnDataType::Int64), + ("u8", ColumnDataType::Uint8), + ("u16", ColumnDataType::Uint16), + ("u32", ColumnDataType::Uint32), + ("u64", ColumnDataType::Uint64), + ("f32", ColumnDataType::Float32), + ("f64", ColumnDataType::Float64), + ("bool", ColumnDataType::Boolean), + ]) + }); + +/// Extract the fields of a struct. +pub(crate) fn extract_struct_fields(data: &Data) -> Option<&FieldsNamed> { + let Data::Struct(DataStruct { + fields: Fields::Named(named), + .. + }) = &data + else { + return None; + }; + + Some(named) +} + +/// Convert an identifier to a semantic type. +pub(crate) fn semantic_type_from_str(ident: &str) -> Option { + // Ignores the case of the identifier. + let lowercase = ident.to_lowercase(); + let lowercase_str = lowercase.as_str(); + SEMANTIC_TYPES.get(lowercase_str).cloned() +} + +/// Convert a field type to a column data type. +pub(crate) fn column_data_type_from_str(ident: &str) -> Option { + // Ignores the case of the identifier. + let lowercase = ident.to_lowercase(); + let lowercase_str = lowercase.as_str(); + DATATYPE_TO_COLUMN_DATA_TYPE.get(lowercase_str).cloned() +} + +#[derive(Default, Clone, Copy)] +pub(crate) enum SemanticType { + #[default] + Field, + Tag, + Timestamp, +} + +pub(crate) enum FieldType<'a> { + Required(&'a Type), + Optional(&'a Type), +} + +impl FieldType<'_> { + pub(crate) fn is_optional(&self) -> bool { + matches!(self, FieldType::Optional(_)) + } + + pub(crate) fn extract_ident(&self) -> Option<&Ident> { + match self { + FieldType::Required(ty) => extract_ident_from_type(ty), + FieldType::Optional(ty) => extract_ident_from_type(ty), + } + } +} + +fn field_type(ty: &Type) -> FieldType<'_> { + if let Type::Reference(TypeReference { elem, .. }) = ty { + return field_type(elem); + } + + if let Type::Path(TypePath { + qself: _, + path: Path { + leading_colon, + segments, + }, + }) = ty + { + if leading_colon.is_none() && segments.len() == 1 { + if let Some(PathSegment { + ident, + arguments: + PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }), + }) = segments.first() + { + if let (1, Some(GenericArgument::Type(t))) = (args.len(), args.first()) { + if ident == "Option" { + return FieldType::Optional(t); + } + } + } + } + } + + FieldType::Required(ty) +} + +fn extract_ident_from_type(ty: &Type) -> Option<&Ident> { + match ty { + Type::Path(TypePath { qself: None, path }) => path.get_ident(), + Type::Reference(type_ref) => extract_ident_from_type(&type_ref.elem), + Type::Group(type_group) => extract_ident_from_type(&type_group.elem), + _ => None, + } +} + +/// Convert a semantic type to a proto semantic type. +pub(crate) fn convert_semantic_type_to_proto_semantic_type( + semantic_type: SemanticType, +) -> greptime_proto::v1::SemanticType { + match semantic_type { + SemanticType::Field => greptime_proto::v1::SemanticType::Field, + SemanticType::Tag => greptime_proto::v1::SemanticType::Tag, + SemanticType::Timestamp => greptime_proto::v1::SemanticType::Timestamp, + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct ColumnDataTypeWithExtension { + pub(crate) data_type: ColumnDataType, + pub(crate) extension: Option, +} + +impl ColumnDataTypeWithExtension { + pub(crate) fn json() -> Self { + Self { + data_type: ColumnDataType::Json, + extension: Some(ColumnDataTypeExtension { + type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())), + }), + } + } +} + +impl From for ColumnDataTypeWithExtension { + fn from(data_type: ColumnDataType) -> Self { + Self { + data_type, + extension: None, + } + } +} + +pub(crate) struct ParsedField<'a> { + pub(crate) ident: &'a Ident, + pub(crate) field_type: FieldType<'a>, + pub(crate) column_data_type: Option, + pub(crate) column_attribute: ColumnAttribute, +} + +/// Parse fields from fields named. +pub(crate) fn parse_fields_from_fields_named(named: &FieldsNamed) -> Result>> { + Ok(named + .named + .iter() + .map(|field| { + let ident = field.ident.as_ref().expect("field must have an ident"); + let field_type = field_type(&field.ty); + let column_data_type = field_type + .extract_ident() + .and_then(convert_primitive_type_to_column_data_type); + let column_attribute = find_column_attribute(&field.attrs) + .map(parse_column_attribute) + .transpose()? + .unwrap_or_default(); + + Ok(ParsedField { + ident, + field_type, + column_data_type, + column_attribute, + }) + }) + .collect::>>>()? + .into_iter() + .filter(|field| !field.column_attribute.skip) + .collect::>()) +} + +fn convert_primitive_type_to_column_data_type( + ident: &Ident, +) -> Option { + PRIMITIVE_TYPE_TO_COLUMN_DATA_TYPE + .get(ident.to_string().as_str()) + .cloned() + .map(ColumnDataTypeWithExtension::from) +} + +/// Get the column data type from the attribute or the inferred column data type. +pub(crate) fn get_column_data_type( + infer_column_data_type: &Option, + attribute: &ColumnAttribute, +) -> Option { + attribute.datatype.or(*infer_column_data_type) +} + +/// Convert a column data type to a value data ident. +pub(crate) fn convert_column_data_type_to_value_data_ident( + column_data_type: &ColumnDataType, +) -> Ident { + match column_data_type { + ColumnDataType::Boolean => format_ident!("BoolValue"), + ColumnDataType::Int8 => format_ident!("I8Value"), + ColumnDataType::Int16 => format_ident!("I16Value"), + ColumnDataType::Int32 => format_ident!("I32Value"), + ColumnDataType::Int64 => format_ident!("I64Value"), + ColumnDataType::Uint8 => format_ident!("U8Value"), + ColumnDataType::Uint16 => format_ident!("U16Value"), + ColumnDataType::Uint32 => format_ident!("U32Value"), + ColumnDataType::Uint64 => format_ident!("U64Value"), + ColumnDataType::Float32 => format_ident!("F32Value"), + ColumnDataType::Float64 => format_ident!("F64Value"), + ColumnDataType::Binary => format_ident!("BinaryValue"), + ColumnDataType::String => format_ident!("StringValue"), + ColumnDataType::Date => format_ident!("DateValue"), + ColumnDataType::Datetime => format_ident!("DatetimeValue"), + ColumnDataType::TimestampSecond => format_ident!("TimestampSecondValue"), + ColumnDataType::TimestampMillisecond => { + format_ident!("TimestampMillisecondValue") + } + ColumnDataType::TimestampMicrosecond => { + format_ident!("TimestampMicrosecondValue") + } + ColumnDataType::TimestampNanosecond => format_ident!("TimestampNanosecondValue"), + ColumnDataType::TimeSecond => format_ident!("TimeSecondValue"), + ColumnDataType::TimeMillisecond => format_ident!("TimeMillisecondValue"), + ColumnDataType::TimeMicrosecond => format_ident!("TimeMicrosecondValue"), + ColumnDataType::TimeNanosecond => format_ident!("TimeNanosecondValue"), + ColumnDataType::IntervalYearMonth => format_ident!("IntervalYearMonthValue"), + ColumnDataType::IntervalDayTime => format_ident!("IntervalDayTimeValue"), + ColumnDataType::IntervalMonthDayNano => { + format_ident!("IntervalMonthDayNanoValue") + } + ColumnDataType::Decimal128 => format_ident!("Decimal128Value"), + // Json is a special case, it is actually a string column. + ColumnDataType::Json => format_ident!("StringValue"), + ColumnDataType::Vector => format_ident!("VectorValue"), + } +} diff --git a/src/common/macro/src/to_row.rs b/src/common/macro/src/to_row.rs deleted file mode 100644 index b123f7cbc3..0000000000 --- a/src/common/macro/src/to_row.rs +++ /dev/null @@ -1,469 +0,0 @@ -// Copyright 2023 Greptime Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::HashMap; - -use greptime_proto::v1::ColumnDataType; -use once_cell::sync::Lazy; -use proc_macro2::{Ident, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use syn::meta::ParseNestedMeta; -use syn::spanned::Spanned; -use syn::{ - AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, Fields, FieldsNamed, - GenericArgument, LitStr, Meta, Path, PathArguments, PathSegment, Result, Type, TypePath, - TypeReference, -}; - -const META_KEY_COL: &str = "col"; -const META_KEY_NAME: &str = "name"; -const META_KEY_DATATYPE: &str = "datatype"; -const META_KEY_SEMANTIC: &str = "semantic"; - -pub(crate) fn derive_to_row_impl(input: DeriveInput) -> Result { - let Data::Struct(DataStruct { - fields: Fields::Named(FieldsNamed { named, .. }), - .. - }) = &input.data - else { - return Err(syn::Error::new( - input.span(), - "ToRow can only be derived for structs", - )); - }; - - let ident = input.ident; - let generics = input.generics; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let fields = named.iter().map(|field| { - ( - field.ident.as_ref().expect("field must have an ident"), - &field.ty, - ) - }); - - // Ident of fields. - let idents = fields.clone().map(|(ident, _)| ident).collect::>(); - // Infer column data types for each field. - let infer_column_data_types = fields - .clone() - .map(|(_, ty)| field_type(ty)) - .flat_map(|ty| { - ty.extract_ident() - .map(convert_primitive_type_to_column_data_type) - }) - .collect::>(); - - // Find attributes for each field. - let column_attributes = named - .iter() - .map(|field| { - let attrs = &field.attrs; - let attr = find_column_attribute(attrs); - match attr { - Some(attr) => parse_attribute(attr), - None => Ok(ColumnAttribute::default()), - } - }) - .collect::>>()?; - let field_types = fields - .clone() - .map(|(_, ty)| field_type(ty)) - .collect::>(); - - // Implement `to_row` method. - let impl_to_row_method = impl_to_row_method_combined( - &idents, - &field_types, - &infer_column_data_types, - &column_attributes, - )?; - - // Implement `schema` method. - let impl_schema_method = - impl_schema_method(&idents, &infer_column_data_types, &column_attributes)?; - - Ok(quote! { - impl #impl_generics #ident #ty_generics #where_clause { - #impl_to_row_method - - #impl_schema_method - } - }) -} - -fn impl_schema_method( - idents: &[&Ident], - infer_column_data_types: &[Option], - column_attributes: &[ColumnAttribute], -) -> Result { - let schemas: Vec = idents - .iter() - .zip(infer_column_data_types.iter()) - .zip(column_attributes.iter()) - .map(|((ident, column_data_type), column_attribute)| { - let Some(column_data_type) = get_column_data_type(column_data_type, column_attribute) - else { - return Err(syn::Error::new( - ident.span(), - format!( - "expected to set data type explicitly via [({META_KEY_COL}({META_KEY_DATATYPE} = \"...\"))]" - ), - )); - }; - - // Uses user explicit name or field name as column name. - let name = column_attribute - .name - .clone() - .unwrap_or_else(|| ident.to_string()); - let name = syn::LitStr::new(&name, ident.span()); - let column_data_type = - syn::LitInt::new(&(column_data_type as i32).to_string(), ident.span()); - let semantic_type_val = match column_attribute.semantic_type { - SemanticType::Field => greptime_proto::v1::SemanticType::Field, - SemanticType::Tag => greptime_proto::v1::SemanticType::Tag, - SemanticType::Timestamp => greptime_proto::v1::SemanticType::Timestamp, - } as i32; - let semantic_type = syn::LitInt::new(&semantic_type_val.to_string(), ident.span()); - - Ok(quote! { - greptime_proto::v1::ColumnSchema { - column_name: #name.to_string(), - datatype: #column_data_type, - datatype_extension: None, - options: None, - semantic_type: #semantic_type, - } - }) - }) - .collect::>()?; - - Ok(quote! { - pub fn schema(&self) -> Vec { - vec![ #(#schemas),* ] - } - }) -} - -fn impl_to_row_method_combined( - idents: &[&Ident], - field_types: &[FieldType<'_>], - infer_column_data_types: &[Option], - column_attributes: &[ColumnAttribute], -) -> Result { - let value_exprs = idents - .iter() - .zip(field_types.iter()) - .zip(infer_column_data_types.iter()) - .zip(column_attributes.iter()) - .map(|(((ident, field_type), column_data_type), column_attribute)| { - let Some(column_data_type) = get_column_data_type(column_data_type, column_attribute) - else { - return Err(syn::Error::new( - ident.span(), - format!( - "expected to set data type explicitly via [({META_KEY_COL}({META_KEY_DATATYPE} = \"...\"))]" - ), - )); - }; - let value_data = convert_column_data_type_to_value_data_ident(&column_data_type); - let expr = if field_type.is_optional() { - quote! { - match &self.#ident { - Some(v) => greptime_proto::v1::Value { - value_data: Some(greptime_proto::v1::value::ValueData::#value_data(v.clone().into())), - }, - None => greptime_proto::v1::Value { value_data: None }, - } - } - } else { - quote! { - greptime_proto::v1::Value { - value_data: Some(greptime_proto::v1::value::ValueData::#value_data(self.#ident.clone().into())), - } - } - }; - Ok(expr) - }) - .collect::>>()?; - - Ok(quote! { - pub fn to_row(&self) -> greptime_proto::v1::Row { - greptime_proto::v1::Row { - values: vec![ #( #value_exprs ),* ] - } - } - }) -} - -fn get_column_data_type( - infer_column_data_type: &Option, - attribute: &ColumnAttribute, -) -> Option { - attribute.column_data_type.or(*infer_column_data_type) -} - -fn convert_column_data_type_to_value_data_ident(column_data_type: &ColumnDataType) -> Ident { - match column_data_type { - ColumnDataType::Boolean => format_ident!("BoolValue"), - ColumnDataType::Int8 => format_ident!("I8Value"), - ColumnDataType::Int16 => format_ident!("I16Value"), - ColumnDataType::Int32 => format_ident!("I32Value"), - ColumnDataType::Int64 => format_ident!("I64Value"), - ColumnDataType::Uint8 => format_ident!("U8Value"), - ColumnDataType::Uint16 => format_ident!("U16Value"), - ColumnDataType::Uint32 => format_ident!("U32Value"), - ColumnDataType::Uint64 => format_ident!("U64Value"), - ColumnDataType::Float32 => format_ident!("F32Value"), - ColumnDataType::Float64 => format_ident!("F64Value"), - ColumnDataType::Binary => format_ident!("BinaryValue"), - ColumnDataType::String => format_ident!("StringValue"), - ColumnDataType::Date => format_ident!("DateValue"), - ColumnDataType::Datetime => format_ident!("DatetimeValue"), - ColumnDataType::TimestampSecond => format_ident!("TimestampSecondValue"), - ColumnDataType::TimestampMillisecond => { - format_ident!("TimestampMillisecondValue") - } - ColumnDataType::TimestampMicrosecond => { - format_ident!("TimestampMicrosecondValue") - } - ColumnDataType::TimestampNanosecond => format_ident!("TimestampNanosecondValue"), - ColumnDataType::TimeSecond => format_ident!("TimeSecondValue"), - ColumnDataType::TimeMillisecond => format_ident!("TimeMillisecondValue"), - ColumnDataType::TimeMicrosecond => format_ident!("TimeMicrosecondValue"), - ColumnDataType::TimeNanosecond => format_ident!("TimeNanosecondValue"), - ColumnDataType::IntervalYearMonth => format_ident!("IntervalYearMonthValue"), - ColumnDataType::IntervalDayTime => format_ident!("IntervalDayTimeValue"), - ColumnDataType::IntervalMonthDayNano => { - format_ident!("IntervalMonthDayNanoValue") - } - ColumnDataType::Decimal128 => format_ident!("Decimal128Value"), - ColumnDataType::Json => format_ident!("JsonValue"), - ColumnDataType::Vector => format_ident!("VectorValue"), - } -} - -fn field_type(ty: &Type) -> FieldType<'_> { - if let Type::Reference(TypeReference { elem, .. }) = ty { - return field_type(elem); - } - - if let Type::Path(TypePath { - qself: _, - path: Path { - leading_colon, - segments, - }, - }) = ty - { - if leading_colon.is_none() && segments.len() == 1 { - if let Some(PathSegment { - ident, - arguments: - PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }), - }) = segments.first() - { - if let (1, Some(GenericArgument::Type(t))) = (args.len(), args.first()) { - if ident == "Option" { - return FieldType::Optional(t); - } - } - } - } - } - - FieldType::Required(ty) -} - -fn extract_ident_from_type(ty: &Type) -> Option<&Ident> { - match ty { - Type::Path(TypePath { qself: None, path }) => path.get_ident(), - Type::Reference(type_ref) => extract_ident_from_type(&type_ref.elem), - Type::Group(type_group) => extract_ident_from_type(&type_group.elem), - _ => None, - } -} - -static PRIMITIVE_TYPE_TO_COLUMN_DATA_TYPE: Lazy> = - Lazy::new(|| { - HashMap::from([ - ("i8", ColumnDataType::Int8), - ("i16", ColumnDataType::Int16), - ("i32", ColumnDataType::Int32), - ("i64", ColumnDataType::Int64), - ("u8", ColumnDataType::Uint8), - ("u16", ColumnDataType::Uint16), - ("u32", ColumnDataType::Uint32), - ("u64", ColumnDataType::Uint64), - ("f32", ColumnDataType::Float32), - ("f64", ColumnDataType::Float64), - ("bool", ColumnDataType::Boolean), - ]) - }); - -static DATATYPE_TO_COLUMN_DATA_TYPE: Lazy> = - Lazy::new(|| { - HashMap::from([ - // Timestamp - ("timestampsecond", ColumnDataType::TimestampSecond), - ("timestampmillisecond", ColumnDataType::TimestampMillisecond), - ( - "timestamptimemicrosecond", - ColumnDataType::TimestampMicrosecond, - ), - ( - "timestamptimenanosecond", - ColumnDataType::TimestampNanosecond, - ), - // Date - ("date", ColumnDataType::Date), - ("datetime", ColumnDataType::Datetime), - // Time - ("timesecond", ColumnDataType::TimeSecond), - ("timemillisecond", ColumnDataType::TimeMillisecond), - ("timemicrosecond", ColumnDataType::TimeMicrosecond), - ("timenanosecond", ColumnDataType::TimeNanosecond), - // Others - ("string", ColumnDataType::String), - ("json", ColumnDataType::Json), - ("decimal128", ColumnDataType::Decimal128), - ("vector", ColumnDataType::Vector), - ]) - }); - -static SEMANTIC_TYPES: Lazy> = Lazy::new(|| { - HashMap::from([ - ("field", SemanticType::Field), - ("tag", SemanticType::Tag), - ("timestamp", SemanticType::Timestamp), - ]) -}); - -fn convert_primitive_type_to_column_data_type(ident: &Ident) -> Option { - PRIMITIVE_TYPE_TO_COLUMN_DATA_TYPE - .get(ident.to_string().as_str()) - .cloned() -} - -fn semantic_type_from_ident(ident: &str) -> Option { - // Ignores the case of the identifier. - let lowercase = ident.to_lowercase(); - let lowercase_str = lowercase.as_str(); - SEMANTIC_TYPES.get(lowercase_str).cloned() -} - -fn convert_field_type_to_column_data_type(ident: &str) -> Option { - // Ignores the case of the identifier. - let lowercase = ident.to_lowercase(); - let lowercase_str = lowercase.as_str(); - DATATYPE_TO_COLUMN_DATA_TYPE.get(lowercase_str).cloned() -} - -#[derive(Default, Clone, Copy)] -enum SemanticType { - #[default] - Field, - Tag, - Timestamp, -} - -enum FieldType<'a> { - Required(&'a Type), - Optional(&'a Type), -} - -impl FieldType<'_> { - fn is_optional(&self) -> bool { - matches!(self, FieldType::Optional(_)) - } - - fn extract_ident(&self) -> Option<&Ident> { - match self { - FieldType::Required(ty) => extract_ident_from_type(ty), - FieldType::Optional(ty) => extract_ident_from_type(ty), - } - } -} - -#[derive(Default)] -struct ColumnAttribute { - name: Option, - column_data_type: Option, - semantic_type: SemanticType, -} - -fn find_column_attribute(attrs: &[Attribute]) -> Option<&Attribute> { - attrs - .iter() - .find(|attr| matches!(&attr.meta, Meta::List(list) if list.path.is_ident(META_KEY_COL))) -} - -fn parse_attribute(attr: &Attribute) -> Result { - match &attr.meta { - Meta::List(list) if list.path.is_ident(META_KEY_COL) => { - let mut attribute = ColumnAttribute::default(); - list.parse_nested_meta(|meta| { - parse_column_attribute(&meta, &mut attribute) - })?; - Ok(attribute) - } - _ => Err(syn::Error::new( - attr.span(), - format!( - "expected `{META_KEY_COL}({META_KEY_NAME} = \"...\", {META_KEY_DATATYPE} = \"...\", {META_KEY_SEMANTIC} = \"...\")`" - ), - )), - } -} - -fn parse_column_attribute( - meta: &ParseNestedMeta<'_>, - attribute: &mut ColumnAttribute, -) -> Result<()> { - let Some(ident) = meta.path.get_ident() else { - return Err(meta.error(format!("expected `{META_KEY_COL}({META_KEY_NAME} = \"...\", {META_KEY_DATATYPE} = \"...\", {META_KEY_SEMANTIC} = \"...\")`"))); - }; - - match ident.to_string().as_str() { - META_KEY_NAME => { - let value = meta.value()?; - let s: LitStr = value.parse()?; - attribute.name = Some(s.value()); - } - META_KEY_DATATYPE => { - let value = meta.value()?; - let s: LitStr = value.parse()?; - let ident = s.value(); - let Some(value) = convert_field_type_to_column_data_type(&ident) else { - return Err(meta.error(format!("unexpected {META_KEY_DATATYPE}: {ident}"))); - }; - attribute.column_data_type = Some(value); - } - META_KEY_SEMANTIC => { - let value = meta.value()?; - let s: LitStr = value.parse()?; - - let ident = s.value(); - let Some(value) = semantic_type_from_ident(&ident) else { - return Err(meta.error(format!("unexpected {META_KEY_SEMANTIC}: {ident}"))); - }; - attribute.semantic_type = value; - } - attr => return Err(meta.error(format!("unexpected attribute: {attr}"))), - } - - Ok(()) -} diff --git a/src/common/macro/tests/test_derive.rs b/src/common/macro/tests/test_derive.rs index cfb709d624..d78663fffc 100644 --- a/src/common/macro/tests/test_derive.rs +++ b/src/common/macro/tests/test_derive.rs @@ -12,10 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common_macro::ToRow; -use greptime_proto::v1::{ColumnDataType, ColumnSchema, Row, SemanticType}; +use common_macro::{IntoRow, Schema, ToRow}; +use greptime_proto::v1::column_data_type_extension::TypeExt; +use greptime_proto::v1::value::ValueData; +use greptime_proto::v1::{ + ColumnDataType, ColumnDataTypeExtension, ColumnSchema, JsonTypeExtension, Row, SemanticType, + Value, +}; -#[derive(ToRow)] +#[derive(ToRow, Schema, IntoRow)] struct ToRowOwned { my_value: i32, #[col(name = "string_value", datatype = "string", semantic = "tag")] @@ -28,6 +33,11 @@ struct ToRowOwned { datatype = "TimestampMillisecond" )] my_timestamp: i64, + #[allow(dead_code)] + #[col(skip)] + my_skip: i32, + #[col(name = "json_value", datatype = "json")] + my_json: String, } #[test] @@ -38,14 +48,18 @@ fn test_to_row() { my_bool: true, my_float: 1.0, my_timestamp: 1718563200000, + my_skip: 1, + my_json: r#"{"name":"John", "age":30}"#.to_string(), }; let row = test.to_row(); assert_row(&row); - let schema = test.schema(); + let schema = ToRowOwned::schema(); assert_schema(&schema); + let row2 = test.into_row(); + assert_row(&row2); } -#[derive(ToRow)] +#[derive(ToRow, Schema)] struct ToRowRef<'a> { my_value: &'a i32, #[col(name = "string_value", datatype = "string", semantic = "tag")] @@ -58,6 +72,8 @@ struct ToRowRef<'a> { datatype = "TimestampMillisecond" )] my_timestamp: &'a i64, + #[col(name = "json_value", datatype = "json")] + my_json: &'a str, } #[test] @@ -69,14 +85,15 @@ fn test_to_row_ref() { my_bool: &true, my_float: &1.0, my_timestamp: &1718563200000, + my_json: r#"{"name":"John", "age":30}"#, }; let row = test.to_row(); assert_row(&row); - let schema = test.schema(); + let schema = ToRowRef::schema(); assert_schema(&schema); } -#[derive(ToRow)] +#[derive(ToRow, IntoRow)] struct ToRowOptional { my_value: Option, #[col(name = "string_value", datatype = "string", semantic = "tag")] @@ -91,17 +108,7 @@ struct ToRowOptional { my_timestamp: i64, } -#[test] -fn test_to_row_optional() { - let test = ToRowOptional { - my_value: None, - my_string: None, - my_bool: None, - my_float: None, - my_timestamp: 1718563200000, - }; - - let row = test.to_row(); +fn assert_row_optional(row: &Row) { assert_eq!(row.values.len(), 5); assert_eq!(row.values[0].value_data, None); assert_eq!(row.values[1].value_data, None); @@ -113,8 +120,24 @@ fn test_to_row_optional() { ); } +#[test] +fn test_to_row_optional() { + let test = ToRowOptional { + my_value: None, + my_string: None, + my_bool: None, + my_float: None, + my_timestamp: 1718563200000, + }; + + let row = test.to_row(); + assert_row_optional(&row); + let row2 = test.into_row(); + assert_row_optional(&row2); +} + fn assert_row(row: &Row) { - assert_eq!(row.values.len(), 5); + assert_eq!(row.values.len(), 6); assert_eq!( row.values[0].value_data, Some(greptime_proto::v1::value::ValueData::I32Value(1)) @@ -140,7 +163,7 @@ fn assert_row(row: &Row) { } fn assert_schema(schema: &[ColumnSchema]) { - assert_eq!(schema.len(), 5); + assert_eq!(schema.len(), 6); assert_eq!(schema[0].column_name, "my_value"); assert_eq!(schema[0].datatype, ColumnDataType::Int32 as i32); assert_eq!(schema[0].semantic_type, SemanticType::Field as i32); @@ -159,4 +182,13 @@ fn assert_schema(schema: &[ColumnSchema]) { ColumnDataType::TimestampMillisecond as i32 ); assert_eq!(schema[4].semantic_type, SemanticType::Timestamp as i32); + assert_eq!(schema[5].column_name, "json_value"); + assert_eq!(schema[5].datatype, ColumnDataType::Json as i32); + assert_eq!(schema[5].semantic_type, SemanticType::Field as i32); + assert_eq!( + schema[5].datatype_extension, + Some(ColumnDataTypeExtension { + type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary as i32)) + }) + ); }