Skip to content

[REVIEW] New Dataset API Clarifying Ownership#1846

Open
HowardHuang1 wants to merge 292 commits into
NVIDIA:release/26.08from
HowardHuang1:HH-Dataset-API
Open

[REVIEW] New Dataset API Clarifying Ownership#1846
HowardHuang1 wants to merge 292 commits into
NVIDIA:release/26.08from
HowardHuang1:HH-Dataset-API

Conversation

@HowardHuang1

@HowardHuang1 HowardHuang1 commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Overview

Addressing #1574 and #1571.

Replaced strided_dataset with padded_dataset class. Added support all the way up to CAGRA code.

Old class structure (Classes + Inheritance):

Screenshot 2026-06-23 at 7 06 37 PM

New Class Structure (ContainerType Tags + Composition):

Screenshot 2026-06-23 at 7 07 23 PM

Inheritance is removed entirely and all dataset types are on the same level of the inheritance tree.

3 Levels:

  1. Storage
Screenshot 2026-07-06 at 11 03 41 AM
  1. Container tags
Screenshot 2026-07-06 at 11 03 11 AM
  1. Public dataset/view aliases
Screenshot 2026-06-23 at 7 02 34 PM

Ownership

The index and cagra::build / cagra::index do not own raw vector storage, they only take views.

  • callers (or the C merged holder) must keep backing memory alive for as long as the index is used.

The old code had a type-erased std::unique_ptr<dataset_view<...>>, i.e. non-owning view handles. The new code uses templates on the index type which determines the type of dataset_view the index holds.

ACE v.s. non-ACE paths on Host

ACE path copies datasets that can't entirely fit in CPU memory in chunks onto GPU memory by calling make_padded_dataset. This is 1x memory on CPU and 1x memory on GPU.

Return types:

Used mainly to maintain lifetime of dataset.

cuvs_cagra_c_api_lifetime_holder

  • unique_ptr<vpq_dataset> vpq_owner
  • unique_ptr padded_dataset_owner
  • raft::device_matrix dataset
  • cagra::index idx
    It is a single C++ struct in cagra.cpp that groups the real cagra::index with any extra heap-owned things the C API had to create so the index’s non-owning views stay valid.

Miscellaneous: Extend Serialize Deserialize

Will fill in later

Factories:

  • make_device_padded_dataset_view
  • make_host_padded_dataset_view
  • make_device_padded_dataset
  • make_host_padded_dataset
  • make_vpq_dataset
    • in pq.hpp and pq.cu
  • make_merged_dataset
    • in cagra.hpp

Places where make_padded_dataset/view are called internally (not by user):

Host non-ACE path

  • cpp/src/neighbors/cagra_build_inst.cu.in
  • cagra_from_host_padded in cpp/src/neighbors/iface/iface.hpp
  • c/src/neighbors/cagra.cpp

Tiered CAGRA

  • update_cagra_ann_dataset_for_stride
  • build_upstream_ann

Ownership in Downstream Functions:

  • build() takes dataset_view only.
  • Downstream functions search / serialize / deserialize / merge only take views.

Improvements:

  • build() function previously only supported device dataset inputs. It now supports host dataset inputs.

Breaking Changes for Dataset API:

The following functions are removed since index no longer owns the dataset, index only takes views:

  • Removed all owning dataset based builds. Build only takes views.
  • Removed all update_dataset() overloads that take owning dataset. Update_dataset() only takes views.
    Removed old functions that took mdspan or derivatives of mdspan.

4 cases where index previously owned dataset [all deprecated paths]:

2 edge case build() paths when attach_dataset_on_build == true and a successful dense attach:

  • Non-ACE / typical padded attach: rows live under index_owning_dataset_storage_ (type-erased owning wrapper, commonly device_padded_dataset).
  • ACE in-memory device_matrix attach: rows live under index_owning_dataset_storage_ (optional raw device_matrix).

Compression Param:

  • implicit vpq dataset creation within build() when compression==True
  • this forces index to own new vpq dataset which violates our new contract that we want index to never own dataset.

Merge:

  • MERGE path: merge() internally creates merged_dataset on a deprecated internal merged dataset creation path. Here, index takes ownership of merged_dataset by storing it in index_owning_dataset_storage_ .

These paths have since been removed.

Attach Dataset

  • Previously, in the old code, ACE attach_dataset_on_build called make_device_padded_dataset on host dataset which did a H2D copy in order to attach dataset to final index.
  • cpp/src/neighbors/detail/cagra/cagra_build.cuh
  • This has since been removed. attach_dataset_on_build is disabled for host build paths. This avoids a H2D copy.

Compressed Dataset

  • Removed old code that did compressed dataset creation within build. This should only happen in the factory.

Merged Dataset

  • Removed implicit memory allocation within merge(), memory allocation now delegated to make_merged_dataset() factory. Removed index ownership within merge.

Deserialize

  • Removed index ownership of dataset during deserialization. Now users are expected to create/declare the dataset type to be deserialized and then pass it as a reference to the deserialize() function which will then populate this dataset and return it to the caller.

Helpers

  • cagra_required_row_width
  • matrix_actual_row_width
  • matrix_row_width_matches_cagra_required
  • convert_dataset_view_to_padded_for_graph_build
  • convert_host_to_device_index
  • attach_device_dataset_on_host_index

How to attach a compressed dataset onto an uncompressed index?

  1. construct a new compressed index
  2. copy over the graph and other params from the uncompressed index
  3. delete the old uncompressed index
  4. attach the vpq dataset onto the compressed index

How to attach a searchable device dataset onto an index built with host build?

  1. Convert host index to device index with helper function convert_host_to_device_index
    a. Utilizes map of host dataset type to device dataset type counterpart

TODOs:

  • Bring back Host functions [DONE]
  • Mark any old functions that are no longer used as [DONE]
  • Use templates wherever possible. Shift towards composition rather than inheritance [DONE]

Recent Updates:

  • build_ace() and build() functions merged on Public API surface
  • removed build_result(), ace_build_result(), and merge_result()
  • deprecated internal vpq dataset creation inside build() when index_params::compression == true --> moved to make_vpq_dataset() factory
  • deprecated internal merged dataset creation inside merge() --> moved to make_merged_dataset() factory. For backwards compatibility, have index take ownership of deprecated internal merged dataset creation path ONLY.
  • build() and build_ace() both had a attach_dataset_on_build which requires ownership of dataset. Ownership is given to index temporarily. This will later be deprecated. Users will be expected to pass padded dataset on device and call search() directly. Attach_dataset_on_build will no longer be supported for host builds.
  • added host versions of dataset API
  • templated build() and downstream functions to work on host datasets
  • added index template type conversion helpers

Future PRs:

PR#2: Add Support for Compressed Datasets

  • pq_dataset
  • bbq_dataset
  • rabitq_dataset
  • sq_dataset

PR#3: Migrate Rest of Algorithms to use Dataset API

  • HNSW
  • IVF
  • Vamana
  • Scann
  • Brute Force

@copy-pr-bot

copy-pr-bot Bot commented Feb 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@aamijar aamijar added non-breaking Introduces a non-breaking change feature request New feature or request labels Feb 25, 2026
@aamijar aamijar moved this to In Progress in Unstructured Data Processing Feb 25, 2026
@aamijar

aamijar commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

/ok to test 5447a4c

@aamijar

aamijar commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

/ok to test 17ab09d

@achirkin achirkin added breaking Introduces a breaking change and removed non-breaking Introduces a non-breaking change labels Feb 25, 2026
@achirkin

Copy link
Copy Markdown
Contributor

NB: I updated the label to breaking, since the description implies removal of a publicly visible class strided_dataset

Comment thread cpp/include/cuvs/neighbors/cagra.hpp
@cjnolet

cjnolet commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Does the dataset(_view) type bring anything on top of mdarray/mdspan in that case?

@achirkin The problem w/ using mdspan/mdarray for this is that it's not carrying along the proper information to either the algorithms nor the user (which is why we created this specialized class for this in the first place!).

Two immediate reasons why this API is necessary:

  1. The user should not have to know that they need to pad a dataset in order to use cagra without the additional copy. They should not need to know how any of these algorithms work internally. They should, however, need to know that CAGRA expects a padded dataset, and they should have an API to construct one so that they can own the dataset class and not have cagra creating one under the hood.
  2. APIs, especially the graph-based APIs, should be able to accept as inputs data which has been quantized using a metod like PQ, which carries with additional information. In the case of PQ, the codebooks are needed to compute the distances. This again decouples the quantization from the algorithm (CAGRA-Q does not need to do its own quantization. It should just accept the quantized vectors). We're being asked for the same behavior with Vamana.

This new API solves both of these problems while leaving the control over the memory ownership entirely in the user's hands. We've discussed this for a long time. We've known this is needed for a long time. it's time to prioritize this and get it done. I agree that an anstract class might make more sense, but ultimately we should not be moving any owneship over to the algorithm (the user should maintain ownership over the class and underlying memory the entire time).

Comment thread cpp/CMakeLists.txt Outdated
@HowardHuang1

HowardHuang1 commented Mar 4, 2026

Copy link
Copy Markdown
Contributor Author

The doc that outlines some of the API design choices can be found in slack. Let me know if there are any parts of the design that can be altered to better suit our users' needs.

The following files are test case files I've added and can be ignored for now. They will be removed before the final merge with upstream repo:

  • cagra_build_view_only.cu
  • cagra_padded_dataset.cu
  • cagra_vpq_build_result.cu
  • dataset_compression.cu
  • dataset_types.cu

@HowardHuang1 HowardHuang1 changed the title [WIP] New Dataset API Clarifying Ownership [REVIEW] New Dataset API Clarifying Ownership Mar 25, 2026
@HowardHuang1
HowardHuang1 changed the base branch from main to release/26.04 March 25, 2026 01:03
@HowardHuang1
HowardHuang1 requested review from a team as code owners March 25, 2026 01:22
@HowardHuang1
HowardHuang1 requested a review from bdice March 25, 2026 01:22
@cjnolet

cjnolet commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

/ok to test 70b6b58

Comment thread cpp/include/cuvs/neighbors/cagra.hpp Outdated
Comment thread cpp/include/cuvs/neighbors/cagra.hpp Outdated
@tfeher
tfeher changed the base branch from release/26.04 to main April 1, 2026 23:31

@achirkin achirkin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry for being so late for the second round of reviews! I have a few suggestions for the dataset type hierarchy.

Comment thread cpp/include/cuvs/neighbors/common.hpp Outdated
Comment thread cpp/include/cuvs/neighbors/cagra_index_wrapper.hpp Outdated
Comment thread cpp/include/cuvs/neighbors/common.hpp Outdated
Comment thread cpp/include/cuvs/neighbors/common.hpp Outdated
Comment thread cpp/include/cuvs/neighbors/common.hpp Outdated
* A populated instance is carried inside `merged_dataset_storage` together with the owning
* device matrices allocated by `make_merged_storage`.
*/
struct merged_dataset {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We're removing this aren't we? A merged dataset is just a new dataset with the rows merged. We don't need a new vocab type for this.

*/
template <typename T, typename IdxT>
struct merged_dataset_storage {
merged_dataset layout{};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Need to remove this too... it's dangerous and not maintainable to keep introducing new vocab types for things like this (especially when we already have things thast can store this).

…VIDIA#2128)

## Summary

Small perf cleanup for the filtered brute-force CSR path in `knn_brute_force.cuh`.

- When sparsity ≥ 0.9, filtered brute-force takes the sparse CSR / `masked_matmul` path. As part of setup it builds a per-nonzero `rows` array via `raft::sparse::convert::csr_to_coo`.
- That `rows` array is only consumed by `cuvs::neighbors::detail::epilogue_on_csr`, which only runs for `L2Expanded` / `L2SqrtExpanded` / `CosineExpanded` (it combines masked inner products with precomputed norms).
- For `InnerProduct`, the epilogue is skipped — so the allocation and the `csr_to_coo` kernel launch were dead work on every IP-metric filtered search hitting the CSR path.

This PR hoists the `rmm::device_uvector<IdxT> rows(...)` allocation and the `csr_to_coo` call inside the L2/Cosine branch, next to their only consumer.

## What changes

- No behavior change for `L2Expanded`, `L2SqrtExpanded`, `CosineExpanded` — same kernels, same order.
- For `InnerProduct` filtered searches that hit the sparse CSR branch: saves one device allocation of `nnz * sizeof(IdxT)` and one kernel launch + write pass per call.

## Test plan

- [ ] Existing filtered brute-force tests pass (`BRUTE_FORCE_TEST`, prefiltered variants) for `InnerProduct`, `L2Expanded`, `L2SqrtExpanded`, `CosineExpanded`.
- [x] No diff in output for L2 / Cosine vs. base.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Authors:
  - Max Buckley (https://github.com/maxwbuckley)
  - Micka (https://github.com/lowener)
  - Anupam (https://github.com/aamijar)

Approvers:
  - Micka (https://github.com/lowener)

URL: NVIDIA#2128
Comment thread cpp/include/cuvs/neighbors/cagra.hpp
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<half, uint32_t>;
template <typename T, typename IdxT, cuvs::neighbors::ann_dataset_view DatasetViewT>
merged_dataset_storage<T, IdxT> make_merged_storage(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please remove. We don't need special "storage" objects. We discussed this offline.

auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
std::vector<cuvs::neighbors::cagra::index<int8_t, uint32_t>*>& indices,
std::vector<cuvs::neighbors::cagra::index<T, IdxT, DatasetViewT>*>& indices,
merged_dataset_storage<T, IdxT>& storage,
const cuvs::neighbors::filtering::base_filter& row_filter =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@HowardHuang1 this looks like a proper row filter (input) not an output like I mentioned offline. In that case, the user should be able to apply the filter and know exactly how many items are going in the final dataset.

Comment thread cpp/include/cuvs/neighbors/cagra.hpp Outdated
*
* @internal
*/
struct fd_transfer {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why do we have detail things in the public apis? Please move this to cpp. If the user shouldn't be interacting with it, it doesn't belong in hpp.

typename container_type::template owning_storage<DataT, IdxT, Accessor>;
using owning_storage_type::owning_storage_type;

[[nodiscard]] auto as_dataset_view() const noexcept

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please just call this "view()" to match all the other vocab type.

/** Compressed dataset. */
raft::device_matrix<uint8_t, index_type, raft::row_major> data;

vpq_dataset(raft::device_matrix<math_type, uint32_t, raft::row_major>&& vq_code_book,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not cagra specific. Please move this to the respective preprocessing/pq.hpp file. Each different quantizer sohuld provide their respective dataset implementation so that when imported, the dataset comes w/ the quantizer.

* - IdxT: index type used for row counts (`n_rows()` return type).
*/
template <typename MatrixT, typename ViewT, typename DataT, typename IdxT>
struct dense_row_major_dataset_owning_storage {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed- for a single dense contiguous owning matrix type, we should be using mdarray... nothing else. If we need some type of composite, we should define that upon use... but i'm not seeing an immediate need for that in this PR.

Comment thread c/include/cuvs/neighbors/cagra.h Outdated
Comment thread c/include/cuvs/neighbors/cagra.h Outdated
cuvsDatasetPaddedView_t padded_dataset,
cuvsCagraIndex_t index);

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is a host cagra index? CAGRA is a GPU algorithn. This naming could be described with more clarity. Does "host cagra index" just imply the underlying dataset is on host? Because there is a potential configuration for CAGRA where the graph is always on host too.

Also, in C, we tend to put the verb last- "Make" "Create" "Destroy" "Attach" should all be last.

I would

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes host cagra index just refers to an index where the attached dataset is on host but the index graph itself is on device. Such a dataset is not immediately searchable.

Comment thread c/include/cuvs/neighbors/cagra.h Outdated
HowardHuang1 and others added 7 commits July 23, 2026 11:44
… not correct. Use owning factory first and then create view from that.
…taset() function. Attach_dataset() takes in any index type as input (host vs device, standard vs padded) and a device padded device and always returns a device padded index which is searchable. It internally dispatches on every combination of input index and input dataset type allowed and does internal host to device or standard to padded conversion if needed. We introduce 2 helper functions: convert_host_to_device_index() and convert_standard_to_padded_index(). These 2 helpers along with the original update_dataset(), which only takes device_padded_index and device_padded_dataset() as input, are internally called within attach_dataset() within each of the dispatch cases.
Comment thread c/src/neighbors/cagra.cpp
Comment on lines +1174 to +1176
extern "C" cuvsError_t cuvsDatasetMakeDevicePadded(cuvsResources_t res,
DLManagedTensor* dataset_tensor,
cuvsDatasetPadded_t* padded_dataset)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All those new cuvsDataset... functions should be in a separate compilation unit (src/core/dataset.cpp) (Not blocking this PR though, it can be a follow-up)

DLDataType dtype;
cuvsDatasetLayout_t layout;
} cuvsDatasetStandard;
typedef cuvsDatasetStandard* cuvsDatasetStandard_t;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The two structs are similar, can't there be just one struct definition and two typedefs? They already represent the same thing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same with the view struct just below

void (*destroy_addr)(void*);
cuvsDatasetViewKind_t kind;
DLDataType dtype;
cuvsDatasetLayout_t layout;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

layout is redundant if kind is already present in the struct

cuvsCagraIndexParams_t params,
DLManagedTensor* dataset,
cuvsCagraIndex_t index);
CUVS_EXPORT cuvsError_t cuvsCagraGetDatasetViewKind(DLManagedTensor* dataset,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any reason this needs to be exposed to the public?

CUVS_EXPORT cuvsError_t cuvsCagraDeserializePadded(cuvsResources_t res,
const char* filename,
cuvsCagraIndex_t index,
cuvsDatasetPadded_t* out_padded_dataset);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question- why isn't the dataset type serialized with the cagra graph (in the even the user wants that serialized with the cagra graph)? This way we can just do "cuvsCagraDeserializeWithDataset" (which throws an error if the serialized file doesn't contain the dataset) and cuvsCagraDeserialize (which doesn't deserialize the dataset, even if it's in the file).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pre-allocating the dataset buffer gives us 2 advantages: (1) no hidden memory allocations, (2) deterministic dataset type.

If we are pre-allocating the dataset buffer, I think we can just parse the type from that directly so there wouldn't be a need to modify the serialize algo itself to serialize the dataset type along with the graph.

If the proposal is to remove the passing in of the dataset buffer entirely in favor of serializing the dataset type with the cagra graph (which more closely mirrors the original serialize function), we would lose the beenfit of advantage (1).

typedef enum {
CUVS_DATASET_VIEW_KIND_DEVICE_PADDED = 0,
CUVS_DATASET_VIEW_KIND_HOST_PADDED = 1,
CUVS_DATASET_VIEW_KIND_DEVICE_STANDARD = 2,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of view kind containing both host and device for each dataset type (this is going to quickly become unmaintainable), please just define the accessor type (HOST and DEVICE) here and reuse the dataset layout enum above.

DLDataType dtype;
cuvsDatasetLayout_t layout;
} cuvsDatasetPadded;
typedef cuvsDatasetPadded* cuvsDatasetPadded_t;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We discussed this offline- we should probably just make a single dataset struct and use the enums to parameterize it so that we know what kind to cast it to in the C++ layer.

…update_dataset() that only updated standard index with standard dataset or padded index with padded dataset() was renamed update_device_dataset_same_layout(). New centralized cuvsCagraUpdateDataset at C API layer was created which internally dispatches to cuvsCagraUpdateDeviceDatasetSameLayout if index type is device_padded_index. For all other index types, it dispatches internally to cuvsCagraAttachDataset. This resolves the mutability issue we were previously experiencing where cuvsCagraAttachDataset guarantees original input index is immutable and returns a new index of new type while cuvsCagraUpdateDeviceDatasetSameLayout requires mutating the input dataset and makes no such index copy. This is not implemented at the C++ API layer yet, it only exists in the C API layer since C API doesn't make immutability guarantees while C++ API does and tries to keep C++ logic immutable to prevent thread conflicts. Downstream langauge wrappers have been updated to call centralized cuvsCagraUpdateDataset as the single sole entry point for all cases of updating/attaching a dataset
…ut from exports in C API layer and remove from downstream wrappers. They are not meant to be exposed to users/callers. Users/callers should use cuvsCagraUpdateDataset as the only C API function they interface with when updating/attaching a dataset
Comment thread c/src/neighbors/cagra.cpp
Comment on lines +2109 to +2119
_deserialize_padded<float>(res, filename, index, out_padded_dataset);
index->dtype.code = kDLFloat;
} else if (dtype.kind == 'e' && dtype.itemsize == 2) {
_deserialize_padded<half>(res, filename, index, out_padded_dataset);
index->dtype.code = kDLFloat;
} else if (dtype.kind == 'i' && dtype.itemsize == 1) {
_deserialize_padded<int8_t>(res, filename, index, out_padded_dataset);
index->dtype.code = kDLInt;
} else if (dtype.kind == 'u' && dtype.itemsize == 1) {
_deserialize_padded<uint8_t>(res, filename, index, out_padded_dataset);
index->dtype.code = kDLUInt;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think there's a bug here in the order of these ops. The _deserialize_padded copies the index's dtype into the handle it returns:

out->dtype     = output_index->dtype;

So the dtype should already be set when it's called. Therefore, we should flip the two lines for each dtype kind branch:

index->dtype.code = ...
_deserialize_padded...

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

Labels

breaking Introduces a breaking change feature request New feature or request

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.