Skip to content

Add support for feature-gated columns in table! macro#4961

Open
LucaCappelletti94 wants to merge 3 commits intodiesel-rs:mainfrom
LucaCappelletti94:gated-columns
Open

Add support for feature-gated columns in table! macro#4961
LucaCappelletti94 wants to merge 3 commits intodiesel-rs:mainfrom
LucaCappelletti94:gated-columns

Conversation

@LucaCappelletti94
Copy link
Contributor

This PR adds support for placing #[cfg(...)] attributes on columns within the table! macro. This allows users to conditionally include columns based on feature flags, which is useful when certain columns are variable, like when the underlying schema changes with certain versions and we want to support several versions (today's PR triggering example for me was Postgres 18 and 17).

Resolves discussion #4922

Example Usage

table! {
    users {
        id -> Integer,
        name -> Text,
        #[cfg(feature = "chrono")]
        created_at -> Timestamp,
        #[cfg(feature = "uuid")]
        user_uuid -> Uuid,
    }
}

Implementation Details

Rust doesn't allow #[cfg] attributes on tuple type elements directly. This means we can't simply write:

// This doesn't work!
pub type SqlType = (
    Integer,
    Text,
    #[cfg(feature = "chrono")]
    Timestamp,
);

For n distinct cfg groups, we generate 2^n variants of all aggregate types, each guarded by the appropriate combination of #[cfg(all(...))] conditions:

#[cfg(all(not(feature = "chrono"), not(feature = "uuid")))]
pub const all_columns: (id, name) = (id, name);

#[cfg(all(feature = "chrono", not(feature = "uuid")))]
pub const all_columns: (id, name, created_at) = (id, name, created_at);

#[cfg(all(not(feature = "chrono"), feature = "uuid"))]
pub const all_columns: (id, name, user_uuid) = (id, name, user_uuid);

#[cfg(all(feature = "chrono", feature = "uuid"))]
pub const all_columns: (id, name, created_at, user_uuid) = (id, name, created_at, user_uuid);

The following are generated with combinatorial variants:

  • all_columns constant
  • SqlType type alias
  • impl diesel::Table for table (specifically AllColumns associated type)
  • impl ValidGrouping<__GB> for star

All trait implementations for individual columns (e.g., Expression, QueryFragment, SelectableExpression, AppearsOnTable, operator impls) are wrapped with the column's #[cfg(...)] attributes so they only exist when the feature is enabled.

The IsContainedInGroupBy cross-product impls between column pairs include cfg attributes from both columns.

New Helper Functions

Added and documented:

  • CfgGroup - Groups columns by their cfg condition
  • collect_cfg_groups() - Organizes columns into cfg groups
  • generate_combined_cfg_condition() - Creates #[cfg(all(...))] with enabled/disabled conditions
  • generate_aggregate_variants() - Generates 2^n variants of all_columns, SqlType, ValidGrouping
  • generate_kind_specific_impls_variants() - Generates 2^n variants of Table/QueryRelation impls

Tests Added

  • Snapshot tests for single and multiple feature-gated columns
  • Type-checking tests that verify the generated code contains:
    • Correct cfg guards for all combinations
    • Correct column tuples in all_columns for each variant
    • Correct types in SqlType for each variant
    • Correct AllColumns associated type for each variant
    • Correct ValidGrouping bounds for star

Limitations

With many distinct cfg groups, (feature-gated) code size grows exponentially (2^n). This is an inherent limitation of Rust's cfg system. In practice, most tables will have at most 1-2 distinct feature groups, keeping the generated code manageable.

@LucaCappelletti94
Copy link
Contributor Author

This PR should be ready for review.

The Windows 2025 + PostgreSQL CI failure is unrelated to these changes - it's a linker error (LNK1181: cannot open input file 'libpq.lib') indicating the PostgreSQL client library wasn't found on the CI runner. The PR changes only modify proc-macro code generation and don't affect native library linking. This is likely to be a transient CI infrastructure issue.

@LucaCappelletti94 LucaCappelletti94 marked this pull request as ready for review February 3, 2026 17:58
@weiznich weiznich requested a review from a team February 3, 2026 19:42
@LucaCappelletti94
Copy link
Contributor Author

I have just slammed against the case of pg_catalog.pg_auth_members (oid) vs pg_catalog.pg_auth_members (roleid, member).

I guess that while it is somewhat reasonable to feature gate the single columns, I suppose that if the primary key changes such as in this case, one really needs to feature gate the entire table.

Do you have any better idea?

Copy link
Member

@weiznich weiznich left a comment

Choose a reason for hiding this comment

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

I'm not really in favour introducing a feature that allows users to generate significantly more code in the expansion of a table macro like this.

I've left a few remarks how that could be stripped down + what I would rather like to prefer doing instead even if that's blocked on future language development.

This is not a full review yet, just a dump of related thoughs


/// Generates all combinatorial variants of aggregate types for feature-gated columns.
///
/// This function generates 2^n variants of `all_columns`, `AllColumns`, and `SqlType`
Copy link
Member

Choose a reason for hiding this comment

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

I would rather like to avoid generating an exponential amount of instances to any user input. The cost of compiling large schemas is already large enough, so I rather do not want to introduce another feature to make this even worse. Either we find a different solution to the underlying problem or we need to accept that certain things require a bit more code (e.g. require the user to put a cfg on the table itself and not only on a specific column.)

For reference there is an open RFC to possible allow putting #[cfg] on tuple types like required to solve this in a better way: rust-lang/rfcs#3532 There are no concerns listed there and there seems to be a slight push to accept that (at least a few months back). So it might be worth to rather push for accepting this RFC and then just implementing the relevant feature.
Other than that, given that this is already allowed for tuple expressions it should be rather simple to get the lang team to also accept it for types. Also it shouldn't be too hard to stabilize this, as there is not much that could go wrong there as far as I can tell.

let mut conditions: Vec<TokenStream> = Vec::new();

for (i, group) in groups.iter().enumerate() {
let is_enabled = (enabled_mask & (1 << i)) != 0;
Copy link
Member

Choose a reason for hiding this comment

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

It's probably not worth to use a bitmask here compared to just a Vec<boo>. The later is better readable and the code shouldn't be that performance critical to need to safe that few bytes of memory.

Comment on lines +313 to +324
impl diesel::Table for table {
type PrimaryKey = #primary_key;
type AllColumns = (#(#column_names,)*);

fn primary_key(&self) -> Self::PrimaryKey {
#primary_key
}

fn all_columns() -> Self::AllColumns {
(#(#column_names,)*)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

There is no reason to duplicate al of that. Only the two associated types need to be put behind a cfg flag. For tuple expressions it's allowed to put a field behind a cfg flag.

@LucaCappelletti94
Copy link
Contributor Author

To illustrate concretely how I am using this, here are a few table! macro calls which without this new functionality were rather unmaintainable:

I didn't notice a particular increase to the (already rather long) compile times, and this schema I think is rather large for my standards.

While I agree that it is always desirable to avoid increase of compile time imposed by default in general, here I feel it is more of a pay-as-you-use-it type of feature. Tables without feature gates should not experience any added cost, and the code that a person needs to edit is exponentially less (before it was 2^n where n is the number of feature gates).

Of course, it is very regrettable that the language at this time does not allow for a cleaner solution.

I will apply the suggested changes and see if I can further trim it down.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants