Skip to content

Commit ae2c2a6

Browse files
Bug fix: consolidation failure on dense arrays without respecting current domain (#5390)
This PR addresses a bug in the `Domain::expand_to_tiles(NDRange*)` function, which previously did not respect the current domain. The lack of this consideration led to errors when the tile extent was larger than the current domain, causing out-of-bounds issues during consolidation. To resolve this, the PR introduces a new function that appropriately accounts for the current domain. [sc-59793] and single-cell-data/TileDB-SOMA#3383 Huge kudos to @-johnkerl for getting 99.9% of this done. --- TYPE: BUG DESC: This PR fixes a bug in `Domain::expand_to_tiles(NDRange*)`, which failed to respect the current domain, causing errors when tile extents exceeded it. A new function is introduced to properly account for the current domain during consolidation. --------- Co-authored-by: John Kerl <[email protected]>
1 parent d766b3a commit ae2c2a6

11 files changed

+262
-15
lines changed

test/src/unit-cppapi-consolidation.cc

+50
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*
3030
* Consolidation tests with the C++ API.
3131
*/
32+
#include "tiledb/sm/cpp_api/tiledb_experimental"
3233

3334
#include <test/support/tdb_catch.h>
3435
#include "test/support/src/helpers.h"
@@ -488,3 +489,52 @@ TEST_CASE(
488489
if (vfs.is_dir(array_name))
489490
vfs.remove_dir(array_name);
490491
}
492+
493+
TEST_CASE(
494+
"C++ API: Test consolidation that respects the current domain",
495+
"[cppapi][consolidation][current_domain]") {
496+
std::string array_name = "cppapi_consolidation_current_domain";
497+
remove_array(array_name);
498+
499+
Context ctx;
500+
501+
Domain domain(ctx);
502+
auto d1 = Dimension::create<int32_t>(ctx, "d1", {{0, 1000000000}}, 50);
503+
auto d2 = Dimension::create<int32_t>(ctx, "d2", {{0, 1000000000}}, 50);
504+
domain.add_dimensions(d1, d2);
505+
506+
auto a = Attribute::create<int>(ctx, "a");
507+
508+
ArraySchema schema(ctx, TILEDB_DENSE);
509+
schema.set_domain(domain);
510+
schema.add_attributes(a);
511+
512+
tiledb::NDRectangle ndrect(ctx, domain);
513+
int32_t range_one[] = {0, 2};
514+
int32_t range_two[] = {0, 3};
515+
ndrect.set_range(0, range_one[0], range_one[1]);
516+
ndrect.set_range(1, range_two[0], range_two[1]);
517+
518+
tiledb::CurrentDomain current_domain(ctx);
519+
current_domain.set_ndrectangle(ndrect);
520+
521+
tiledb::ArraySchemaExperimental::set_current_domain(
522+
ctx, schema, current_domain);
523+
524+
Array::create(array_name, schema);
525+
526+
std::vector<int> data = {
527+
-60, 79, -8, 100, 88, -19, -100, -111, -72, -85, 58, -41};
528+
529+
// Write it twice so there is something to consolidate
530+
write_array(array_name, {0, 2, 0, 3}, data);
531+
write_array(array_name, {0, 2, 0, 3}, data);
532+
533+
CHECK(tiledb::test::num_fragments(array_name) == 2);
534+
Context ctx2;
535+
Config config;
536+
REQUIRE_NOTHROW(Array::consolidate(ctx2, array_name, &config));
537+
CHECK(tiledb::test::num_fragments(array_name) == 3);
538+
539+
remove_array(array_name);
540+
}

tiledb/sm/array_schema/array_schema.h

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
#include "tiledb/common/common.h"
4040
#include "tiledb/common/pmr.h"
4141
#include "tiledb/common/status.h"
42+
#include "tiledb/sm/array_schema/current_domain.h"
43+
#include "tiledb/sm/array_schema/domain.h"
4244
#include "tiledb/sm/filesystem/uri.h"
4345
#include "tiledb/sm/filter/filter_pipeline.h"
4446
#include "tiledb/sm/misc/constants.h"

tiledb/sm/array_schema/current_domain.cc

+168
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,174 @@ void CurrentDomain::check_schema_sanity(
240240
}
241241
}
242242

243+
/*
244+
* This is a templatized auxiliary for expand_to_tiles,
245+
* dispatched on the (necessarily integral) type of a given domain slot.
246+
*/
247+
template <typename T>
248+
void expand_to_tiles_aux(
249+
CurrentDomain::dimension_size_type dimidx,
250+
const Dimension* dimptr,
251+
std::shared_ptr<NDRectangle> cur_dom_ndrect,
252+
NDRange& query_ndrange) {
253+
// No extents for string dims, etc.
254+
if constexpr (!std::is_integral_v<T>) {
255+
return;
256+
}
257+
258+
// Find the initial lo/hi for the query range on this dimension.
259+
auto query_slot = query_ndrange[dimidx];
260+
auto query_slot_range = (const T*)query_slot.data();
261+
262+
// Find the lo/hi for the current domain on this dimension.
263+
auto cur_dom_slot_range = (const T*)cur_dom_ndrect->get_range(dimidx).data();
264+
265+
// Find the lo/hi for the core domain (max domain) on this dimension.
266+
auto dim_dom = (const T*)dimptr->domain().data();
267+
268+
// Find the tile extent.
269+
auto tile_extent = *(const T*)dimptr->tile_extent().data();
270+
271+
// Compute tile indices: e.g. if the extent is 512 and the query lo is
272+
// 1027, that's tile 2.
273+
T domain_low = dim_dom[0];
274+
auto tile_idx0 =
275+
Dimension::tile_idx(query_slot_range[0], domain_low, tile_extent);
276+
auto tile_idx1 =
277+
Dimension::tile_idx(query_slot_range[1], domain_low, tile_extent);
278+
279+
// Round up to a multiple of the tile coords. E.g. if the query range
280+
// starts out as (3,4) but the tile extent is 512, that will become (0,511).
281+
T result[2];
282+
result[0] = Dimension::tile_coord_low(tile_idx0, domain_low, tile_extent);
283+
result[1] = Dimension::tile_coord_high(tile_idx1, domain_low, tile_extent);
284+
285+
/*
286+
* Since there is a current domain, though (we assume our caller checks this),
287+
* rounding up to a multiple of the tile extent will lead to an out-of-bounds
288+
* read. Make the query range lo no smaller than current domain lo on this
289+
* dimension, and make the query range hi no larger than current domain hi on
290+
* this dimension.
291+
*/
292+
result[0] = std::max(result[0], cur_dom_slot_range[0]);
293+
result[1] = std::min(result[1], cur_dom_slot_range[1]);
294+
295+
// Update the query range
296+
query_slot.set_range(result, sizeof(result));
297+
}
298+
299+
/* The job here is, for a given domain slot:
300+
* Given query ranges (nominally, for dense consolidation)
301+
* Given the core current domain (which may be empty)
302+
* Given the core (max) domain
303+
* Given initial query bounds
304+
* If the current domain is empty:
305+
* o round the query to tile boundaries
306+
* Else:
307+
* o round the query to tile boundaries, but not outside the current domain
308+
*
309+
* Example on one dim slot:
310+
* - Say non-empty domain is (3,4)
311+
* - Say tile extent is 512
312+
* - Say domain is (0,99999)
313+
* - If current domain is empty: send (3,4) to (0,511)
314+
* - If current domain is (2, 63): send (3,4) to (2,63)
315+
*/
316+
void CurrentDomain::expand_to_tiles(
317+
const Domain& domain, NDRange& query_ndrange) const {
318+
if (query_ndrange.empty()) {
319+
throw std::invalid_argument("Query range is empty");
320+
}
321+
322+
if (this->empty()) {
323+
domain.expand_to_tiles_when_no_current_domain(query_ndrange);
324+
return;
325+
}
326+
327+
if (this->type() != CurrentDomainType::NDRECTANGLE) {
328+
return;
329+
}
330+
331+
auto cur_dom_ndrect = this->ndrectangle();
332+
333+
if (query_ndrange.size() != domain.dim_num()) {
334+
throw std::invalid_argument(
335+
"Query range size does not match domain dimension size");
336+
}
337+
338+
for (CurrentDomain::dimension_size_type dimidx = 0; dimidx < domain.dim_num();
339+
dimidx++) {
340+
const auto dimptr = domain.dimension_ptr(dimidx);
341+
342+
if (dimptr->var_size()) {
343+
continue;
344+
}
345+
346+
if (!dimptr->tile_extent()) {
347+
continue;
348+
}
349+
350+
switch (dimptr->type()) {
351+
case Datatype::INT64:
352+
case Datatype::DATETIME_YEAR:
353+
case Datatype::DATETIME_MONTH:
354+
case Datatype::DATETIME_WEEK:
355+
case Datatype::DATETIME_DAY:
356+
case Datatype::DATETIME_HR:
357+
case Datatype::DATETIME_MIN:
358+
case Datatype::DATETIME_SEC:
359+
case Datatype::DATETIME_MS:
360+
case Datatype::DATETIME_US:
361+
case Datatype::DATETIME_NS:
362+
case Datatype::DATETIME_PS:
363+
case Datatype::DATETIME_FS:
364+
case Datatype::DATETIME_AS:
365+
case Datatype::TIME_HR:
366+
case Datatype::TIME_MIN:
367+
case Datatype::TIME_SEC:
368+
case Datatype::TIME_MS:
369+
case Datatype::TIME_US:
370+
case Datatype::TIME_NS:
371+
case Datatype::TIME_PS:
372+
case Datatype::TIME_FS:
373+
case Datatype::TIME_AS:
374+
expand_to_tiles_aux<int64_t>(
375+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
376+
break;
377+
case Datatype::UINT64:
378+
expand_to_tiles_aux<uint64_t>(
379+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
380+
break;
381+
case Datatype::INT32:
382+
expand_to_tiles_aux<int32_t>(
383+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
384+
break;
385+
case Datatype::UINT32:
386+
expand_to_tiles_aux<uint32_t>(
387+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
388+
break;
389+
case Datatype::INT16:
390+
expand_to_tiles_aux<int16_t>(
391+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
392+
break;
393+
case Datatype::UINT16:
394+
expand_to_tiles_aux<uint16_t>(
395+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
396+
break;
397+
case Datatype::INT8:
398+
expand_to_tiles_aux<int8_t>(
399+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
400+
break;
401+
case Datatype::UINT8:
402+
expand_to_tiles_aux<uint8_t>(
403+
dimidx, dimptr, cur_dom_ndrect, query_ndrange);
404+
break;
405+
default:
406+
break;
407+
}
408+
}
409+
}
410+
243411
} // namespace tiledb::sm
244412

245413
std::ostream& operator<<(

tiledb/sm/array_schema/current_domain.h

+12
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ class CurrentDomain {
207207
*/
208208
void check_schema_sanity(shared_ptr<Domain> schema_domain) const;
209209

210+
/**
211+
* Expands the input query domain (query_ndrange) so that it aligns with the
212+
* boundaries of the array's regular tiles. (i.e., it maps the domain onto the
213+
* regular tile grid) in the same way as
214+
* Domain::expand_to_tiles_when_no_current_domain(NDRange*), but while
215+
* respecting the current domain.
216+
*
217+
* @param domain The domain to be considered.
218+
* @param query_ndrange The query domain to be expanded.
219+
*/
220+
void expand_to_tiles(const Domain& domain, NDRange& query_ndrange) const;
221+
210222
private:
211223
/* ********************************* */
212224
/* PRIVATE ATTRIBUTES */

tiledb/sm/array_schema/domain.cc

+5-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
*/
3232

3333
#include "domain.h"
34+
#include "current_domain.h"
3435
#include "dimension.h"
3536
#include "domain_data_ref.h"
3637
#include "domain_typed_data_view.h"
38+
#include "ndrectangle.h"
3739
#include "tiledb/common/blank.h"
3840
#include "tiledb/common/heap_memory.h"
3941
#include "tiledb/common/logger.h"
@@ -390,12 +392,13 @@ void Domain::expand_ndrange(const NDRange& r1, NDRange* r2) const {
390392
}
391393
}
392394

393-
void Domain::expand_to_tiles(NDRange* ndrange) const {
395+
void Domain::expand_to_tiles_when_no_current_domain(
396+
NDRange& query_ndrange) const {
394397
for (unsigned d = 0; d < dim_num_; ++d) {
395398
const auto dim = dimension_ptrs_[d];
396399
// Applicable only to fixed-sized dimensions
397400
if (!dim->var_size()) {
398-
dim->expand_to_tile(&(*ndrange)[d]);
401+
dim->expand_to_tile(&(query_ndrange)[d]);
399402
}
400403
}
401404
}

tiledb/sm/array_schema/domain.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ class Buffer;
5858
class ConstBuffer;
5959
class Dimension;
6060
class DomainTypedDataView;
61+
class CurrentDomain;
62+
class NDRectangle;
6163
class FilterPipeline;
6264
class MemoryTracker;
6365
enum class Datatype : uint8_t;
@@ -277,8 +279,10 @@ class Domain {
277279
* the array's regular tiles (i.e., it maps it on the regular tile grid).
278280
* If the array has no regular tile grid or real domain, the function
279281
* does not do anything.
282+
*
283+
* @param query_ndrange The query domain to be expanded.
280284
*/
281-
void expand_to_tiles(NDRange* ndrange) const;
285+
void expand_to_tiles_when_no_current_domain(NDRange& query_ndrange) const;
282286

283287
/**
284288
* Retrieves the tile coordinates of the input cell coordinates.

tiledb/sm/consolidator/fragment_consolidator.cc

+4-2
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,8 @@ Status FragmentConsolidator::consolidate_fragments(
431431

432432
// Expand domain to full tiles
433433
auto expanded_union_non_empty_domains = union_non_empty_domains;
434-
domain.expand_to_tiles(&expanded_union_non_empty_domains);
434+
array_for_reads->array_schema_latest().current_domain().expand_to_tiles(
435+
domain, expanded_union_non_empty_domains);
435436

436437
// Now iterate all fragments and see if the consolidation can lead to data
437438
// loss
@@ -756,7 +757,8 @@ Status FragmentConsolidator::create_queries(
756757
if (dense) {
757758
NDRange read_subarray = subarray;
758759
auto& domain{array_for_reads->array_schema_latest().domain()};
759-
domain.expand_to_tiles(&read_subarray);
760+
array_for_reads->array_schema_latest().current_domain().expand_to_tiles(
761+
domain, read_subarray);
760762
throw_if_not_ok(query_r->set_subarray_unsafe(read_subarray));
761763
}
762764

tiledb/sm/fragment/fragment_info.cc

+8-4
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,10 @@ Status FragmentInfo::load(const ArrayDirectory& array_dir) {
912912

913913
// compute expanded non-empty domain (only for dense fragments)
914914
auto expanded_non_empty_domain = non_empty_domain;
915-
if (!sparse)
916-
array_schema->domain().expand_to_tiles(&expanded_non_empty_domain);
915+
if (!sparse) {
916+
array_schema->current_domain().expand_to_tiles(
917+
array_schema->domain(), expanded_non_empty_domain);
918+
}
917919

918920
// Push new fragment info
919921
single_fragment_info_vec_.emplace_back(SingleFragmentInfo(
@@ -1154,8 +1156,10 @@ tuple<Status, optional<SingleFragmentInfo>> FragmentInfo::load(
11541156
// (only for dense fragments)
11551157
const auto& non_empty_domain = meta->non_empty_domain();
11561158
auto expanded_non_empty_domain = non_empty_domain;
1157-
if (!sparse)
1158-
meta->array_schema()->domain().expand_to_tiles(&expanded_non_empty_domain);
1159+
if (!sparse) {
1160+
meta->array_schema()->current_domain().expand_to_tiles(
1161+
meta->array_schema()->domain(), expanded_non_empty_domain);
1162+
}
11591163

11601164
// Set fragment info
11611165
ret = SingleFragmentInfo(

tiledb/sm/fragment/fragment_metadata.cc

+5-4
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ void FragmentMetadata::init_domain(const NDRange& non_empty_domain) {
711711

712712
// Set expanded domain
713713
domain_ = non_empty_domain_;
714-
domain.expand_to_tiles(&domain_);
714+
array_schema_->current_domain().expand_to_tiles(domain, domain_);
715715
}
716716
}
717717

@@ -2116,7 +2116,7 @@ void FragmentMetadata::load_non_empty_domain_v1_v2(Deserializer& deserializer) {
21162116
// Get expanded domain
21172117
if (!non_empty_domain_.empty()) {
21182118
domain_ = non_empty_domain_;
2119-
array_schema_->domain().expand_to_tiles(&domain_);
2119+
array_schema_->domain().expand_to_tiles_when_no_current_domain(domain_);
21202120
}
21212121
}
21222122

@@ -2150,7 +2150,7 @@ void FragmentMetadata::load_non_empty_domain_v3_v4(Deserializer& deserializer) {
21502150
// Get expanded domain
21512151
if (!non_empty_domain_.empty()) {
21522152
domain_ = non_empty_domain_;
2153-
array_schema_->domain().expand_to_tiles(&domain_);
2153+
array_schema_->domain().expand_to_tiles_when_no_current_domain(domain_);
21542154
}
21552155
}
21562156

@@ -2173,7 +2173,8 @@ void FragmentMetadata::load_non_empty_domain_v5_or_higher(
21732173
// Get expanded domain
21742174
if (!non_empty_domain_.empty()) {
21752175
domain_ = non_empty_domain_;
2176-
array_schema_->domain().expand_to_tiles(&domain_);
2176+
array_schema_->current_domain().expand_to_tiles(
2177+
array_schema_->domain(), domain_);
21772178
}
21782179
}
21792180

0 commit comments

Comments
 (0)