Skip to content

Conversation

@ziara1
Copy link

@ziara1 ziara1 commented Jan 8, 2026

Motivation

Users often define data structures using tuple structs (structs with unnamed fields), such as struct Point(f64, f64) or struct UserId(i32). Currently, the DeserializeRow derive macro only supports structs with named fields, where it maps database columns to Rust fields by name. This limitation forces users to either use named structs (which might not fit their domain model) or manually implement the deserialization traits, resulting in unnecessary boilerplate.

Solution

This commit extends the DeserializeRow derive macro to support tuple structs.

When deriving DeserializeRow for a tuple struct, the macro generates an implementation that maps database columns to struct fields based on their order (index).

  • The 1st column in the result set maps to the 0th field of the struct.
  • The 2nd column maps to the 1st field, and so on.

What's done

  • Refactored deserialize_row_derive

    • Modified the entry point to detect syn::Fields::Unnamed (tuple structs).
    • Delegated logic to a new deserialize_tuple_struct_derive function for clear separation from named struct logic.
  • Implemented Tuple Logic

    • Type Checking: The generated type_check method iterates through the provided ColumnSpec slice and the struct fields. It verifies that the database column type matches the expected Rust type for that position. It also verifies that the number of columns matches the number of fields.
    • Deserialization: The generated deserialize method consumes the ColumnIterator sequentially, deserializing each column into the corresponding field of the tuple struct.
  • Validation

    • Ensured that strict column count checks are in place (errors are returned if the database returns too few or too many columns).

What has been tested

Tests

Added a suite of unit tests covering:

  • Single-field wrappers: struct Wrapper(i32) (Newtype pattern).
  • Multi-field tuples: struct MultiTuple(i32, String, f64) to verify correct ordering.
  • Type Safety: Verified that type_check correctly returns an error if the column type (e.g., Text) does not match the field type (e.g., Int).
  • Error Handling: Verified that mismatched column counts (e.g., empty row for a struct requiring fields) return appropriate errors.

Fixes: #852

Pre-review checklist

  • I have split my patch into logically separate commits.
  • All commit messages clearly explain what they change and why.
  • I added relevant tests for new features and bug fixes.
  • All commits compile, pass static checks and pass test.
  • PR description sums up the changes and reasons why they should be introduced.
  • I have provided docstrings for the public items that I want to introduce.
  • I have adjusted the documentation in ./docs/source/.
  • I added appropriate Fixes: annotations to PR description.

@github-actions
Copy link

github-actions bot commented Jan 8, 2026

cargo semver-checks found no API-breaking changes in this PR.
Checked commit: f27ff4d

@ziara1 ziara1 force-pushed the feature/fromrow_for_type_wrapper branch 2 times, most recently from 369eede to bc59c45 Compare January 8, 2026 17:52
@ziara1
Copy link
Author

ziara1 commented Jan 8, 2026

@wprzytula

@ziara1 ziara1 marked this pull request as ready for review January 8, 2026 18:46
@wprzytula wprzytula added the area/proc-macros Related to procedural macros label Jan 8, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the DeserializeRow derive macro to support tuple structs (structs with unnamed fields), enabling users to deserialize database rows into tuple structs like struct Point(f64, f64) or struct UserId(i32) using positional column mapping instead of name-based mapping.

Key changes:

  • Added detection and routing logic for tuple structs in the derive macro entry point
  • Implemented deserialize_tuple_struct_derive function with order-based column-to-field mapping
  • Added test suite covering basic tuple struct scenarios

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
scylla-macros/src/deserialize/row.rs Added tuple struct detection in entry point and new implementation function with type checking and deserialization logic for positional field mapping
scylla-cql/src/types_tests.rs Added test module covering single-field wrappers, multi-field tuples, type checking, and column count validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ziara1 ziara1 force-pushed the feature/fromrow_for_type_wrapper branch from bc59c45 to f27ff4d Compare January 8, 2026 22:35
@ziara1
Copy link
Author

ziara1 commented Jan 9, 2026

@wprzytula

@wprzytula wprzytula added this to the 1.5.0 milestone Jan 12, 2026
Comment on lines +72 to +80
let input: syn::DeriveInput = syn::parse(tokens_input)?;

if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Unnamed(fields),
..
}) = &input.data
{
return deserialize_tuple_struct_derive(&input, fields);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here you are skipping all of normal deserialization implementation.
I get that StructDesc doesn't support unnamed right now, but the solution is to fix that, not skip it altogether. Note that it even has a TODO about it! See lines 60-65 in deserialize/mod.rs. Otherwise you are introducing a lot of duplication, that we will need to handle with every future change to this code, and it also makes testing / review much more difficult.

Copy link
Collaborator

Choose a reason for hiding this comment

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

As an example, when reviewing this I discovered that named impl has a serious bug. The following test fails on main:

    #[derive(DeserializeRow, Debug, PartialEq)]
    #[scylla(crate = "crate")]
    struct GenericConstrainedStruct<T> {
        a: T,
    }

It is because in generated impl we use raw DeserializeValue, instead of using the path from _macro_internal. Fixing this surfaced another issue: we pass only one lifetime to DeserializeValue here, so it still doesn't compile.

I'm sure we have other bugs. I'm sure your impl also has some hidden bugs / missing parts. For example, it doesn't work in the following case:

    trait MyTrait {}

    #[derive(DeserializeRow, Debug, PartialEq)]
    struct GenericTupleConstrained<T>(T)
    where
        T: MyTrait;

If each such issue will need to be handled in multiple locations, fixing such problems will be extremely difficult and frustrating.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks to this PR I started to investigate generics support on our macros, and discovered a lot of bugs :D

Comment on lines +103 to +123
fn check_attributes_for_tuple_structs(attrs: &[syn::Attribute]) -> Result<(), syn::Error> {
for attr in attrs {
if !attr.path().is_ident("scylla") {
continue;
}

attr.parse_nested_meta(|meta| {
if meta.path.is_ident("flavor") || meta.path.is_ident("skip_name_checks") {
return Err(meta.error(
"struct-level attributes `flavor` and `skip_name_checks` are not supported for tuple structs; tuple structs always use order-based matching",
));
}
if meta.input.peek(syn::Token![=]) {
let _ = meta.value()?.parse::<syn::Expr>()?;
}

Ok(())
})?;
}
Ok(())
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this manually parse attrs instead of relying on StructAttrs?

Comment on lines +72 to +80
let input: syn::DeriveInput = syn::parse(tokens_input)?;

if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Unnamed(fields),
..
}) = &input.data
{
return deserialize_tuple_struct_derive(&input, fields);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Even if we retain the current duplicated approach, this is not a proper way to do this. Named and unnamed cases should be handled similarly. Now you treat it as an exceptional case, be having a check and delegating to another function, while the named case is handle here. Better option would be to have a match.

@Lorak-mmk
Copy link
Collaborator

Also, we have one more file with macros tests. See scylla/tests/integration/macros/hygiene.rs. It verifies that we are not depending on specific paths being imported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/proc-macros Related to procedural macros

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FromRow is not working for type wrapper

3 participants