Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

override column type using macro attribute #67

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion charybdis-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,10 @@ pub fn charybdis_view_model(args: TokenStream, input: TokenStream) -> TokenStrea

#[proc_macro_attribute]
pub fn charybdis_udt_model(_: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let mut input = parse_macro_input!(input as DeriveInput);

CharybdisFields::proxy_charybdis_attrs_to_scylla(&mut input);
CharybdisFields::strip_charybdis_attributes(&mut input);

let gen = quote! {
#[derive(charybdis::macros::scylla::SerializeValue)]
Expand Down
13 changes: 12 additions & 1 deletion charybdis-parser/src/fields.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::{HashMap, HashSet};
use std::str::FromStr;

use darling::FromAttributes;
use syn::spanned::Spanned;
Expand Down Expand Up @@ -44,6 +45,9 @@ pub enum CqlType {
pub struct FieldAttributes {
#[darling(default)]
pub ignore: Option<bool>,

#[darling(default)]
pub column_type: Option<String>,
}

pub struct Field<'a> {
Expand All @@ -52,6 +56,7 @@ pub struct Field<'a> {
pub ty: Type,
pub ty_path: syn::TypePath,
pub outer_type: CqlType,
pub column_type_override: Option<String>,
pub span: proc_macro2::Span,
pub attrs: &'a Vec<syn::Attribute>,
pub ignore: bool,
Expand Down Expand Up @@ -107,6 +112,11 @@ impl<'a> Field<'a> {
FieldAttributes::from_attributes(&field.attrs)
.map(|char_attrs| {
let ignore = char_attrs.ignore.unwrap_or(false);

let column_type = char_attrs.column_type.clone().map(
|tname| CqlType::from_str(tname.as_str()).unwrap()
).unwrap_or_else(|| Field::outer_type(&field.ty, ignore));
Copy link
Member

@GoranBrkuljan GoranBrkuljan Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we can do:

let column_type =
            char_attrs
                .column_type
                .as_ref()
                .map_or(Field::outer_type(&field.ty, ignore), |tname| {
                    let error = format!("Unknown column type: {}", tname);
                    CqlType::from_str(tname.as_str()).expect(&error)
                });

So we can improve error handling. Otherwise, we will not have a clue why migrations failed. Also, we avoid cloning.


let ident = field.ident.clone().unwrap();

Field {
Expand All @@ -117,7 +127,8 @@ impl<'a> Field<'a> {
Type::Path(type_path) => type_path.clone(),
_ => panic!("Only type path is supported!"),
},
outer_type: Field::outer_type(&field.ty, ignore),
outer_type: column_type,
column_type_override: char_attrs.column_type.clone(),
Copy link
Member

@GoranBrkuljan GoranBrkuljan Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need the 'clone' here if we borrow value for the column_type

span: field.span(),
attrs: &field.attrs,
ignore,
Expand Down
81 changes: 56 additions & 25 deletions charybdis-parser/src/schema/code_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,47 @@ pub struct CodeSchema {
pub materialized_views: SchemaObjects,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove migration changes, I will handle 'target' ignore' on a separate branch.

}


/// List all rust files under all /src dirs under the project dir.
fn _list_rust_source_files(project_root: &PathBuf) -> Vec<PathBuf> {
// look for src/ dirs max 4 levels down, to avoid stack overflow when we hit target/
let mut src_dirs = vec![];
let mut it = WalkDir::new(project_root).max_depth(4).into_iter();
loop {
let entry = match it.next() {
None => break,
Some(Err(err)) => panic!("ERROR: {}", err),
Some(Ok(entry)) => entry,
};
let path = entry.path();
if !path.is_dir() {
continue;
}
if path.ends_with("target") {
it.skip_current_dir();
continue;
}
if path.ends_with("src") {
src_dirs.push(path.to_owned());
it.skip_current_dir();
continue;
}
}

let mut src_files = vec![];
for src in src_dirs {
for entry in WalkDir::new(&src).max_depth(8) {
let entry: DirEntry = entry.unwrap();
let path = entry.path();
if path.is_file() && path.to_str().unwrap().ends_with(".rs") {
src_files.push(path.to_owned());
}
}
}

src_files
}

impl CodeSchema {
pub fn new(project_root: &String) -> CodeSchema {
let mut current_code_schema = CodeSchema {
Expand All @@ -48,31 +89,21 @@ impl CodeSchema {

pub fn get_models_from_code(&mut self, project_root: &String) {
let project_root: PathBuf = PathBuf::from(project_root);
for entry in WalkDir::new(project_root) {
let entry: DirEntry = entry.unwrap();

if entry.path().is_file() {
let path = entry.path().to_str().unwrap();

if !path.ends_with(".rs") {
continue;
}

let file_content: String = parser::parse_file_as_string(entry.path());
let ast: syn::File = syn::parse_file(&file_content)
.map_err(|e| {
println!(
"{}\n",
format!("Error parsing file: {}", file_content).bright_red().bold()
);
e
})
.unwrap();

self.populate_materialized_views(&ast);
self.populate_udts(&ast);
self.populate_tables(&ast);
}
for entry in _list_rust_source_files(&project_root) {
let file_content: String = parser::parse_file_as_string(&entry);
let ast: syn::File = syn::parse_file(&file_content)
.map_err(|e| {
println!(
"{}\n",
format!("Error parsing file: {}", file_content).bright_red().bold()
);
e
})
.unwrap();

self.populate_materialized_views(&ast);
self.populate_udts(&ast);
self.populate_tables(&ast);
}
}

Expand Down
2 changes: 1 addition & 1 deletion charybdis-parser/src/schema/code_schema/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fn extract_schema_object(item_struct: &ItemStruct, model_macro: &ModelMacro) ->

for field in db_fields {
let field_name = field.ident.to_string();
let field_type = type_with_arguments(&field.ty_path);
let field_type = field.column_type_override.unwrap_or_else(|| type_with_arguments(&field.ty_path));
let is_static = schema_object.static_columns.contains(&field_name);

schema_object.push_field(field_name, field_type, is_static);
Expand Down
4 changes: 4 additions & 0 deletions charybdis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ serde = { version = "1.0.200", features = ["derive"] }
colored = "2.1.0"
bigdecimal = { version = "0.4.3", features = ["serde"] }


[features]
migrate = ["charybdis-migrate"]

[dev-dependencies]
tokio = "1.42.0"
strum = { version = "0.26.3", features = ["derive"] }
serde = "1.0"
serde_json = "1.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also fix no endlines in files!

26 changes: 26 additions & 0 deletions charybdis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,3 +827,29 @@ pub struct User {
So field `organization` will be ignored in all operations and
default value will be used when deserializing from other data sources.
It can be used to hold data that is not persisted in database.

## Custom Fields

Any rust type can be used directly in table or UDT definition.
User must choose a ScyllaDB backing type (such as "TinyInt" or "Text")
and implement `SerializeValue` and `DeserializeValue` traits:


```rust
#[charybdis_model(...)]
pub struct User {
id: Uuid,
#[charybdis(column_type = "Text")]
extra_data: CustomField,
}

impl<'frame, 'metadata> DeserializeValue<'frame, 'metadata> for CustomField {
...
}

impl SerializeValue for CustomField {
...
}
```

See `custom_field.rs` integration test for examples using int and text encoding.
107 changes: 107 additions & 0 deletions charybdis/tests/integrations/custom_fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use charybdis::scylla::deserialize::DeserializeValue;
use charybdis::scylla::frame::response::result::ColumnType;
use charybdis::scylla::SerializeValue;
use charybdis::types::Text;

#[derive(Debug, Default, Clone, PartialEq, strum::FromRepr)]
#[repr(i8)]
pub enum AddressTypeCustomField {
#[default]
HomeAddress = 0,
WorkAddress = 1,
}

#[derive(Debug)]
struct AddressTypeCustomDeserializeErr(i8);
impl std::fmt::Display for AddressTypeCustomDeserializeErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AddressTypeCustomDeserializeErr({})", self.0)
}
}
impl std::error::Error for AddressTypeCustomDeserializeErr {}

impl<'frame, 'metadata> DeserializeValue<'frame, 'metadata> for AddressTypeCustomField {
fn type_check(
typ: &scylla::frame::response::result::ColumnType,
) -> std::result::Result<(), scylla::deserialize::TypeCheckError> {
Copy link
Member

@GoranBrkuljan GoranBrkuljan Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, path prefix for ColumnType and Result is not needed it's already in the scope because of use charybdis::scylla::frame::response::result::ColumnType; and because std::result::Result is automatically in the scope.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's update this in other functions as well.

<i8 as DeserializeValue<'frame, 'metadata>>::type_check(typ)
}

fn deserialize(
typ: &'metadata ColumnType<'metadata>,
v: Option<scylla::deserialize::FrameSlice<'frame>>,
) -> std::result::Result<Self, scylla::deserialize::DeserializationError> {
let si8 = <i8 as DeserializeValue<'frame, 'metadata>>::deserialize(typ, v)?;
let s = Self::from_repr(si8);
s.ok_or_else(|| scylla::deserialize::DeserializationError::new(AddressTypeCustomDeserializeErr(si8)))
}
}

impl SerializeValue for AddressTypeCustomField {
fn serialize<'b>(
&self,
typ: &ColumnType,
writer: scylla::serialize::writers::CellWriter<'b>,
) -> Result<scylla::serialize::writers::WrittenCellProof<'b>, scylla::serialize::SerializationError> {
let disc = self.clone() as i8;

let v = <i8 as SerializeValue>::serialize(&disc, typ, writer)?;
Ok(v)
}
}

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct UserExtraDataCustomField {
pub user_tags: Vec<(String, String)>,
}

impl Default for UserExtraDataCustomField {
fn default() -> Self {
Self { user_tags: vec![("some_key".to_string(), "some_value".to_string())] }
}
}

#[derive(Debug)]
struct UserExtraDataDeserializeErr(String);
impl std::fmt::Display for UserExtraDataDeserializeErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "UserExtraDataDeserializeErr({})", self.0)
}
}
impl std::error::Error for UserExtraDataDeserializeErr {}

impl<'frame, 'metadata> DeserializeValue<'frame, 'metadata> for UserExtraDataCustomField {
fn type_check(
typ: &scylla::frame::response::result::ColumnType,
) -> std::result::Result<(), scylla::deserialize::TypeCheckError> {
<Text as DeserializeValue<'frame, 'metadata>>::type_check(typ)
}

fn deserialize(
typ: &'metadata ColumnType<'metadata>,
v: Option<scylla::deserialize::FrameSlice<'frame>>,
) -> std::result::Result<Self, scylla::deserialize::DeserializationError> {
let si8 = <Text as DeserializeValue<'frame, 'metadata>>::deserialize(typ, v)?;
serde_json::from_str::<UserExtraDataCustomField>(&si8)
.map_err(
|_e| scylla::deserialize::DeserializationError::new(
UserExtraDataDeserializeErr(si8)))
}
}

impl SerializeValue for UserExtraDataCustomField {
fn serialize<'b>(
&self,
typ: &ColumnType,
writer: scylla::serialize::writers::CellWriter<'b>,
) -> Result<scylla::serialize::writers::WrittenCellProof<'b>, scylla::serialize::SerializationError> {

let disc = serde_json::to_string(&self)
.map_err(|_e| scylla::serialize::SerializationError::new(
_e
))?;

let v = <Text as SerializeValue>::serialize(&disc, typ, writer)?;
Ok(v)
}
}
2 changes: 2 additions & 0 deletions charybdis/tests/integrations/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod common;
mod model;
mod query;

mod custom_fields;
Loading
Loading