From 1d0675dd3ab809db562b61eb3c374011c104bdcb Mon Sep 17 00:00:00 2001 From: Pearu Peterson Date: Fri, 13 Jan 2023 09:36:29 +0200 Subject: [PATCH 1/7] Support GeoMultiPolygon input and output column arguments in UDTFs (#7136) * Support GeoMultiPolygon input and output column arguments in UDTFs * Add complete tests for GeoPolygon and GeoMultiPolygon. Fix various issues. Signed-off-by: jack --- QueryEngine/ColumnarResults.cpp | 213 +- QueryEngine/InputMetadata.cpp | 14 +- QueryEngine/ResultSetIteration.cpp | 107 + .../TableFunctions/TableFunctionManager.h | 10 +- .../TestFunctions/GeoTestTableFunctions.cpp | 290 ++- .../TestFunctions/TableFunctionsTesting.h | 11 + QueryEngine/TargetExprBuilder.cpp | 3 +- QueryEngine/Utils/FlatBuffer.h | 1719 ++++++++++++++++- QueryEngine/heavydbTypes.h | 527 ++++- Shared/sqltypes.h | 280 +++ Shared/sqltypes_lite.h | 66 + Tests/TableFunctionsTest.cpp | 176 +- 12 files changed, 3247 insertions(+), 169 deletions(-) create mode 100644 Shared/sqltypes_lite.h diff --git a/QueryEngine/ColumnarResults.cpp b/QueryEngine/ColumnarResults.cpp index ae932777b7..97b41c0d69 100644 --- a/QueryEngine/ColumnarResults.cpp +++ b/QueryEngine/ColumnarResults.cpp @@ -204,9 +204,10 @@ int64_t countNumberOfValuesGeoPolygon(const ResultSet& rows, CHECK(ns); const auto s_ptr = boost::get(ns); if (s_ptr) { - // We count the number of commas and parenthesis in WKT representation - // of a polygon (e.g. POLYGON ((0 0,4 0,4 4,0 4,0 0),(1 1,1 2,2 2,2 1,1 1))) - // to get the number of points it contains: + // We count the number of commas in WKT representation + // of a polygon (e.g. POLYGON ((0 0,4 0,4 4,0 4,0 0),(1 + // 1,1 2,2 2,2 1,1 1))) to get the number of points it + // contains: running_count += std::count(s_ptr->begin(), s_ptr->end(), ',') + 1; } } else if (const auto tv = @@ -220,10 +221,60 @@ int64_t countNumberOfValuesGeoPolygon(const ResultSet& rows, : sizeof(double)) / 2; } else if (const auto tv = boost::get(&crt_row[column_idx])) { - const auto s = boost::get(tv->get()); - std::vector* d = s.coords.get(); - CHECK(d); - running_count += d->size(); + if (tv->get_ptr() != nullptr) { + const auto s = boost::get(tv->get()); + std::vector* d = s.coords.get(); + CHECK(d); + running_count += d->size(); + } // else s is NULL + } else { + UNREACHABLE(); + } + } + return running_count; + }, + std::plus()); +} + +int64_t countNumberOfValuesGeoMultiPolygon(const ResultSet& rows, + const SQLTypeInfo& ti, + const size_t column_idx) { + return tbb::parallel_reduce( + tbb::blocked_range(0, rows.rowCount()), + static_cast(0), + [&](tbb::blocked_range r, int64_t running_count) { + for (int i = r.begin(); i < r.end(); ++i) { + const auto crt_row = rows.getRowAtNoTranslations(i); + if (const auto tv = boost::get(&crt_row[column_idx])) { + const auto ns = boost::get(tv); + CHECK(ns); + const auto s_ptr = boost::get(ns); + if (s_ptr && *s_ptr != "NULL") { + // We count the number of commas in WKT representation + // of a multi-polygon (e.g. MULTIPOLYGON (((0 0,4 0,4 + // 4,0 4,0 0),(1 1,1 2,2 2,2 1,1 1)), (...))) to get + // the number of points it contains: + running_count += std::count(s_ptr->begin(), s_ptr->end(), ',') + 1; + } + } else if (const auto tv = + boost::get(&crt_row[column_idx])) { + const auto s = boost::get(tv); + CHECK(s); + if (s->coords_data != nullptr && s->coords_data->pointer != nullptr) { + VarlenDatum* d = s->coords_data.get(); + if (ti.get_compression() == kENCODING_GEOINT) { + running_count += d->length / (2 * sizeof(int32_t)); + } else { + running_count += d->length / (2 * sizeof(double)); + } + } // else s is NULL + } else if (const auto tv = boost::get(&crt_row[column_idx])) { + if (tv->get_ptr() != nullptr) { + const auto s = boost::get(tv->get()); + std::vector* d = s.coords.get(); + CHECK(d); + running_count += d->size() / 2; + } // else s is NULL } else { UNREACHABLE(); } @@ -283,10 +334,14 @@ ColumnarResults::ColumnarResults(std::shared_ptr row_set_mem_ case kPOLYGON: values_count = countNumberOfValuesGeoPolygon(rows, ti, i); break; + case kMULTIPOLYGON: + values_count = countNumberOfValuesGeoMultiPolygon(rows, ti, i); + break; default: UNREACHABLE() << "count number of values not implemented for " << ti.toString(); } + // TODO: include sizes count to optimize flatbuffer size const int64_t flatbuffer_size = getFlatBufferSize(num_rows_, values_count, ti); column_buffers_[i] = row_set_mem_owner->allocate(flatbuffer_size, thread_idx_); FlatBufferManager m{column_buffers_[i]}; @@ -670,7 +725,7 @@ inline void ColumnarResults::writeBackCell(const TargetValue& col_val, const auto ns = boost::get(tv); CHECK(ns); const auto s_ptr = boost::get(ns); - if (s_ptr == nullptr) { + if (s_ptr == nullptr || *s_ptr == "NULL") { auto lock_scope = (write_mutex == nullptr ? std::unique_lock() : std::unique_lock(*write_mutex)); @@ -725,26 +780,134 @@ inline void ColumnarResults::writeBackCell(const TargetValue& col_val, } CHECK_EQ(status, FlatBufferManager::Status::Success); } else if (const auto tv = boost::get(&col_val)) { - /* - Warning: the following code fails for NULL row values - because of the failure to detect the nullness correctly. - */ - const auto s = boost::get(tv->get()); - const std::vector* d = s.coords.get(); - const std::vector* r = s.ring_sizes.get(); - CHECK(d); - CHECK(r); - std::vector compressed_coords = - Geospatial::compress_coords(*d, type_info); - { + if (tv->get_ptr() == nullptr) { auto lock_scope = (write_mutex == nullptr ? std::unique_lock() : std::unique_lock(*write_mutex)); - status = m.setItemCountsAndData( - row_idx, - r->data(), - r->size(), - reinterpret_cast(compressed_coords.data())); + status = m.setNull(row_idx); + } else { + const auto s = boost::get(tv->get()); + const std::vector* d = s.coords.get(); + const std::vector* r = s.ring_sizes.get(); + CHECK(d); + CHECK(r); + if (d->size() == 0) { + CHECK_EQ(r->size(), 0); + auto lock_scope = + (write_mutex == nullptr ? std::unique_lock() + : std::unique_lock(*write_mutex)); + status = m.setNull(row_idx); + } else { + std::vector compressed_coords = + Geospatial::compress_coords(*d, type_info); + { + auto lock_scope = + (write_mutex == nullptr ? std::unique_lock() + : std::unique_lock(*write_mutex)); + status = m.setItemCountsAndData( + row_idx, + r->data(), + r->size(), + reinterpret_cast(compressed_coords.data())); + } + } + } + CHECK_EQ(status, FlatBufferManager::Status::Success); + } else { + UNREACHABLE(); + } + break; + } + case kMULTIPOLYGON: { + CHECK(FlatBufferManager::isFlatBuffer(column_buffers_[column_idx])); + FlatBufferManager m{column_buffers_[column_idx]}; + const SQLTypeInfoLite* ti_lite = + reinterpret_cast(m.get_user_data_buffer()); + if (ti_lite->compression == SQLTypeInfoLite::GEOINT) { + CHECK_EQ(type_info.get_compression(), kENCODING_GEOINT); + } else { + CHECK_EQ(type_info.get_compression(), kENCODING_NONE); + } + FlatBufferManager::Status status{}; + if (const auto tv = boost::get(&col_val)) { + const auto ns = boost::get(tv); + CHECK(ns); + const auto s_ptr = boost::get(ns); + if (s_ptr == nullptr || *s_ptr == "NULL") { + auto lock_scope = + (write_mutex == nullptr ? std::unique_lock() + : std::unique_lock(*write_mutex)); + status = m.setNull(row_idx); + } else { + std::vector coords; + std::vector ring_sizes; + std::vector polygon_sizes; + std::vector bounds; + int64_t approx_nof_coords = 2 * std::count(s_ptr->begin(), s_ptr->end(), ','); + int64_t approx_nof_rings = std::count(s_ptr->begin(), s_ptr->end(), '(') - 1; + int64_t approx_nof_polygons = approx_nof_rings; // upper bound + coords.reserve(approx_nof_coords); + ring_sizes.reserve(approx_nof_rings); + polygon_sizes.reserve(approx_nof_polygons); + bounds.reserve(4); + const auto gdal_wkt_ls = Geospatial::GeoMultiPolygon(*s_ptr); + gdal_wkt_ls.getColumns(coords, ring_sizes, polygon_sizes, bounds); + const std::vector compressed_coords = + Geospatial::compress_coords(coords, type_info); + { + auto lock_scope = + (write_mutex == nullptr ? std::unique_lock() + : std::unique_lock(*write_mutex)); + status = m.setItem(row_idx, compressed_coords, ring_sizes, polygon_sizes); + } + } + CHECK_EQ(status, FlatBufferManager::Status::Success); + } else if (const auto tv = boost::get(&col_val)) { + const auto s = boost::get(tv); + CHECK(s); + if (s->coords_data == nullptr || s->coords_data->pointer == nullptr) { + status = m.setNull(row_idx); + } else { + const VarlenDatum* d = s->coords_data.get(); + const VarlenDatum* r = s->ring_sizes_data.get(); + const VarlenDatum* p = s->poly_rings_data.get(); + CHECK(d); + CHECK(d->pointer); + { + auto lock_scope = + (write_mutex == nullptr ? std::unique_lock() + : std::unique_lock(*write_mutex)); + status = m.setItem(row_idx, + d->pointer, + d->length, + reinterpret_cast(r->pointer), + r->length / sizeof(int32_t), + reinterpret_cast(p->pointer), + p->length / sizeof(int32_t)); + } + } + CHECK_EQ(status, FlatBufferManager::Status::Success); + } else if (const auto tv = boost::get(&col_val)) { + if (tv->get_ptr() == nullptr) { + auto lock_scope = + (write_mutex == nullptr ? std::unique_lock() + : std::unique_lock(*write_mutex)); + status = m.setNull(row_idx); + } else { + const auto s = boost::get(tv->get()); + const std::vector* d = s.coords.get(); + const std::vector* r = s.ring_sizes.get(); + const std::vector* p = s.poly_rings.get(); + CHECK(d); + CHECK_NE(d->size(), 0); + std::vector compressed_coords = + Geospatial::compress_coords(*d, type_info); + { + auto lock_scope = + (write_mutex == nullptr ? std::unique_lock() + : std::unique_lock(*write_mutex)); + status = m.setItem(row_idx, compressed_coords, *r, *p); + } } CHECK_EQ(status, FlatBufferManager::Status::Success); } else { diff --git a/QueryEngine/InputMetadata.cpp b/QueryEngine/InputMetadata.cpp index 23cf3f628a..82f3c759fb 100644 --- a/QueryEngine/InputMetadata.cpp +++ b/QueryEngine/InputMetadata.cpp @@ -266,14 +266,20 @@ ChunkMetadataMap synthesize_metadata_table_function(const ResultSet* rows) { FlatBufferManager m{const_cast(columnar_buffer)}; chunk_metadata->numBytes = m.getBufferSize(); if (is_geometry) { - // a geometry value is a pair of coordinates but its element - // type value is a int or double, hence multiplication by 2: - values_count = m.get_nof_values() * 2; + if (col_sql_type_info.get_type() == kMULTIPOLYGON) { + values_count = m.getValuesCount(); + values_buffer = m.get_values_buffer(); + } else { + // a geometry value is a pair of coordinates but its element + // type value is a int or double, hence multiplication by 2: + values_count = m.get_nof_values() * 2; + values_buffer = m.get_values(); + } } else { CHECK(is_array); values_count = m.get_nof_values(); + values_buffer = m.get_values(); } - values_buffer = m.get_values(); } else { chunk_metadata->numBytes = row_count * col_type_info.get_size(); values_count = row_count; diff --git a/QueryEngine/ResultSetIteration.cpp b/QueryEngine/ResultSetIteration.cpp index 909396c0a7..6ba47194fd 100644 --- a/QueryEngine/ResultSetIteration.cpp +++ b/QueryEngine/ResultSetIteration.cpp @@ -1875,6 +1875,113 @@ TargetValue ResultSet::makeGeoTargetValue(const int8_t* geo_target_ptr, varlen_buffer[getCoordsDataPtr(geo_target_ptr) + 2].size())); } else if (col_lazy_fetch && col_lazy_fetch->is_lazily_fetched) { const auto& frag_col_buffers = getFragColBuffers(); + auto ptr = frag_col_buffers[col_lazy_fetch->local_col_id]; + if (FlatBufferManager::isFlatBuffer(ptr)) { + FlatBufferManager m{const_cast(ptr)}; + const SQLTypeInfoLite* ti_lite = + reinterpret_cast(m.get_user_data_buffer()); + if (ti_lite->compression == SQLTypeInfoLite::GEOINT) { + CHECK_EQ(target_info.sql_type.get_compression(), kENCODING_GEOINT); + } else { + CHECK_EQ(target_info.sql_type.get_compression(), kENCODING_NONE); + } + int64_t index = getCoordsDataPtr(geo_target_ptr); + bool is_null; + std::vector coords; + std::vector rings; + std::vector poly_rings; + int8_t* points; + int32_t nof_points; + int8_t* ring_sizes; + int32_t nof_rings; + int8_t* polygon_sizes; + int32_t nof_polygons; + if (ResultSet::GeoReturnType::WktString == geo_return_type_) { + if (ti_lite->compression == SQLTypeInfoLite::GEOINT) { + std::vector ccoords; + m.getItem(index, ccoords, rings, poly_rings, is_null); + coords = *decompress_coords( + target_info.sql_type, + reinterpret_cast(ccoords.data()), + ccoords.size() * sizeof(int32_t)); + } else { + m.getItem(index, coords, rings, poly_rings, is_null); + } + if (is_null) { + return NullableString("NULL"); + } + Geospatial::GeoMultiPolygon mpoly(coords, rings, poly_rings); + return NullableString(mpoly.getWktString()); + } else if (ResultSet::GeoReturnType::GeoTargetValuePtr == geo_return_type_) { + m.getItem(index, + nof_points, + points, + nof_rings, + ring_sizes, + nof_polygons, + polygon_sizes, + is_null); + if (is_null) { + return GeoMultiPolyTargetValuePtr(); + } + auto coords = std::make_shared( + nof_points * m.getValueSize(), points, false); + auto rings = + std::make_shared(nof_rings * sizeof(int32_t), + reinterpret_cast(ring_sizes), + false); + auto poly_rings = + std::make_shared(nof_polygons * sizeof(int32_t), + reinterpret_cast(polygon_sizes), + false); + return GeoMultiPolyTargetValuePtr( + {std::move(coords), std::move(rings), std::move(poly_rings)}); + } else if (ResultSet::GeoReturnType::GeoTargetValue == geo_return_type_) { + if (ti_lite->compression == SQLTypeInfoLite::GEOINT) { + std::vector ccoords; + m.getItem(index, ccoords, rings, poly_rings, is_null); + if (is_null) { + return GeoTargetValue(); + } + coords = *decompress_coords( + target_info.sql_type, + reinterpret_cast(ccoords.data()), + ccoords.size() * sizeof(int32_t)); + } else { + m.getItem(index, coords, rings, poly_rings, is_null); + if (is_null) { + return GeoTargetValue(); + } + } + return GeoTargetValue(GeoMultiPolyTargetValue(coords, rings, poly_rings)); + } else if (ResultSet::GeoReturnType::GeoTargetValueGpuPtr == geo_return_type_) { + m.getItem(index, + nof_points, + points, + nof_rings, + ring_sizes, + nof_polygons, + polygon_sizes, + is_null); + if (is_null) { + return GeoMultiPolyTargetValuePtr(); + } + auto coords = std::make_shared( + nof_points * m.getValueSize(), points, false); + auto rings = + std::make_shared(nof_rings * sizeof(int32_t), + reinterpret_cast(ring_sizes), + false); + auto poly_rings = + std::make_shared(nof_polygons * sizeof(int32_t), + reinterpret_cast(polygon_sizes), + false); + return GeoMultiPolyTargetValuePtr( + {std::move(coords), std::move(rings), std::move(poly_rings)}); + } else { + UNREACHABLE(); + } + } return GeoTargetValueBuilder::build( target_info.sql_type, diff --git a/QueryEngine/TableFunctions/TableFunctionManager.h b/QueryEngine/TableFunctions/TableFunctionManager.h index be4116f46c..47fe1bc314 100644 --- a/QueryEngine/TableFunctions/TableFunctionManager.h +++ b/QueryEngine/TableFunctions/TableFunctionManager.h @@ -147,7 +147,8 @@ struct TableFunctionManager { switch (ti.get_type()) { case kARRAY: case kLINESTRING: - case kPOLYGON: { + case kPOLYGON: + case kMULTIPOLYGON: { if (output_item_values_total_number_[i] == -1) { throw std::runtime_error("set_output_item_values_total_number(" + std::to_string(i) + @@ -221,7 +222,8 @@ struct TableFunctionManager { switch (ti.get_type()) { case kARRAY: case kLINESTRING: - case kPOLYGON: { + case kPOLYGON: + case kMULTIPOLYGON: { total_number = output_item_values_total_number_[i]; break; } @@ -232,6 +234,10 @@ struct TableFunctionManager { << ti.toString(); } initializeFlatBuffer(m, output_num_rows_, total_number, ti); + CHECK(FlatBufferManager::isFlatBuffer(output_buffers_ptr)); + // Checks if the implementations of getFlatBufferSize and + // initializeFlatBuffer in sqltypes.h are in sync: + CHECK_EQ(m.getBufferSize(), query_mem_desc.getFlatBufferSize(i)); output_buffers_ptr = align_to_int64(output_buffers_ptr + m.getBufferSize()); } else { const size_t col_width = ti.get_size(); diff --git a/QueryEngine/TableFunctions/TestFunctions/GeoTestTableFunctions.cpp b/QueryEngine/TableFunctions/TestFunctions/GeoTestTableFunctions.cpp index e360745a35..29b9088fa4 100644 --- a/QueryEngine/TableFunctions/TestFunctions/GeoTestTableFunctions.cpp +++ b/QueryEngine/TableFunctions/TestFunctions/GeoTestTableFunctions.cpp @@ -127,10 +127,13 @@ EXTENSION_NOINLINE int32_t ct_make_polygon3__cpu_(TableFunctionManager& mgr, mgr.set_output_item_values_total_number( 0, rings.getNofValues() + holes1.getNofValues() + holes2.getNofValues()); mgr.set_output_row_size(size); + // Initialize polygons + int count_nulls = 0; for (int64_t i = 0; i < size; i++) { if (rings.isNull(i)) { polygons.setNull(i); sizes.setNull(i); + count_nulls++; } else { std::vector> polygon_coords; @@ -145,12 +148,124 @@ EXTENSION_NOINLINE int32_t ct_make_polygon3__cpu_(TableFunctionManager& mgr, return mgr.ERROR_MESSAGE("fromCoords failed: " + ::toString(status)); } int nofpoints = 0; - for (int j = 0; j < polygon.size(); j++) { + for (size_t j = 0; j < polygon.size(); j++) { nofpoints += polygon.size(j); } sizes[i] = nofpoints; } } + + // Check polygons content + if (count_nulls == 0) { + return mgr.ERROR_MESSAGE("counting null test failed: count_nulls=" + + ::toString(count_nulls) + ", expected non-zero."); + } + + for (int64_t i = 0; i < size; i++) { + if (polygons.isNull(i)) { + count_nulls--; + } else { + std::vector> polygon_coords; + polygon_coords.push_back(rings[i].toCoords()); + if (!holes1.isNull(i)) { + polygon_coords.push_back(holes1[i].toCoords()); + } + if (!holes2.isNull(i)) { + polygon_coords.push_back(holes2[i].toCoords()); + } + + // polygons[i] is Geo::Polygon instances + // polygons[i][j] is Geo::LineString instances + // polygons[i][j][k] is Geo::Point2D instances + + auto nof_lines = polygons[i].size(); + + if (nof_lines != polygon_coords.size()) { + return mgr.ERROR_MESSAGE( + "polygon size test failed: nof_lines=" + ::toString(nof_lines) + + ", expected " + ::toString(polygon_coords.size()) + "."); + } + std::vector> poly_coords = polygons[i].toCoords(); + if (nof_lines != poly_coords.size()) { + return mgr.ERROR_MESSAGE( + "polygon toCoords size test failed: poly_coords.size()=" + + ::toString(poly_coords.size()) + ", expected " + ::toString(nof_lines) + "."); + } + + auto poly = polygons[i]; + + for (size_t j = 0; j < poly.size(); j++) { + Geo::LineString line = poly[j]; + std::vector line_coords = line.toCoords(); + auto nof_points = polygon_coords[j].size() / 2; + if (poly.size(j) != nof_points) { + return mgr.ERROR_MESSAGE("polygon linestring size test failed: poly.size(" + + ::toString(j) + ")=" + ::toString(poly.size(j)) + + ", expected " + ::toString(nof_points) + "."); + } + if (line.size() != nof_points) { + return mgr.ERROR_MESSAGE("polygon linestring size test failed: line.size()=" + + ::toString(line.size()) + ", expected " + + ::toString(nof_points) + "."); + } + if (poly_coords[j].size() != nof_points * 2) { + return mgr.ERROR_MESSAGE( + "polygon linestring coords size test failed: poly_coords[j].size()=" + + ::toString(poly_coords[j].size()) + ", expected " + + ::toString(nof_points * 2) + "."); + } + if (line_coords.size() != nof_points * 2) { + return mgr.ERROR_MESSAGE( + "polygon linestring coords size test failed: line_coords.size()=" + + ::toString(line_coords.size()) + ", expected " + + ::toString(nof_points * 2) + "."); + } + for (size_t k = 0; k < nof_points; k++) { + if (std::abs(polygon_coords[j][2 * k] - line_coords[2 * k]) > 1e-7) { + return mgr.ERROR_MESSAGE( + "polygon linestring X coord test failed: line_coords[2*k]=" + + ::toString(line_coords[2 * k]) + ", expected " + + ::toString(polygon_coords[j][2 * k]) + "."); + } + if (std::abs(polygon_coords[j][2 * k] - poly_coords[j][2 * k]) > 1e-7) { + return mgr.ERROR_MESSAGE("polygon X coord test failed: poly_coords[j][2*k]=" + + ::toString(poly_coords[j][2 * k]) + ", expected " + + ::toString(polygon_coords[j][2 * k]) + "."); + } + if (std::abs(polygon_coords[j][2 * k] - line[k].x) > 1e-7) { + return mgr.ERROR_MESSAGE( + "polygon linestring X coord test failed: line[k].x=" + + ::toString(line[k].x) + ", expected " + + ::toString(polygon_coords[j][2 * k]) + "."); + } + if (std::abs(polygon_coords[j][2 * k + 1] - line_coords[2 * k + 1]) > 1e-7) { + return mgr.ERROR_MESSAGE( + "polygon linestring Y coord test failed: line_coords[2*k+1]=" + + ::toString(line_coords[2 * k + 1]) + ", expected " + + ::toString(polygon_coords[j][2 * k + 1]) + "."); + } + if (std::abs(polygon_coords[j][2 * k + 1] - poly_coords[j][2 * k + 1]) > 1e-7) { + return mgr.ERROR_MESSAGE( + "polygon Y coord test failed: poly_coords[j][2*k+1]=" + + ::toString(poly_coords[j][2 * k + 1]) + ", expected " + + ::toString(polygon_coords[j][2 * k + 1]) + "."); + } + if (std::abs(polygon_coords[j][2 * k + 1] - line[k].y) > 1e-7) { + return mgr.ERROR_MESSAGE( + "polygon linestring X coord test failed: line[k].y=" + + ::toString(line[k].y) + ", expected " + + ::toString(polygon_coords[j][2 * k + 1]) + "."); + } + } + } + } + } + + if (count_nulls != 0) { + return mgr.ERROR_MESSAGE("counting null test failed: count_nulls=" + + ::toString(count_nulls) + ", expected 0."); + } + return size; } @@ -175,4 +290,175 @@ EXTENSION_NOINLINE int32_t ct_make_linestring2__cpu_(TableFunctionManager& mgr, return size; } -#endif // #ifndef __CUDACC__ \ No newline at end of file +EXTENSION_NOINLINE int32_t +ct_make_multipolygon__cpu_(TableFunctionManager& mgr, + const Column& polygons, + Column& mpolygons) { + auto size = polygons.size(); + mgr.set_output_item_values_total_number(0, polygons.getNofValues()); + mgr.set_output_row_size(size); + + // Initialize mpolygons + int count_nulls = 0; + for (int64_t i = 0; i < size; i++) { + if (polygons.isNull(i)) { + mpolygons.setNull(i); + count_nulls++; + } else { + std::vector>> mpolygon_coords; + mpolygon_coords.reserve(1); + std::vector> polygon_coords = polygons[i].toCoords(); + mpolygon_coords.push_back(polygon_coords); + auto status = mpolygons[i].fromCoords(mpolygon_coords); + if (status != FlatBufferManager::Status::Success) { + return mgr.ERROR_MESSAGE("fromCoords failed: " + ::toString(status)); + } + } + } + + // Check mpolygons content + if (count_nulls == 0) { + return mgr.ERROR_MESSAGE("counting null test failed: count_nulls=" + + ::toString(count_nulls) + ", expected non-zero."); + } + + for (int64_t i = 0; i < size; i++) { + if (mpolygons.isNull(i)) { + count_nulls--; + } else { + std::vector> polygon_coords = polygons[i].toCoords(); + + // mpolygons[i] is Geo::MultiPolygon instances + // mpolygons[i][j] is Geo::Polygon instances + // mpolygons[i][j][k] is Geo::LineString instances + // mpolygons[i][j][k][l] is Geo::Point2D instances + + auto nof_polygons = mpolygons[i].size(); + if (nof_polygons != 1) { + return mgr.ERROR_MESSAGE("multipolygon size test failed: nof_polygons=" + + ::toString(nof_polygons) + ", expected 1."); + } + + std::vector>> mpolygon_coords = + mpolygons[i].toCoords(); + if (nof_polygons != mpolygon_coords.size()) { + return mgr.ERROR_MESSAGE( + "multipolygon toCoords size test failed: mpolygon_coords.size()=" + + ::toString(mpolygon_coords.size()) + ", expected " + + ::toString(nof_polygons) + "."); + } + + Geo::Polygon poly = mpolygons[i][0]; + std::vector> poly_coords = mpolygon_coords[0]; + if (poly.size() != polygon_coords.size()) { + return mgr.ERROR_MESSAGE("multipolygon polygon size test failed: poly.size()=" + + ::toString(poly.size()) + ", expected " + + ::toString(polygon_coords.size()) + "."); + } + + if (poly_coords.size() != polygon_coords.size()) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon coords size test failed: poly_coords.size()=" + + ::toString(poly_coords.size()) + ", expected " + + ::toString(polygon_coords.size()) + "."); + } + + for (size_t j = 0; j < poly.size(); j++) { + Geo::LineString line = poly[j]; + std::vector line_coords = line.toCoords(); + auto nof_points = polygon_coords[j].size() / 2; + if (poly.size(j) != nof_points) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring size test failed: poly.size(" + + ::toString(j) + ")=" + ::toString(poly.size(j)) + ", expected " + + ::toString(nof_points) + "."); + } + if (line.size() != nof_points) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring size test failed: line.size()=" + + ::toString(line.size()) + ", expected " + ::toString(nof_points) + "."); + } + if (poly_coords[j].size() != nof_points * 2) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring coords size test failed: " + "poly_coords[j].size()=" + + ::toString(poly_coords[j].size()) + ", expected " + + ::toString(nof_points * 2) + "."); + } + if (line_coords.size() != nof_points * 2) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring coords size test failed: " + "line_coords.size()=" + + ::toString(line_coords.size()) + ", expected " + + ::toString(nof_points * 2) + "."); + } + + for (size_t k = 0; k < nof_points; k++) { + if (std::abs(polygon_coords[j][2 * k] - line_coords[2 * k]) > 1e-7) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring X coord test failed: line_coords[2*k]=" + + ::toString(line_coords[2 * k]) + ", expected " + + ::toString(polygon_coords[j][2 * k]) + "."); + } + if (std::abs(polygon_coords[j][2 * k] - poly_coords[j][2 * k]) > 1e-7) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon X coord test failed: poly_coords[j][2*k]=" + + ::toString(poly_coords[j][2 * k]) + ", expected " + + ::toString(polygon_coords[j][2 * k]) + "."); + } + if (std::abs(polygon_coords[j][2 * k] - line[k].x) > 1e-7) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring X coord test failed: line[k].x=" + + ::toString(line[k].x) + ", expected " + + ::toString(polygon_coords[j][2 * k]) + "."); + } + if (std::abs(polygon_coords[j][2 * k + 1] - line_coords[2 * k + 1]) > 1e-7) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring Y coord test failed: " + "line_coords[2*k+1]=" + + ::toString(line_coords[2 * k + 1]) + ", expected " + + ::toString(polygon_coords[j][2 * k + 1]) + "."); + } + if (std::abs(polygon_coords[j][2 * k + 1] - poly_coords[j][2 * k + 1]) > 1e-7) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon Y coord test failed: poly_coords[j][2*k+1]=" + + ::toString(poly_coords[j][2 * k + 1]) + ", expected " + + ::toString(polygon_coords[j][2 * k + 1]) + "."); + } + if (std::abs(polygon_coords[j][2 * k + 1] - line[k].y) > 1e-7) { + return mgr.ERROR_MESSAGE( + "multipolygon polygon linestring X coord test failed: line[k].y=" + + ::toString(line[k].y) + ", expected " + + ::toString(polygon_coords[j][2 * k + 1]) + "."); + } + } + } + } + } + + if (count_nulls != 0) { + return mgr.ERROR_MESSAGE("counting null test failed: count_nulls=" + + ::toString(count_nulls) + ", expected 0."); + } + + return size; +} + +EXTENSION_NOINLINE int32_t ct_polygonn__cpu_(TableFunctionManager& mgr, + const Column& mpolygons, + int64_t n, + Column& polygons) { + auto size = mpolygons.size(); + mgr.set_output_item_values_total_number(0, mpolygons.getNofValues()); + mgr.set_output_row_size(size); + for (int64_t i = 0; i < size; i++) { + if (mpolygons.isNull(i)) { + polygons.setNull(i); + } else { + polygons.setItem(i, mpolygons[i][n - 1]); + } + } + return size; +} + +#endif // #ifndef __CUDACC__ diff --git a/QueryEngine/TableFunctions/TestFunctions/TableFunctionsTesting.h b/QueryEngine/TableFunctions/TestFunctions/TableFunctionsTesting.h index c92e2f0c52..f01b0e21e8 100644 --- a/QueryEngine/TableFunctions/TestFunctions/TableFunctionsTesting.h +++ b/QueryEngine/TableFunctions/TestFunctions/TableFunctionsTesting.h @@ -1317,6 +1317,8 @@ EXTENSION_NOINLINE int32_t ct_require_range__cpu_(const Column& input1, UDTF: ct_linestringn__cpu_(TableFunctionManager, Column polygons, int64_t n) -> Column linestrings UDTF: ct_make_polygon3__cpu_(TableFunctionManager, Cursor rings, Column holes1, Column holes2>) -> Column polygons, Column sizes UDTF: ct_make_linestring2__cpu_(TableFunctionManager, Cursor x, Column y>, double dx, double dy) -> Column linestrings + UDTF: ct_make_multipolygon__cpu_(TableFunctionManager, Column polygons) -> Column mpolygons + UDTF: ct_polygonn__cpu_(TableFunctionManager, Column mpolygons, int64_t n) -> Column polygons */ // clang-format on @@ -1360,6 +1362,15 @@ EXTENSION_NOINLINE int32_t ct_make_linestring2__cpu_(TableFunctionManager& mgr, double dy, Column& linestrings); +EXTENSION_NOINLINE int32_t ct_make_multipolygon__cpu_(TableFunctionManager& mgr, + const Column& polygons, + Column& mpolygons); + +EXTENSION_NOINLINE int32_t ct_polygonn__cpu_(TableFunctionManager& mgr, + const Column& mpolygons, + int64_t n, + Column& polygons); + #endif // ifndef __CUDACC__ // clang-format off diff --git a/QueryEngine/TargetExprBuilder.cpp b/QueryEngine/TargetExprBuilder.cpp index 60dbca2091..e4b956f890 100644 --- a/QueryEngine/TargetExprBuilder.cpp +++ b/QueryEngine/TargetExprBuilder.cpp @@ -134,7 +134,8 @@ void TargetExprCodegen::codegen( const bool varlen_projection = is_varlen_projection(target_expr, target_info.sql_type); /* TODO: find a better way to determine if target uses FlatBuffer storage or not. */ - const bool uses_flatbuffer = target_info.sql_type.get_type() == kPOLYGON; + const bool uses_flatbuffer = (target_info.sql_type.get_type() == kPOLYGON || + target_info.sql_type.get_type() == kMULTIPOLYGON); const auto agg_fn_names = agg_fn_base_names(target_info, varlen_projection); const auto window_func = dynamic_cast(target_expr); WindowProjectNodeContext::resetWindowFunctionContext(executor); diff --git a/QueryEngine/Utils/FlatBuffer.h b/QueryEngine/Utils/FlatBuffer.h index c1130b5ae1..281cfce90e 100644 --- a/QueryEngine/Utils/FlatBuffer.h +++ b/QueryEngine/Utils/FlatBuffer.h @@ -50,7 +50,6 @@ interpretation depends on the format parameters specified above. The size of the raw data buffer depends on the format id and the user-specified parameters in the data format metadata. - All buffers above are aligned to the 64-bit boundaries. In summary, the memory layout of a flatbuffer is: @@ -170,6 +169,183 @@ into rings and compressed_indices describes the partitioning of rings into polygons. + NestedArray format specification + -------------------------------- + + NestedArray represents a storage for zero, one, two, and three + dimensional ragged arrays. The storage format consists of sizes and + values buffers (plus offset buffers to optimize accessing + items). The sizes buffer stores the sizes of ragged arrays at + various levels and the values buffer stores the values of ragged + arrays. + + The NestedArray storage is used as a uniform storage schema for + different types (variable-length arrays, geotypes, etc) with + variable dimensionality. For example, a GeoMultiPolygon + + GeoMultiPolygon([ + GeoPolygon([LineString([(x000, y000), (x001, y001), ...])], + LineString([(x010, y010), (x011, y011), ...])], + ...]), + GeoPolygon([LineString([(x100, y100), (x101, y101), ...])], + LineString([(x110, y110), (x111, y111), ...])], + ...]), + ... + ]) + + is represented as a three dimensional ragged array where the sizes + buffer contains the number of polygons in the multi-polygon, all the + numbers of linestrings in polygons, all the numbers of points in + linestrings, and finally, the values buffer contains all the + coordinates. Note that a "value" is defined as a point with two + coordinates. + + The current implementation of NestedArray supports dimensionalities + up to 3 but the format can be extended to arbitrary dimensions. + + NestedArray API + --------------- + + To compute flatbuffer size required to represent a nested array with + the given dimensionsinality, total items count, estimated total + sizes and values counts, value type, and user data buffer size, + use:: + + int64_t compute_flatbuffer_size(dimensions, + total_items_count, + total_sizes_count, + total_values_count, + value_type, + user_data_size) + + To initialize the provided buffer for nested array format, use:: + + Status .initialize(dimensions, + total_items_count, + total_sizes_count, + total_values_count, + value_type, + null_value_ptr, + user_data_ptr, user_data_size) + + To test if the provided buffer contains an initialized FlatBuffer:: + + bool isFlatBuffer(buffer) + + To get the size of an initialized FlatBuffer:: + + int64_t getBufferSize(buffer) + int64_t .getBufferSize() + + To get the size of the values buffer:: + + size_t .getValuesBufferSize() + + To get the size of a value:: + + size_t .getValueSize() + + To get the number of specified values:: + + size_t .getValuesCount() + + To get the dimensionality of a nested array:: + + size_t .getDimensions() + + To get various buffers:: + + int8_t* .get_user_data_buffer() + int8_t* .get_values_buffer() + sizes_t* .get_sizes_buffer() + offsets_t* .get_values_offsets() + offsets_t* .get_sizes_offsets() + int8_t* .getNullValuePtr() + + To test if the provided buffer contains null value:: + + bool .containsNullValue() + + To get the item and subitems of a nested array:: + + template + Status .getItem(index, + vector& values, + vector& sizes, + vector& sizes_of_sizes, + bool& is_null) # ndims == 3 + + Status .getItem(index, + subindex, + vector& values, + vector& sizes, + bool& is_null) # ndims == 3 + + Status .getItem(index, + subindex, + subsubindex, + vector& values, + bool& is_null) # ndims == 3 + + Status .getItem(index, + int32_t& nof_values, + int8_t*& values, + int32_t& nof_sizes, + int8_t*& sizes, + int32_t& nof_sizes_of_sizes, + int8_t*& sizes_of_sizes, + bool& is_null) # ndims == 3 + + Status .getItem(index, + subindex, + int32_t& nof_values, + int8_t*& values, + int32_t& nof_sizes, + int8_t*& sizes, + bool& is_null) # ndims == 3 + + Status .getItem(index, + subindex, + subsubindex, + int32_t& nof_values, + int8_t*& values, + int32_t& nof_sizes, + int8_t*& sizes, + bool& is_null) # ndims == 3 + + To get the item or subitem lengths:: + + Status .getLength(index, size_t& length) # ndims == 3 + Status .getLength(index, subindex, size_t& length) # ndims == 3 + Status .getLength(index, subindex, subsubindex, size_t& length) # ndims == 3 + + To set an item of a nested array array:: + + Status .setItem(index, vector& arr) # ndims == 1 + + Status .setItem(index, vector>& arr) # ndims == 2 + + template + Status .setItem(index, vector>>& arr) # ndims == 3 + + template + Status .setItem(index, + vector + vector& sizes, + vector& sizes_of_sizes) # ndims == 3 + + Status setItem(const int64_t index, + int8_t* values_buf, + size_t values_buf_size, + int32_t* sizes_buf, + int32_t nof_sizes, + int32_t* sizes_of_sizes_buf, + int32_t nof_sizes_of_sizes) # ndims == 3 + + To test if item is NULL:: + + Status isNull(index, bool& is_null) + FlatBuffer usage ---------------- @@ -188,14 +364,30 @@ */ // clang-format on +#ifdef FLATBUFFER_ERROR_ABORTS +#include "../../Shared/toString.h" +#define RETURN_ERROR(exc) \ + { \ + PRINT(exc); \ + abort(); \ + return (exc); \ + } +#else +#define RETURN_ERROR(exc) return (exc) +#endif + #include #ifdef HAVE_TOSTRING #include #include #endif +#include #include "../../Shared/funcannotations.h" +#define FLATBUFFER_UNREACHABLE() \ + { abort(); } + // Notice that the format value is used to recognize if a memory // buffer uses some flat buffer format or not. To minimize chances for // false positive test results, use a non-trival integer value when @@ -207,7 +399,7 @@ enum FlatBufferFormat { GeoPolygonFormatId = 0x67706f6c79676f6e, // hex repr of 'gpolygon' // GeoMultiPointFormatId = 0x47656f706f696e74, // hex repr of 'Geopoint' // GeoMultiLineStringFormatId = 0x476c696e65737472, // hex repr of 'Glinestr' - // GeoMultiPolygonFormatId = 0x47706f6c79676f6e, // hex repr of 'Gpolygon' + NestedArrayFormatId = 0x6e65737465644152 // hex repr of 'nestedAR' }; inline int64_t _align_to_int64(int64_t addr) { @@ -216,18 +408,80 @@ inline int64_t _align_to_int64(int64_t addr) { } struct FlatBufferManager { + enum ValueType { + Bool8, + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float32, + Float64, + PointInt32, + PointFloat64 + }; + +#ifdef HAVE_TOSTRING + static std::string toString(const ValueType& type); +#endif + + static size_t get_size(ValueType type) { + switch (type) { + case Bool8: + case Int8: + case UInt8: + return 1; + case Int16: + case UInt16: + return 2; + case Int32: + case UInt32: + case Float32: + return 4; + case Int64: + case UInt64: + case Float64: + case PointInt32: + return 8; + case PointFloat64: + return 16; + } + FLATBUFFER_UNREACHABLE(); + return 0; + } + + /* + sizes_t is the type of a container size. Here we use int32_t + because Geospatial uses it as the type for the vector of ring and + polygon sizes. + + offsets_t is the type of offsets that is used to locate + sub-buffers within the FlatBuffer main buffer. Because NULL items + are encoded as negative offset values, the offsets type must be a + signed type. Hence, we define offsets_t as int64_t. + */ + + typedef int32_t sizes_t; + typedef int64_t offsets_t; + +#define FLATBUFFER_SIZES_T_VALUE_TYPE Int32 +#define FLATBUFFER_OFFSETS_T_VALUE_TYPE UInt64 + struct BaseWorker { - int64_t format_id; - int64_t flatbuffer_size; - int64_t format_metadata_offset; // the offset of the data format metadata buffer - int64_t format_worker_offset; // the offset of the data format worker buffer + FlatBufferFormat format_id; + offsets_t flatbuffer_size; + offsets_t format_metadata_offset; // the offset of the data format metadata buffer + offsets_t format_worker_offset; // the offset of the data format worker buffer #ifdef HAVE_TOSTRING std::string toString() const { std::string result = ::typeName(this) + "{"; result += "format_id=" + std::to_string(format_id); - result += ", flatbuffer_size=" + std::to_string(flatbuffer_size); - result += ", format_metadata_offset=" + std::to_string(format_metadata_offset); - result += ", format_worker_offset=" + std::to_string(format_worker_offset); + result += ",\n flatbuffer_size=" + std::to_string(flatbuffer_size); + result += ",\n format_metadata_offset=" + std::to_string(format_metadata_offset); + result += ",\n format_worker_offset=" + std::to_string(format_worker_offset); result += "}"; return result; } @@ -401,22 +655,80 @@ struct FlatBufferManager { #endif }; + struct NestedArrayWorker { + int64_t specified_items_count; + // all offsets are in bytes + offsets_t storage_indices_offset; + offsets_t sizes_offsets_offset; + offsets_t values_offsets_offset; + offsets_t sizes_buffer_offset; + offsets_t values_buffer_offset; + offsets_t user_data_buffer_offset; + size_t value_size; +#ifdef HAVE_TOSTRING + std::string toString() const { + std::string result = ::typeName(this) + "{"; + result += "specified_items_count=" + std::to_string(specified_items_count); + result += ",\n storage_indices_offset=" + std::to_string(storage_indices_offset); + result += ",\n sizes_offsets_offset=" + std::to_string(sizes_offsets_offset); + result += ",\n values_offsets_offset=" + std::to_string(values_offsets_offset); + result += ",\n sizes_buffer_offset=" + std::to_string(sizes_buffer_offset); + result += ",\n values_buffer_offset=" + std::to_string(values_buffer_offset); + result += + ",\n user_data_buffer_offset=" + std::to_string(user_data_buffer_offset); + result += ",\n value_size=" + std::to_string(value_size); + result += "}"; + return result; + } +#endif + }; + + struct NestedArray { + size_t dimensions; + int64_t total_items_count; + int64_t total_sizes_count; + int64_t total_values_count; + ValueType value_type; + size_t user_data_size; +#ifdef HAVE_TOSTRING + std::string toString() const { + std::string result = ::typeName(this) + "{"; + result += "dimensions=" + std::to_string(dimensions); + result += ",\n total_items_count=" + std::to_string(total_items_count); + result += ",\n total_sizes_count=" + std::to_string(total_sizes_count); + result += ",\n total_values_count=" + std::to_string(total_values_count); + result += ",\n value_type=" + FlatBufferManager::toString(value_type); + result += ",\n user_data_size=" + std::to_string(user_data_size); + result += "}"; + return result; + } +#endif + }; + enum Status { Success = 0, IndexError, SubIndexError, SizeError, + FlatbufferSizeError, ItemAlreadySpecifiedError, ItemUnspecifiedError, UnexpectedNullItemError, ValuesBufferTooSmallError, + SizesBufferTooSmallError, CompressedIndices2BufferTooSmallError, MemoryError, NotImplementedError, NotSupportedFormatError, + InvalidUserDataError, + DimensionalityError, + TypeError, + UserDataError, + InconsistentSizesError, UnknownFormatError }; + // FlatBuffer main buffer. It is the only member of the FlatBuffer struct. int8_t* buffer; // Check if a buffer contains FlatBuffer formatted data @@ -424,8 +736,9 @@ struct FlatBufferManager { if (buffer) { // warning: assume that buffer size is at least 8 bytes const auto* base = reinterpret_cast(buffer); - FlatBufferFormat header_format = static_cast(base->format_id); + FlatBufferFormat header_format = base->format_id; switch (header_format) { + case NestedArrayFormatId: case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: @@ -436,14 +749,17 @@ struct FlatBufferManager { ((int64_t*)buffer)[flatbuffer_size / sizeof(int64_t) - 1]); return footer_format == header_format; } - } break; - default:; + break; + } + default: + break; } } return false; } // Return the allocation size of the the FlatBuffer storage, in bytes + // TODO: return size_t value, 0 when not a flat buffer static int64_t getBufferSize(const void* buffer) { if (isFlatBuffer(buffer)) { return reinterpret_cast(buffer)->flatbuffer_size; @@ -453,17 +769,40 @@ struct FlatBufferManager { } // Return the allocation size of the the FlatBuffer storage, in bytes + // TODO: int64_t -> size_t inline int64_t getBufferSize() const { return reinterpret_cast(buffer)->flatbuffer_size; } + inline bool isNestedArray() const { return format() == NestedArrayFormatId; } + + inline size_t getValueSize() const { return getNestedArrayWorker()->value_size; } + + inline size_t getValuesBufferSize() const { + const auto* metadata = getNestedArrayMetadata(); + const auto* worker = getNestedArrayWorker(); + return worker->value_size * metadata->total_values_count; + } + + inline size_t getValuesCount() const { + const auto* worker = getNestedArrayWorker(); + const auto* values_offsets = get_values_offsets(); + const auto storage_index = worker->specified_items_count; + const auto values_offset = values_offsets[storage_index]; + if (values_offset < 0) { + return -(values_offset + 1); + } + return values_offset; + } + // Return the format of FlatBuffer HOST DEVICE inline FlatBufferFormat format() const { const auto* base = reinterpret_cast(buffer); - return static_cast(base->format_id); + return base->format_id; } // Return the number of items + // To be deprecated in favor of NestedArray format HOST DEVICE inline int64_t itemsCount() const { switch (format()) { case VarlenArrayFormatId: @@ -474,10 +813,15 @@ struct FlatBufferManager { return getGeoLineStringMetadata()->total_items_count; case GeoPolygonFormatId: return getGeoPolygonMetadata()->total_items_count; + case NestedArrayFormatId: + return getNestedArrayMetadata()->total_items_count; + default: + break; } return -1; // invalid value } + // To be deprecated in favor of NestedArray format HOST DEVICE inline int64_t items2Count() const { switch (format()) { case VarlenArrayFormatId: @@ -486,11 +830,33 @@ struct FlatBufferManager { break; case GeoPolygonFormatId: return getGeoPolygonMetadata()->max_nof_rings; + default: + break; } return -1; // invalid value } - HOST DEVICE inline int64_t dtypeSize() const { + // To be deprecated in favor of NestedArray format + HOST DEVICE inline int64_t valueByteSize() const { + switch (format()) { + case VarlenArrayFormatId: + return getVarlenArrayMetadata()->dtype_size; + case GeoPointFormatId: + return 2 * (getGeoPointMetadata()->is_geoint ? sizeof(int32_t) : sizeof(double)); + case GeoLineStringFormatId: + return 2 * + (getGeoLineStringMetadata()->is_geoint ? sizeof(int32_t) : sizeof(double)); + case GeoPolygonFormatId: + return 2 * + (getGeoPolygonMetadata()->is_geoint ? sizeof(int32_t) : sizeof(double)); + default: + break; + } + return -1; + } + + // To be deprecated in favor of NestedArray format + HOST DEVICE inline int64_t dtypeSize() const { // TODO: use valueByteSize instead switch (format()) { case VarlenArrayFormatId: return getVarlenArrayMetadata()->dtype_size; @@ -502,12 +868,15 @@ struct FlatBufferManager { case GeoPolygonFormatId: return 2 * (getGeoPolygonMetadata()->is_geoint ? sizeof(int32_t) : sizeof(double)); + default: + break; } return -1; } // VarlenArray support: + // To be deprecated in favor of NestedArray format static int64_t compute_flatbuffer_size(FlatBufferFormat format_id, const int8_t* format_metadata_ptr) { int64_t flatbuffer_size = _align_to_int64(sizeof(FlatBufferManager::BaseWorker)); @@ -578,7 +947,8 @@ struct FlatBufferManager { (format_metadata->total_items_count)); // storage_indices buffer size break; } - default:; + default: + FLATBUFFER_UNREACHABLE(); } flatbuffer_size += _align_to_int64(sizeof(int64_t)); // footer format id return flatbuffer_size; @@ -610,19 +980,175 @@ struct FlatBufferManager { return reinterpret_cast(buffer + base->format_metadata_offset); \ } + // To be deprecated in favor of NestedArray format FLATBUFFER_MANAGER_FORMAT_TOOLS(VarlenArray); FLATBUFFER_MANAGER_FORMAT_TOOLS(GeoPoint); FLATBUFFER_MANAGER_FORMAT_TOOLS(GeoLineString); FLATBUFFER_MANAGER_FORMAT_TOOLS(GeoPolygon); + /* + HOST DEVICE inline NestedArrayWorker* getNestedArrayWorker() { + auto* base = getBaseWorker(); + return reinterpret_cast(buffer + base->format_worker_offset); + } + HOST DEVICE inline const NestedArrayWorker* getNestedArrayWorker() const { + const auto* base = getBaseWorker(); + return reinterpret_cast(buffer + + base->format_worker_offset); + } + */ +#define FLATBUFFER_MANAGER_FORMAT_TOOLS_NEW(TYPENAME) \ + HOST DEVICE inline NestedArrayWorker* get##TYPENAME##Worker() { \ + auto* base = getBaseWorker(); \ + return reinterpret_cast(buffer + base->format_worker_offset); \ + } \ + HOST DEVICE inline TYPENAME* get##TYPENAME##Metadata() { \ + auto* base = getBaseWorker(); \ + return reinterpret_cast(buffer + base->format_metadata_offset); \ + } \ + HOST DEVICE inline const NestedArrayWorker* get##TYPENAME##Worker() const { \ + const auto* base = getBaseWorker(); \ + return reinterpret_cast(buffer + \ + base->format_worker_offset); \ + } \ + HOST DEVICE inline const TYPENAME* get##TYPENAME##Metadata() const { \ + const auto* base = getBaseWorker(); \ + return reinterpret_cast(buffer + base->format_metadata_offset); \ + } + + FLATBUFFER_MANAGER_FORMAT_TOOLS(NestedArray); #undef FLATBUFFER_MANAGER_FORMAT_TOOLS +#undef FLATBUFFER_MANAGER_FORMAT_TOOLS_NEW + +#define FLATBUFFER_MANAGER_SET_OFFSET(OBJ, NAME, SIZE) \ + offset = OBJ->NAME##_offset = offset + _align_to_int64(previous_size); \ + previous_size = SIZE; + + static int64_t compute_flatbuffer_size(int64_t dimensions, + int64_t total_items_count, + int64_t total_sizes_count, + int64_t total_values_count, + ValueType value_type, + size_t user_data_size) { + size_t value_size = get_size(value_type); + offsets_t flatbuffer_size = _align_to_int64(sizeof(FlatBufferManager::BaseWorker)); + flatbuffer_size += _align_to_int64(sizeof(NestedArray)); + flatbuffer_size += _align_to_int64(sizeof(NestedArrayWorker)); + flatbuffer_size += + _align_to_int64(value_size * (total_values_count + 1)); // values buffer + flatbuffer_size += + _align_to_int64(sizeof(sizes_t) * total_sizes_count); // sizes buffer + flatbuffer_size += + _align_to_int64(sizeof(offsets_t) * (total_items_count + 1)); // values offsets + flatbuffer_size += _align_to_int64( + sizeof(offsets_t) * (total_items_count * dimensions + 1)); // sizes offsets + flatbuffer_size += _align_to_int64( + sizeof(sizes_t) * total_items_count); // storage indices, must use signed type + flatbuffer_size += _align_to_int64(user_data_size); // user data + flatbuffer_size += _align_to_int64(sizeof(int64_t)); // format id + return flatbuffer_size; + } + + Status initialize(FlatBufferFormat format_id, // TODO: eliminate format_id or add it to + // compute_flatbuffer_size + int64_t dimensions, + int64_t total_items_count, + int64_t total_sizes_count, + int64_t total_values_count, + ValueType value_type, + const int8_t* null_value_ptr, + const int8_t* user_data_ptr, + size_t user_data_size) { + auto* base = getBaseWorker(); + base->format_id = format_id; + size_t value_size = get_size(value_type); + base->flatbuffer_size = compute_flatbuffer_size(dimensions, + total_items_count, + total_sizes_count, + total_values_count, + value_type, + user_data_size); + offsets_t offset = 0; + size_t previous_size = sizeof(FlatBufferManager::BaseWorker); + FLATBUFFER_MANAGER_SET_OFFSET(base, format_metadata, sizeof(NestedArray)); + FLATBUFFER_MANAGER_SET_OFFSET(base, format_worker, sizeof(NestedArrayWorker)); + + auto* metadata = getNestedArrayMetadata(); + metadata->dimensions = dimensions; + metadata->total_items_count = total_items_count; + metadata->total_sizes_count = total_sizes_count; + metadata->total_values_count = total_values_count; + metadata->value_type = value_type; + metadata->user_data_size = user_data_size; + + auto* worker = getNestedArrayWorker(); + worker->specified_items_count = 0; + worker->value_size = value_size; + + FLATBUFFER_MANAGER_SET_OFFSET( + worker, values_buffer, value_size * (total_values_count + 1)); + FLATBUFFER_MANAGER_SET_OFFSET( + worker, sizes_buffer, sizeof(sizes_t) * total_sizes_count); + FLATBUFFER_MANAGER_SET_OFFSET( + worker, values_offsets, sizeof(offsets_t) * (total_items_count + 1)); + FLATBUFFER_MANAGER_SET_OFFSET( + worker, sizes_offsets, sizeof(offsets_t) * (total_items_count * dimensions + 1)); + FLATBUFFER_MANAGER_SET_OFFSET( + worker, storage_indices, sizeof(sizes_t) * total_items_count); + FLATBUFFER_MANAGER_SET_OFFSET(worker, user_data_buffer, user_data_size); + + if (base->flatbuffer_size != + offset + _align_to_int64(previous_size) + _align_to_int64(sizeof(int64_t))) { + RETURN_ERROR(FlatbufferSizeError); + } + + offsets_t* values_offsets = get_values_offsets(); + offsets_t* sizes_offsets = get_sizes_offsets(); + values_offsets[0] = 0; + sizes_offsets[0] = 0; + sizes_t* storage_indices = get_storage_indices_new(); + for (int i = 0; i < total_items_count; i++) { + storage_indices[i] = -1; + } + + // the last value in values_buffer stores a null value: + int8_t* null_value_buffer = get_values_buffer() + value_size * total_values_count; + if (null_value_ptr != nullptr) { + if (memcpy(null_value_buffer, null_value_ptr, value_size) == nullptr) { + RETURN_ERROR(MemoryError); + } + } else { + if (memset(null_value_buffer, 0, value_size) == nullptr) { + RETURN_ERROR(MemoryError); + } + } + + if (user_data_size > 0 && user_data_ptr != nullptr) { + int8_t* user_data_buffer = get_user_data_buffer(); + if (memcpy(user_data_buffer, user_data_ptr, user_data_size) == nullptr) { + RETURN_ERROR(MemoryError); + } + } + + ((int64_t*)buffer)[base->flatbuffer_size / sizeof(int64_t) - 1] = + static_cast(format_id); + + if (isFlatBuffer(buffer)) { + return Success; + } + RETURN_ERROR(UnknownFormatError); + } + // To be deprecated in favor of NestedArray format void initialize(FlatBufferFormat format_id, const int8_t* format_metadata_ptr) { auto* base = getBaseWorker(); base->format_id = format_id; base->flatbuffer_size = compute_flatbuffer_size(format_id, format_metadata_ptr); base->format_metadata_offset = _align_to_int64(sizeof(FlatBufferManager::BaseWorker)); switch (format_id) { + case NestedArrayFormatId: + FLATBUFFER_UNREACHABLE(); + break; case VarlenArrayFormatId: { base->format_worker_offset = base->format_metadata_offset + @@ -779,7 +1305,16 @@ struct FlatBufferManager { // Low-level API + inline size_t getDimensions() const { + if (isNestedArray()) { + return getNestedArrayMetadata()->dimensions; + } + FLATBUFFER_UNREACHABLE(); + return 0; + } + // Return the upper bound to the total number of points in all items + // To be deprecated in favor of NestedArray format inline int64_t get_max_nof_values() const { switch (format()) { case VarlenArrayFormatId: @@ -790,13 +1325,20 @@ struct FlatBufferManager { return getGeoLineStringMetadata()->max_nof_values; case GeoPolygonFormatId: return getGeoPolygonMetadata()->max_nof_values; + default: + break; } return -1; } // Return the total number of values in all specified items + // To be deprecated in favor of NestedArray format inline int64_t get_nof_values() const { switch (format()) { + case NestedArrayFormatId: { + FLATBUFFER_UNREACHABLE(); + break; + } case GeoPolygonFormatId: { const int64_t storage_count2 = get_storage_count2(); const int64_t* compressed_indices2 = get_compressed_indices2(); @@ -811,6 +1353,7 @@ struct FlatBufferManager { } // Return the number of specified items + // To be deprecated in favor of NestedArray format HOST DEVICE inline int64_t& get_storage_count() { switch (format()) { case VarlenArrayFormatId: @@ -821,11 +1364,14 @@ struct FlatBufferManager { return getGeoLineStringWorker()->items_count; case GeoPolygonFormatId: return getGeoPolygonWorker()->items_count; + default: + break; } static int64_t dummy_storage_count = -1; return dummy_storage_count; } + // To be deprecated in favor of NestedArray format inline const int64_t& get_storage_count() const { switch (format()) { case VarlenArrayFormatId: @@ -836,12 +1382,15 @@ struct FlatBufferManager { return getGeoLineStringWorker()->items_count; case GeoPolygonFormatId: return getGeoPolygonWorker()->items_count; + default: + break; } static int64_t dummy = -1; return dummy; } // Return the number of specified blocks + // To be deprecated in favor of NestedArray format HOST DEVICE inline int64_t& get_storage_count2() { switch (format()) { case GeoPolygonFormatId: @@ -849,12 +1398,14 @@ struct FlatBufferManager { case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: + default: break; } static int64_t dummy_storage_count = -1; return dummy_storage_count; } + // To be deprecated in favor of NestedArray format inline const int64_t& get_storage_count2() const { switch (format()) { case GeoPolygonFormatId: @@ -862,6 +1413,7 @@ struct FlatBufferManager { case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: + default: break; } static int64_t dummy_storage_count = -1; @@ -869,6 +1421,7 @@ struct FlatBufferManager { } // Return the size of values buffer in bytes + // To be deprecated in favor of NestedArray format inline int64_t get_values_buffer_size() const { switch (format()) { case VarlenArrayFormatId: { @@ -889,12 +1442,15 @@ struct FlatBufferManager { const auto* worker = getGeoPolygonWorker(); return worker->compressed_indices2_offset - worker->values_offset; } + default: + break; } static int64_t dummy = -1; return dummy; } // Return the size of compressed_indices2 buffer in bytes + // To be deprecated in favor of NestedArray format inline int64_t get_compressed_indices2_buffer_size() const { switch (format()) { case GeoPolygonFormatId: { @@ -904,6 +1460,7 @@ struct FlatBufferManager { case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: + default: break; } static int64_t dummy = -1; @@ -911,6 +1468,7 @@ struct FlatBufferManager { } // Return the pointer to values buffer + // To be deprecated in favor of NestedArray format HOST DEVICE inline int8_t* get_values() { int64_t offset = 0; switch (format()) { @@ -932,6 +1490,7 @@ struct FlatBufferManager { return buffer + offset; } + // To be deprecated in favor of NestedArray format inline const int8_t* get_values() const { int64_t offset = 0; switch (format()) { @@ -954,6 +1513,7 @@ struct FlatBufferManager { } // Return the pointer to counts2 buffer + // To be deprecated in favor of NestedArray format HOST DEVICE inline int32_t* get_counts2() { int64_t offset = 0; switch (format()) { @@ -966,6 +1526,7 @@ struct FlatBufferManager { return reinterpret_cast(buffer + offset); } + // To be deprecated in favor of NestedArray format inline const int32_t* get_counts2() const { int64_t offset = 0; switch (format()) { @@ -979,6 +1540,7 @@ struct FlatBufferManager { } // Return the pointer to compressed indices2 buffer + // To be deprecated in favor of NestedArray format HOST DEVICE inline int64_t* get_compressed_indices2() { int64_t offset = 0; switch (format()) { @@ -991,6 +1553,7 @@ struct FlatBufferManager { return reinterpret_cast(buffer + offset); } + // To be deprecated in favor of NestedArray format inline const int64_t* get_compressed_indices2() const { int64_t offset = 0; switch (format()) { @@ -1004,6 +1567,7 @@ struct FlatBufferManager { } // Return the pointer to compressed indices buffer + // To be deprecated in favor of NestedArray format HOST DEVICE inline int64_t* get_compressed_indices() { int64_t offset = 0; switch (format()) { @@ -1022,6 +1586,7 @@ struct FlatBufferManager { return reinterpret_cast(buffer + offset); } + // To be deprecated in favor of NestedArray format inline const int64_t* get_compressed_indices() const { int64_t offset = 0; switch (format()) { @@ -1040,7 +1605,73 @@ struct FlatBufferManager { return reinterpret_cast(buffer + offset); } - // Return the pointer to storage indices buffer +#define FLATBUFFER_GET_BUFFER_METHODS(BUFFERNAME, BUFFERTYPE) \ + HOST DEVICE inline BUFFERTYPE* get_##BUFFERNAME() { \ + int64_t offset = 0; \ + switch (format()) { \ + case NestedArrayFormatId: \ + offset = getNestedArrayWorker()->BUFFERNAME##_offset; \ + break; \ + default: \ + return nullptr; \ + } \ + return reinterpret_cast(buffer + offset); \ + } \ + inline const BUFFERTYPE* get_##BUFFERNAME() const { \ + int64_t offset = 0; \ + switch (format()) { \ + case NestedArrayFormatId: \ + offset = getNestedArrayWorker()->BUFFERNAME##_offset; \ + break; \ + default: \ + return nullptr; \ + } \ + return reinterpret_cast(buffer + offset); \ + } + + FLATBUFFER_GET_BUFFER_METHODS(user_data_buffer, int8_t); + FLATBUFFER_GET_BUFFER_METHODS(values_buffer, int8_t); + FLATBUFFER_GET_BUFFER_METHODS(sizes_buffer, sizes_t); + FLATBUFFER_GET_BUFFER_METHODS(values_offsets, offsets_t); + FLATBUFFER_GET_BUFFER_METHODS(sizes_offsets, offsets_t); + +#undef FLATBUFFER_GET_BUFFER_METHODS + + inline const int8_t* getNullValuePtr() const { + if (isNestedArray()) { + return get_values_buffer() + getValuesBufferSize(); + } + return nullptr; + } + + inline bool containsNullValue(const int8_t* value_ptr) const { + const int8_t* null_value_ptr = getNullValuePtr(); + if (null_value_ptr != nullptr) { + switch (getValueSize()) { + case 1: + return *null_value_ptr == *value_ptr; + case 2: + return *reinterpret_cast(null_value_ptr) == + *reinterpret_cast(value_ptr); + case 4: + return *reinterpret_cast(null_value_ptr) == + *reinterpret_cast(value_ptr); + case 8: + return *reinterpret_cast(null_value_ptr) == + *reinterpret_cast(value_ptr); + case 16: + return (*reinterpret_cast(null_value_ptr) == + *reinterpret_cast(value_ptr) && + *(reinterpret_cast(null_value_ptr) + 1) == + *(reinterpret_cast(value_ptr) + 1)); + default: + break; + } + } + return false; + } + + // To be deprecated in favor of NestedArray format HOST DEVICE inline int64_t* get_storage_indices() { int64_t offset = 0; switch (format()) { @@ -1059,6 +1690,7 @@ struct FlatBufferManager { return reinterpret_cast(buffer + offset); } + // To be deprecated in favor of NestedArray format inline const int64_t* get_storage_indices() const { int64_t offset = 0; switch (format()) { @@ -1077,18 +1709,711 @@ struct FlatBufferManager { return reinterpret_cast(buffer + offset); } + // TODO: rename to get_storage_indices + HOST DEVICE inline sizes_t* get_storage_indices_new() { + offsets_t offset = 0; + switch (format()) { + case NestedArrayFormatId: + offset = getNestedArrayWorker()->storage_indices_offset; + break; + default: + return nullptr; + } + return reinterpret_cast(buffer + offset); + } + + inline const sizes_t* get_storage_indices_new() const { + offsets_t offset = 0; + switch (format()) { + case NestedArrayFormatId: + offset = getNestedArrayWorker()->storage_indices_offset; + break; + default: + return nullptr; + } + return reinterpret_cast(buffer + offset); + } + + Status getItemPrepare(const int64_t index, const size_t ndims) const { + if (index < 0 || index >= itemsCount()) { + RETURN_ERROR(IndexError); + } + if (format() != NestedArrayFormatId) { + RETURN_ERROR(NotSupportedFormatError); + } + if (getDimensions() != ndims) { + RETURN_ERROR(DimensionalityError); + } + return Success; + } + + inline sizes_t get_storage_index(const int64_t index) const { + return get_storage_indices_new()[index]; + } + // High-level API + Status getLength(const int64_t index, size_t& length) const { + const size_t ndims = getDimensions(); + Status status = getItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + if (ndims == 3) { + const auto* values_offsets = get_values_offsets(); + const auto values_offset = values_offsets[storage_index]; + if (values_offset < 0) { + length = 0; + return Success; + } + const auto* sizes_offsets = get_sizes_offsets(); + const auto* sizes_buffer = get_sizes_buffer(); + const auto sizes_offset = sizes_offsets[storage_index * ndims]; + length = sizes_buffer[sizes_offset]; + } else { + RETURN_ERROR(NotImplementedError); + } + + return Success; + } + + Status getLength(const int64_t index, const int64_t subindex, size_t& length) const { + const size_t ndims = getDimensions(); + Status status = getItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + if (ndims == 3) { + const auto* values_offsets = get_values_offsets(); + const auto values_offset = values_offsets[storage_index]; + if (values_offset < 0) { + RETURN_ERROR(IndexError); + } + const auto* sizes_offsets = get_sizes_offsets(); + const auto* sizes_buffer = get_sizes_buffer(); + const auto sizes2_offset = sizes_offsets[storage_index * ndims + 1]; + length = (sizes_buffer + sizes2_offset)[subindex]; + } else { + RETURN_ERROR(NotImplementedError); + } + return Success; + } + + Status getLength(const int64_t index, + const int64_t subindex, + const int64_t subsubindex, + size_t& length) const { + const size_t ndims = getDimensions(); + Status status = getItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + if (ndims == 3) { + const auto* values_offsets = get_values_offsets(); + const auto values_offset = values_offsets[storage_index]; + if (values_offset < 0) { + RETURN_ERROR(IndexError); + } + const auto* sizes_offsets = get_sizes_offsets(); + const auto* sizes_buffer = get_sizes_buffer(); + const auto sizes2_offset = sizes_offsets[storage_index * ndims + 1]; + const auto sizes3_offset = sizes_offsets[storage_index * ndims + 2]; + offsets_t soffset = 0; + for (int64_t i = 0; i < subindex; i++) { + soffset += (sizes_buffer + sizes2_offset)[i]; + } + length = (sizes_buffer + sizes3_offset + soffset)[subsubindex]; + } else { + RETURN_ERROR(NotImplementedError); + } + return Success; + } + + // TODO: parametrize sizes type + template + Status getItem(const int64_t index, + std::vector& values, + std::vector& sizes, + std::vector& sizes_of_sizes, + bool& is_null) { + if constexpr (!std::is_same::value) { + if constexpr (std::is_same::value) { + const auto* metadata = getNestedArrayMetadata(); + if (metadata->value_type != PointFloat64) { + RETURN_ERROR(TypeError); + } + } else if constexpr (std::is_same::value) { + const auto* metadata = getNestedArrayMetadata(); + if (metadata->value_type != PointInt32) { + RETURN_ERROR(TypeError); + } + } else { + RETURN_ERROR(NotImplementedError); + } + } + int32_t nof_values; + int8_t* values_ptr; + int32_t nof_sizes; + int8_t* sizes_ptr; + int32_t nof_sizes_of_sizes; + int8_t* sizes_of_sizes_ptr; + + Status status = getItem(index, + nof_values, + values_ptr, + nof_sizes, + sizes_ptr, + nof_sizes_of_sizes, + sizes_of_sizes_ptr, + is_null); + + if (status != Success) { + return status; + } + if (is_null) { + return Success; + } + const auto valuesize = getValueSize(); + const auto values_count = nof_values * valuesize / sizeof(CT); + values.reserve(values_count); + values.insert(values.end(), + reinterpret_cast(values_ptr), + reinterpret_cast(values_ptr) + values_count); + + sizes.reserve(nof_sizes); + sizes.insert(sizes.end(), + reinterpret_cast(sizes_ptr), + reinterpret_cast(sizes_ptr + nof_sizes * sizeof(sizes_t))); + + sizes_of_sizes.reserve(nof_sizes_of_sizes); + sizes_of_sizes.insert(sizes_of_sizes.end(), + reinterpret_cast(sizes_of_sizes_ptr), + reinterpret_cast( + sizes_of_sizes_ptr + nof_sizes_of_sizes * sizeof(sizes_t))); + + return Success; + } + + template + Status getItem(const int64_t index, + const int64_t subindex, + std::vector& values, + std::vector& sizes, + bool& is_null) { + if constexpr (!std::is_same::value) { + if constexpr (std::is_same::value) { + const auto* metadata = getNestedArrayMetadata(); + if (metadata->value_type != PointFloat64) { + RETURN_ERROR(TypeError); + } + } else if constexpr (std::is_same::value) { + const auto* metadata = getNestedArrayMetadata(); + if (metadata->value_type != PointInt32) { + RETURN_ERROR(TypeError); + } + } else { + RETURN_ERROR(NotImplementedError); + } + } + int32_t nof_values; + int8_t* values_ptr; + int32_t nof_sizes; + int8_t* sizes_ptr; + + Status status = + getItem(index, subindex, nof_values, values_ptr, nof_sizes, sizes_ptr, is_null); + + if (status != Success) { + return status; + } + if (is_null) { + return Success; + } + const auto valuesize = getValueSize(); + const auto values_count = nof_values * valuesize / sizeof(CT); + values.reserve(values_count); + values.insert(values.end(), + reinterpret_cast(values_ptr), + reinterpret_cast(values_ptr) + values_count); + + sizes.reserve(nof_sizes); + sizes.insert(sizes.end(), + reinterpret_cast(sizes_ptr), + reinterpret_cast(sizes_ptr + nof_sizes * sizeof(sizes_t))); + + return Success; + } + + template + Status getItem(const int64_t index, + const int64_t subindex, + const int64_t subsubindex, + std::vector& values, + bool& is_null) { + if constexpr (!std::is_same::value) { + if constexpr (std::is_same::value) { + const auto* metadata = getNestedArrayMetadata(); + if (metadata->value_type != PointFloat64) { + RETURN_ERROR(TypeError); + } + } else if constexpr (std::is_same::value) { + const auto* metadata = getNestedArrayMetadata(); + if (metadata->value_type != PointInt32) { + RETURN_ERROR(TypeError); + } + } else { + RETURN_ERROR(NotImplementedError); + } + } + int32_t nof_values; + int8_t* values_ptr; + + Status status = + getItem(index, subindex, subsubindex, nof_values, values_ptr, is_null); + if (status != Success) { + return status; + } + if (is_null) { + return Success; + } + const auto valuesize = getValueSize(); + const auto values_count = nof_values * valuesize / sizeof(CT); + values.reserve(values_count); + values.insert(values.end(), + reinterpret_cast(values_ptr), + reinterpret_cast(values_ptr) + values_count); + return Success; + } + + Status getItem(const int64_t index, + const int64_t subindex, + int32_t& nof_values, + int8_t*& values, + int32_t& nof_sizes, + int8_t*& sizes, + bool& is_null) { + const size_t ndims = 3; + Status status = getItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + auto* values_offsets = get_values_offsets(); + auto values_offset = values_offsets[storage_index]; + if (values_offset < 0) { + is_null = true; + nof_values = 0; + nof_sizes = 0; + values = nullptr; + sizes = nullptr; + return Success; + } + + const auto* sizes_offsets = get_sizes_offsets(); + auto* sizes_buffer = get_sizes_buffer(); + auto* values_buffer = get_values_buffer(); + const auto valuesize = getValueSize(); + const auto sizes2_offset = sizes_offsets[storage_index * ndims + 1]; + const auto sizes3_offset = sizes_offsets[storage_index * ndims + 2]; + + is_null = false; + offsets_t soffset = 0; + for (int64_t i = 0; i < subindex; i++) { + soffset += (sizes_buffer + sizes2_offset)[i]; + } + nof_sizes = (sizes_buffer + sizes2_offset)[subindex]; + values = values_buffer + (values_offset + soffset) * valuesize; + sizes = reinterpret_cast(sizes_buffer + sizes3_offset + soffset); + nof_values = 0; + for (int64_t i = 0; i < nof_sizes; i++) { + nof_values += sizes[i]; + } + return Success; + } + + Status getItem(const int64_t index, + const int64_t subindex, + const int64_t subsubindex, + int32_t& nof_values, + int8_t*& values, + bool& is_null) { + const size_t ndims = 3; + Status status = getItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + auto* values_offsets = get_values_offsets(); + auto values_offset = values_offsets[storage_index]; + if (values_offset < 0) { + is_null = true; + nof_values = 0; + values = nullptr; + return Success; + } + + const auto* sizes_offsets = get_sizes_offsets(); + auto* sizes_buffer = get_sizes_buffer(); + auto* values_buffer = get_values_buffer(); + const auto valuesize = getValueSize(); + const auto sizes2_offset = sizes_offsets[storage_index * ndims + 1]; + const auto sizes3_offset = sizes_offsets[storage_index * ndims + 2]; + + is_null = false; + offsets_t soffset = 0; + for (int64_t i = 0; i < subindex; i++) { + soffset += (sizes_buffer + sizes2_offset)[i]; + } + offsets_t soffset2 = 0; + for (int64_t i = 0; i < subsubindex; i++) { + soffset2 += (sizes_buffer + sizes3_offset + soffset)[i]; + } + values = values_buffer + (values_offset + soffset + soffset2) * valuesize; + nof_values = (sizes_buffer + sizes3_offset + soffset)[subsubindex]; + return Success; + } + + Status getItem(const int64_t index, + int32_t& nof_values, + int8_t*& values, + int32_t& nof_sizes, + int8_t*& sizes, + int32_t& nof_sizes_of_sizes, + int8_t*& sizes_of_sizes, + bool& is_null) { + const size_t ndims = 3; + Status status = getItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + + auto* values_offsets = get_values_offsets(); + auto values_offset = values_offsets[storage_index]; + if (values_offset < 0) { + is_null = true; + nof_values = 0; + nof_sizes = 0; + nof_sizes_of_sizes = 0; + values = nullptr; + sizes = nullptr; + sizes_of_sizes = nullptr; + return Success; + } + + const auto* sizes_offsets = get_sizes_offsets(); + auto* sizes_buffer = get_sizes_buffer(); + auto* values_buffer = get_values_buffer(); + const auto valuesize = getValueSize(); + const auto sizes_offset = sizes_offsets[storage_index * ndims]; + const auto sizes2_offset = sizes_offsets[storage_index * ndims + 1]; + const auto sizes3_offset = sizes_offsets[storage_index * ndims + 2]; + const auto next_values_offset = values_offsets[storage_index + 1]; + + is_null = false; + if (next_values_offset < 0) { + nof_values = -(next_values_offset + 1) - values_offset; + } else { + nof_values = next_values_offset - values_offset; + } + values = values_buffer + values_offset * valuesize; + + nof_sizes_of_sizes = sizes_buffer[sizes_offset]; + sizes_of_sizes = reinterpret_cast(sizes_buffer + sizes2_offset); + sizes = reinterpret_cast(sizes_buffer + sizes3_offset); + nof_sizes = 0; + for (int32_t i = 0; i < nof_sizes_of_sizes; i++) { + nof_sizes += (sizes_buffer + sizes2_offset)[i]; + } + return Success; + } + + Status setItemPrepare(const int64_t index, const size_t ndims) { + if (index < 0 || index >= itemsCount()) { + RETURN_ERROR(IndexError); + } + if (format() != NestedArrayFormatId) { + RETURN_ERROR(NotSupportedFormatError); + } + if (getDimensions() != ndims) { + RETURN_ERROR(DimensionalityError); + } + auto* storage_indices = get_storage_indices_new(); + if (storage_indices[index] >= 0) { + RETURN_ERROR(ItemAlreadySpecifiedError); + } + auto* worker = getNestedArrayWorker(); + auto storage_index = worker->specified_items_count; + storage_indices[index] = storage_index; + worker->specified_items_count++; + return Success; + } + + // TODO: rename to setNull + Status setNullNew(int64_t index) { + const size_t ndims = getDimensions(); + Status status = setItemPrepare(index, ndims); + if (status != Success) { + RETURN_ERROR(status); + } + const auto storage_index = get_storage_index(index); + + auto* values_offsets = get_values_offsets(); + auto* sizes_offsets = get_sizes_offsets(); + + const auto values_offset = values_offsets[storage_index]; + const auto sizes_offset = sizes_offsets[storage_index * ndims]; + auto* sizes_buffer = get_sizes_buffer(); + + sizes_buffer[sizes_offset] = 0; + for (size_t i = 0; i < ndims; i++) { + sizes_offsets[storage_index * ndims + i + 1] = sizes_offset + 1; + } + values_offsets[storage_index] = -(values_offset + 1); + values_offsets[storage_index + 1] = values_offset; + return Success; + } + + Status setItem(const int64_t index, std::vector& arr) { + const size_t ndims = 1; + Status status = setItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + + auto* values_offsets = get_values_offsets(); + auto* sizes_offsets = get_sizes_offsets(); + auto* sizes_buffer = get_sizes_buffer(); + auto* values_buffer = get_values_buffer(); + const auto* metadata = getNestedArrayMetadata(); + const auto valuesize = getValueSize(); + + auto values_offset = values_offsets[storage_index]; + const auto sizes_offset = sizes_offsets[storage_index * ndims]; + + sizes_t sz = (arr.size() * sizeof(double)) / valuesize; + sizes_buffer[sizes_offset] = sz; + + if (values_offset + sz > metadata->total_values_count) { + RETURN_ERROR(ValuesBufferTooSmallError); + } + if (memcpy(values_buffer + values_offset * valuesize, arr.data(), sz * valuesize) == + nullptr) { + RETURN_ERROR(MemoryError); + } + values_offset += sz; + + sizes_offsets[storage_index * ndims + 1] = sizes_offset + 1; + values_offsets[storage_index + 1] = values_offset; + return Success; + } + + Status setItem(const int64_t index, const std::vector>& item) { + const size_t ndims = 2; + Status status = setItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + + auto* values_offsets = get_values_offsets(); + auto* sizes_offsets = get_sizes_offsets(); + auto* sizes_buffer = get_sizes_buffer(); + auto* values_buffer = get_values_buffer(); + const auto* metadata = getNestedArrayMetadata(); + const auto valuesize = getValueSize(); + const sizes_t size = item.size(); + + auto values_offset = values_offsets[storage_index]; + const auto sizes_offset = sizes_offsets[storage_index * ndims]; + const auto sizes2_offset = sizes_offset + 1; + if (sizes2_offset + size > metadata->total_sizes_count) { + RETURN_ERROR(SizesBufferTooSmallError); + } + sizes_buffer[sizes_offset] = size; + for (sizes_t i = 0; i < size; i++) { + std::vector arr = item[i]; + sizes_t sz = (arr.size() * sizeof(double)) / valuesize; + sizes_buffer[sizes2_offset + i] = sz; + if (values_offset + sz > metadata->total_values_count) { + RETURN_ERROR(ValuesBufferTooSmallError); + } + if (memcpy(values_buffer + values_offset * valuesize, arr.data(), sz * valuesize) == + nullptr) { + RETURN_ERROR(MemoryError); + } + values_offset += sz; + } + sizes_offsets[storage_index * ndims + 1] = sizes2_offset; + sizes_offsets[storage_index * ndims + 2] = sizes2_offset + size; + values_offsets[storage_index + 1] = values_offset; + return Success; + } + + template + Status setItem(const int64_t index, + const std::vector>>& item) { + const size_t ndims = 3; + Status status = setItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + + auto* values_offsets = get_values_offsets(); + auto* sizes_offsets = get_sizes_offsets(); + auto* sizes_buffer = get_sizes_buffer(); + auto* values_buffer = get_values_buffer(); + const auto* metadata = getNestedArrayMetadata(); + const auto valuesize = getValueSize(); + const sizes_t size = item.size(); + + auto values_offset = values_offsets[storage_index]; + const auto sizes_offset = sizes_offsets[storage_index * ndims]; + const auto sizes2_offset = sizes_offset + 1; + const auto sizes3_offset = sizes2_offset + size; + sizes_t i3 = 0; + sizes_buffer[sizes_offset] = size; + + for (sizes_t i = 0; i < size; i++) { + const std::vector>& item2 = item[i]; + sizes_t size2 = item2.size(); + if (sizes3_offset + i3 + size2 > metadata->total_sizes_count) { + RETURN_ERROR(SizesBufferTooSmallError); + } + sizes_buffer[sizes2_offset + i] = size2; + for (sizes_t j = 0; j < size2; j++) { + const std::vector& arr = item2[j]; + sizes_t sz = (arr.size() * sizeof(CT)) / valuesize; + sizes_buffer[sizes3_offset + i3] = sz; + i3 += 1; + if (values_offset + sz > metadata->total_values_count) { + RETURN_ERROR(ValuesBufferTooSmallError); + } + if (memcpy(values_buffer + values_offset * valuesize, + arr.data(), + sz * valuesize) == nullptr) { + RETURN_ERROR(MemoryError); + } + values_offset += sz; + } + } + sizes_offsets[storage_index * ndims + 1] = sizes2_offset; + sizes_offsets[storage_index * ndims + 2] = sizes3_offset; + sizes_offsets[storage_index * ndims + 3] = sizes3_offset + i3; + values_offsets[storage_index + 1] = values_offset; + return Success; + } + + template + Status setItem(const int64_t index, + const std::vector& values, + const std::vector& sizes, + const std::vector& sizes_of_sizes) { + const auto* metadata = getNestedArrayMetadata(); + if constexpr (!std::is_same::value) { + if constexpr (std::is_same::value) { + if (metadata->value_type != PointFloat64) { + RETURN_ERROR(TypeError); + } + } else if constexpr (std::is_same::value) { + if (metadata->value_type != PointInt32) { + RETURN_ERROR(TypeError); + } + } else { + RETURN_ERROR(NotImplementedError); + } + } + return setItem(index, + reinterpret_cast(values.data()), + values.size() * sizeof(CT), + sizes.data(), + sizes.size(), + sizes_of_sizes.data(), + sizes_of_sizes.size()); + } + + Status setItem(const int64_t index, + const int8_t* values_buf, + const size_t values_buf_size, // in bytes + const int32_t* sizes_buf, + const int32_t nof_sizes, + const int32_t* sizes_of_sizes_buf, + const int32_t nof_sizes_of_sizes) { + const size_t ndims = 3; + Status status = setItemPrepare(index, ndims); + if (status != Success) { + RETURN_ERROR(status); + } + const auto* metadata = getNestedArrayMetadata(); + const auto storage_index = get_storage_index(index); + + auto* values_offsets = get_values_offsets(); + auto* sizes_offsets = get_sizes_offsets(); + auto* sizes_buffer = get_sizes_buffer(); + auto* values_buffer = get_values_buffer(); + const auto valuesize = getValueSize(); + auto values_offset = values_offsets[storage_index]; + const auto sizes_offset = sizes_offsets[storage_index * ndims]; + const auto sizes2_offset = sizes_offset + 1; + const auto sizes3_offset = sizes2_offset + nof_sizes_of_sizes; + if (sizes_offset + 1 + nof_sizes_of_sizes + nof_sizes > metadata->total_sizes_count) { + RETURN_ERROR(SizesBufferTooSmallError); + } + sizes_t sum_sizes_of_sizes = 0; + for (sizes_t i = 0; i < nof_sizes_of_sizes; i++) { + sum_sizes_of_sizes += sizes_of_sizes_buf[i]; + } + if (sum_sizes_of_sizes != nof_sizes) { + RETURN_ERROR(InconsistentSizesError); + } + sizes_t sum_sizes = 0; + for (sizes_t i = 0; i < nof_sizes; i++) { + sum_sizes += sizes_buf[i]; + } + sizes_t values_count = values_buf_size / valuesize; + if (sum_sizes != values_count) { + RETURN_ERROR(InconsistentSizesError); + } + if (values_offset + values_count > metadata->total_values_count) { + RETURN_ERROR(ValuesBufferTooSmallError); + } + sizes_buffer[sizes_offset] = nof_sizes_of_sizes; + if (memcpy(sizes_buffer + sizes2_offset, + sizes_of_sizes_buf, + nof_sizes_of_sizes * sizeof(int32_t)) == nullptr) { + RETURN_ERROR(MemoryError); + } + if (memcpy(sizes_buffer + sizes3_offset, sizes_buf, nof_sizes * sizeof(int32_t)) == + nullptr) { + RETURN_ERROR(MemoryError); + } + if (memcpy(values_buffer + values_offset * valuesize, values_buf, values_buf_size) == + nullptr) { + RETURN_ERROR(MemoryError); + } + sizes_offsets[storage_index * ndims + 1] = sizes2_offset; + sizes_offsets[storage_index * ndims + 2] = sizes3_offset; + sizes_offsets[storage_index * ndims + 3] = sizes3_offset + sum_sizes_of_sizes; + values_offsets[storage_index + 1] = values_offset + values_count; + return Success; + } + // Set a new item with index and size (in bytes) and initialize its // elements from source buffer. The item values will be // uninitialized when source buffer is nullptr. If dest != nullptr // then the item's buffer pointer will be stored in *dest. + // To be deprecated in favor of NestedArray format Status setItem(const int64_t index, const int8_t* src, const int64_t size, int8_t** dest = nullptr) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1102,20 +2427,20 @@ struct FlatBufferManager { // bytes? } if (storage_indices[index] >= 0) { - return ItemAlreadySpecifiedError; + RETURN_ERROR(ItemAlreadySpecifiedError); } const int64_t cindex = compressed_indices[storage_count]; const int64_t values_buffer_size = get_values_buffer_size(); const int64_t csize = cindex * itemsize; if (csize + size > values_buffer_size) { - return ValuesBufferTooSmallError; + RETURN_ERROR(ValuesBufferTooSmallError); } break; } case GeoPointFormatId: { const int64_t itemsize = dtypeSize(); if (size != itemsize) { - return SizeError; + RETURN_ERROR(SizeError); } break; } @@ -1125,12 +2450,13 @@ struct FlatBufferManager { return setItemCountsAndData(index, &counts, 1, src, dest); } default: - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } return setItemNoValidation(index, src, size, dest); } // Same as setItem but performs no input validation + // To be deprecated in favor of NestedArray format Status setItemNoValidation(const int64_t index, const int8_t* src, const int64_t size, @@ -1149,7 +2475,7 @@ struct FlatBufferManager { storage_indices[index] = storage_count; compressed_indices[storage_count + 1] = cindex + values_count; if (size > 0 && src != nullptr && memcpy(values + csize, src, size) == nullptr) { - return MemoryError; + RETURN_ERROR(MemoryError); } if (dest != nullptr) { *dest = values + csize; @@ -1162,7 +2488,7 @@ struct FlatBufferManager { const int64_t itemsize = dtypeSize(); const int64_t csize = index * itemsize; if (src != nullptr && memcpy(values + csize, src, size) == nullptr) { - return MemoryError; + RETURN_ERROR(MemoryError); } if (dest != nullptr) { *dest = values + csize; @@ -1175,25 +2501,26 @@ struct FlatBufferManager { return setItemCountsAndDataNoValidation(index, &counts, 1, src, dest); } default: - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } return Success; } + // To be deprecated in favor of NestedArray format Status setItemCountsAndData(const int64_t index, const int32_t* counts, const int64_t nof_counts, const int8_t* src, int8_t** dest = nullptr) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: - return NotSupportedFormatError; + RETURN_ERROR(NotSupportedFormatError); case GeoPolygonFormatId: { const int64_t& storage_count = get_storage_count(); const int64_t& storage_count2 = get_storage_count2(); @@ -1202,14 +2529,14 @@ struct FlatBufferManager { const int64_t* storage_indices = get_storage_indices(); const int64_t valuesize = dtypeSize(); if (storage_indices[index] >= 0) { - return ItemAlreadySpecifiedError; + RETURN_ERROR(ItemAlreadySpecifiedError); } const int64_t compressed_indices2_buffer_size = get_compressed_indices2_buffer_size(); if (compressed_indices[storage_count] + nof_counts > compressed_indices2_buffer_size) { - return CompressedIndices2BufferTooSmallError; + RETURN_ERROR(CompressedIndices2BufferTooSmallError); } const int64_t offset = compressed_indices2[storage_count2] * valuesize; @@ -1219,16 +2546,17 @@ struct FlatBufferManager { } const int64_t values_buffer_size = get_values_buffer_size(); if (offset + size > values_buffer_size) { - return ValuesBufferTooSmallError; + RETURN_ERROR(ValuesBufferTooSmallError); } break; } default: - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } return setItemCountsAndDataNoValidation(index, counts, nof_counts, src, dest); } + // To be deprecated in favor of NestedArray format // Same as setItem but performs no input validation Status setItemCountsAndDataNoValidation( const int64_t index, @@ -1240,7 +2568,7 @@ struct FlatBufferManager { case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: - return NotSupportedFormatError; + RETURN_ERROR(NotSupportedFormatError); case GeoPolygonFormatId: { int64_t& storage_count = get_storage_count(); int64_t& storage_count2 = get_storage_count2(); @@ -1266,7 +2594,7 @@ struct FlatBufferManager { compressed_indices2[storage_count2] = cindex2; } if (size > 0 && src != nullptr && memcpy(values + offset, src, size) == nullptr) { - return MemoryError; + RETURN_ERROR(MemoryError); } if (dest != nullptr) { *dest = values + offset; @@ -1275,19 +2603,20 @@ struct FlatBufferManager { break; } default: - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } return Success; } + // To be deprecated in favor of NestedArray format Status setSubItem(const int64_t index, const int64_t subindex, const int8_t* src, const int64_t size, int8_t** dest = nullptr) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1298,13 +2627,13 @@ struct FlatBufferManager { const int64_t* storage_indices = get_storage_indices(); const int64_t storage_index = storage_indices[index]; if (storage_index < 0) { - return ItemUnspecifiedError; + RETURN_ERROR(ItemUnspecifiedError); } int64_t* compressed_indices = get_compressed_indices(); const int64_t cindex = compressed_indices[storage_index]; if (cindex < 0) { if (size > 0) { - return UnexpectedNullItemError; + RETURN_ERROR(UnexpectedNullItemError); } } else { const int64_t next_cindex = compressed_indices[storage_index + 1]; @@ -1319,17 +2648,18 @@ struct FlatBufferManager { const int64_t next_cindex2 = compressed_indices2[cindex + subindex + 1]; const int64_t expected_size = (next_cindex2 - cindex2) * valuesize; if (expected_size != size) { - return SizeError; + RETURN_ERROR(SizeError); } } break; } default: - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } return setSubItemNoValidation(index, subindex, src, size, dest); } + // To be deprecated in favor of NestedArray format Status setSubItemNoValidation(const int64_t index, const int64_t subindex, const int8_t* src, @@ -1339,7 +2669,7 @@ struct FlatBufferManager { case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: - return NotSupportedFormatError; + RETURN_ERROR(NotSupportedFormatError); case GeoPolygonFormatId: { const int64_t* storage_indices = get_storage_indices(); const int64_t storage_index = storage_indices[index]; @@ -1351,7 +2681,7 @@ struct FlatBufferManager { const int64_t cindex2 = compressed_indices2[cindex + subindex]; const int64_t offset = cindex2 * valuesize; if (size > 0 && src != nullptr && memcpy(values + offset, src, size) == nullptr) { - return MemoryError; + RETURN_ERROR(MemoryError); } if (dest != nullptr) { *dest = values + offset; @@ -1359,7 +2689,7 @@ struct FlatBufferManager { break; } default: - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } return Success; } @@ -1371,9 +2701,10 @@ struct FlatBufferManager { return setItemNoValidation(index, nullptr, size, dest); } + // To be deprecated in favor of NestedArray format Status concatItem(int64_t index, const int8_t* src, int64_t size) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1389,15 +2720,15 @@ struct FlatBufferManager { return setItem(index, src, size, nullptr); } if (size % itemsize != 0) { - return SizeError; + RETURN_ERROR(SizeError); } if (storage_index != storage_count) { - return IndexError; // index does not correspond to the last set - // item, only the last item can be - // concatenated + RETURN_ERROR(IndexError); // index does not correspond to the last set + // item, only the last item can be + // concatenated } if (compressed_indices[storage_index] < 0) { - return NotImplementedError; // todo: support concat to null when last + RETURN_ERROR(NotImplementedError); // todo: support concat to null when last } int64_t values_count = compressed_indices[next_storage_count] - compressed_indices[storage_index]; @@ -1406,19 +2737,20 @@ struct FlatBufferManager { int8_t* ptr = values + compressed_indices[storage_index] * itemsize; if (size > 0 && src != nullptr && memcpy(ptr + values_count * itemsize, src, size) == nullptr) { - return MemoryError; + RETURN_ERROR(MemoryError); } return Success; } default:; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } // Set item with index as a null item + // To be deprecated in favor of NestedArray format Status setNull(int64_t index) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1433,11 +2765,17 @@ struct FlatBufferManager { case GeoPointFormatId: { return setNullNoValidation(index); } + case NestedArrayFormatId: { + return setNullNew(index); + } + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } // Same as setNull but performs no input validation + // To be deprecated in favor of NestedArray format Status setNullNoValidation(int64_t index) { switch (format()) { case VarlenArrayFormatId: @@ -1471,16 +2809,29 @@ struct FlatBufferManager { break; } default: - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } return Success; } // Check if the item is unspecified or null. Status isNull(int64_t index, bool& is_null) const { + if (isNestedArray()) { + const size_t ndims = getDimensions(); + Status status = getItemPrepare(index, ndims); + if (status != Success) { + return status; + } + const auto storage_index = get_storage_index(index); + const auto* values_offsets = get_values_offsets(); + const auto values_offset = values_offsets[storage_index]; + is_null = values_offset < 0; + return Success; + } if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } + // To be deprecated in favor of NestedArray format: switch (format()) { case VarlenArrayFormatId: case GeoLineStringFormatId: @@ -1489,7 +2840,7 @@ struct FlatBufferManager { const int64_t* storage_indices = get_storage_indices(); const int64_t storage_index = storage_indices[index]; if (storage_index < 0) { - return ItemUnspecifiedError; + RETURN_ERROR(ItemUnspecifiedError); } is_null = (compressed_indices[storage_index] < 0); return Success; @@ -1509,16 +2860,19 @@ struct FlatBufferManager { } return Success; } + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } // Get item at index by storing its size (in bytes), values buffer, // and nullity information to the corresponding pointer // arguments. + // To be deprecated in favor of NestedArray format HOST DEVICE Status getItem(int64_t index, int64_t& size, int8_t*& dest, bool& is_null) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1557,10 +2911,13 @@ struct FlatBufferManager { } case GeoPolygonFormatId: break; + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } + // To be deprecated in favor of NestedArray format HOST DEVICE Status getItem(int64_t index, size_t& size, int8_t*& dest, bool& is_null) { int64_t sz{0}; Status status = getItem(index, sz, dest, is_null); @@ -1568,13 +2925,14 @@ struct FlatBufferManager { return status; } + // To be deprecated in favor of NestedArray format HOST DEVICE Status getItem2(int64_t index, int64_t*& cumcounts, int64_t& nof_counts, int8_t*& dest, bool& is_null) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1589,7 +2947,7 @@ struct FlatBufferManager { const int64_t storage_index = storage_indices[index]; if (storage_index < 0) { - return ItemUnspecifiedError; + RETURN_ERROR(ItemUnspecifiedError); } const int64_t cindex = compressed_indices[storage_index]; if (cindex < 0) { @@ -1609,10 +2967,13 @@ struct FlatBufferManager { } return Success; } + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } + // To be deprecated in favor of NestedArray format HOST DEVICE Status getItemCountsAndData(const int64_t index, int32_t*& counts, int64_t& nof_counts, @@ -1620,7 +2981,7 @@ struct FlatBufferManager { int64_t& size, bool& is_null) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1635,7 +2996,7 @@ struct FlatBufferManager { const int64_t storage_index = storage_indices[index]; if (storage_index < 0) { - return ItemUnspecifiedError; + RETURN_ERROR(ItemUnspecifiedError); } const int64_t cindex = compressed_indices[storage_index]; if (cindex < 0) { @@ -1658,13 +3019,16 @@ struct FlatBufferManager { } return Success; } + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } + // To be deprecated in favor of NestedArray format Status getItemLength(const int64_t index, int64_t& length) const { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case GeoPointFormatId: @@ -1676,7 +3040,7 @@ struct FlatBufferManager { const int64_t* storage_indices = get_storage_indices(); const int64_t storage_index = storage_indices[index]; if (storage_index < 0) { - return ItemUnspecifiedError; + RETURN_ERROR(ItemUnspecifiedError); } const int64_t cindex = compressed_indices[storage_index]; if (cindex < 0) { @@ -1687,15 +3051,18 @@ struct FlatBufferManager { } return Success; } + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } + // To be deprecated in favor of NestedArray format Status getSubItemLength(const int64_t index, const int64_t subindex, int64_t& length) const { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: @@ -1707,7 +3074,7 @@ struct FlatBufferManager { const int64_t* compressed_indices = get_compressed_indices(); const int64_t storage_index = storage_indices[index]; if (storage_index < 0) { - return ItemUnspecifiedError; + RETURN_ERROR(ItemUnspecifiedError); } const int64_t cindex = compressed_indices[storage_index]; if (cindex < 0) { @@ -1726,29 +3093,32 @@ struct FlatBufferManager { } return Success; } + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } // Get a subitem data of an item, e.g. a linestring within a polygon + // To be deprecated in favor of NestedArray format HOST DEVICE Status getSubItem(int64_t index, int64_t subindex, int64_t& size, int8_t*& dest, bool& is_null) { if (index < 0 || index >= itemsCount()) { - return IndexError; + RETURN_ERROR(IndexError); } switch (format()) { case VarlenArrayFormatId: case GeoPointFormatId: case GeoLineStringFormatId: - return NotSupportedFormatError; + RETURN_ERROR(NotSupportedFormatError); case GeoPolygonFormatId: { const int64_t* storage_indices = get_storage_indices(); const int64_t storage_index = storage_indices[index]; if (storage_index < 0) { - return ItemUnspecifiedError; + RETURN_ERROR(ItemUnspecifiedError); } int64_t* compressed_indices = get_compressed_indices(); const int64_t cindex = compressed_indices[storage_index]; @@ -1774,20 +3144,136 @@ struct FlatBufferManager { } return Success; } + default: + break; } - return UnknownFormatError; + RETURN_ERROR(UnknownFormatError); } #ifdef HAVE_TOSTRING #define HAVE_FLATBUFFER_TOSTRING + std::string bufferToString(const int8_t* buffer, + const size_t size, + ValueType value_type) const { + size_t value_size = get_size(value_type); + size_t count = size / value_size; + std::string result = ""; + for (size_t i = 0; i < count; i++) { + if (i > 0) { + result += ", "; + } + switch (value_type) { + case Bool8: + result += (buffer[i] ? "true" : "false"); + break; + case Int8: + result += std::to_string(buffer[i]); + break; + case Int16: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case Int32: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case Int64: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case UInt8: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case UInt16: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case UInt32: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case UInt64: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case Float32: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case Float64: + result += std::to_string(reinterpret_cast(buffer)[i]); + break; + case PointInt32: + result += "("; + if (containsNullValue(buffer + 2 * i * sizeof(int32_t))) { + result += "NULL"; + } else { + result += std::to_string(reinterpret_cast(buffer)[2 * i]); + result += ", "; + result += std::to_string(reinterpret_cast(buffer)[2 * i + 1]); + } + result += ")"; + break; + case PointFloat64: + result += "("; + if (containsNullValue(buffer + 2 * i * sizeof(double))) { + result += "NULL"; + } else { + result += std::to_string(reinterpret_cast(buffer)[2 * i]); + result += ", "; + result += std::to_string(reinterpret_cast(buffer)[2 * i + 1]); + } + result += ")"; + break; + } + } + return result; + } + std::string toString() const { if (buffer == nullptr) { return ::typeName(this) + "[UNINITIALIZED]"; } - std::string result = typeName(this) + "("; + std::string result = typeName(this) + "@" + ::toString((void*)buffer) + "("; result += "" + getBaseWorker()->toString(); - const auto fmt = format(); + if (isNestedArray()) { + const auto* metadata = getNestedArrayMetadata(); + const auto* worker = getNestedArrayWorker(); + result += ",\n " + metadata->toString(); + result += ",\n " + worker->toString(); + result += ",\n values_buffer=[" + + bufferToString( + get_values_buffer(), getValuesBufferSize(), metadata->value_type) + + "]"; + result += ",\n sizes_buffer=[" + + bufferToString( + reinterpret_cast(get_sizes_buffer()), + metadata->total_sizes_count * get_size(FLATBUFFER_SIZES_T_VALUE_TYPE), + FLATBUFFER_SIZES_T_VALUE_TYPE) + + "]"; + result += ",\n values_offsets=[" + + bufferToString(reinterpret_cast(get_values_offsets()), + (metadata->total_items_count + 1) * + get_size(FLATBUFFER_OFFSETS_T_VALUE_TYPE), + FLATBUFFER_OFFSETS_T_VALUE_TYPE) + + "]"; + result += ",\n sizes_offsets=[" + + bufferToString(reinterpret_cast(get_sizes_offsets()), + (metadata->total_items_count * metadata->dimensions + 1) * + get_size(FLATBUFFER_OFFSETS_T_VALUE_TYPE), + FLATBUFFER_OFFSETS_T_VALUE_TYPE) + + "]"; + result += ",\n storage_indices=[" + + bufferToString( + reinterpret_cast(get_storage_indices_new()), + metadata->total_items_count * get_size(FLATBUFFER_SIZES_T_VALUE_TYPE), + FLATBUFFER_SIZES_T_VALUE_TYPE) + + "]"; + result += ",\n user_data_buffer=[" + + bufferToString(get_user_data_buffer(), metadata->user_data_size, Int8) + + "]"; + result += ")"; + return result; + } + + // To be deprecated in favor of NestedArray format: + const FlatBufferFormat fmt = format(); + + std::cout << "fmt=" << static_cast(fmt) << ", " << sizeof(fmt) << std::endl; switch (fmt) { case VarlenArrayFormatId: { result += ", " + getVarlenArrayMetadata()->toString(); @@ -1809,6 +3295,8 @@ struct FlatBufferManager { result += ", " + getGeoPolygonWorker()->toString(); break; } + default: + break; } switch (fmt) { @@ -1904,6 +3392,64 @@ struct FlatBufferManager { }; #ifdef HAVE_TOSTRING +inline std::ostream& operator<<(std::ostream& os, + FlatBufferManager::ValueType const type) { + switch (type) { + case FlatBufferManager::Bool8: + os << "Bool8"; + break; + case FlatBufferManager::Int8: + os << "Int8"; + break; + case FlatBufferManager::Int16: + os << "Int16"; + break; + case FlatBufferManager::Int32: + os << "Int32"; + break; + case FlatBufferManager::Int64: + os << "Int64"; + break; + case FlatBufferManager::UInt8: + os << "UInt8"; + break; + case FlatBufferManager::UInt16: + os << "UInt16"; + break; + case FlatBufferManager::UInt32: + os << "UInt32"; + break; + case FlatBufferManager::UInt64: + os << "UInt64"; + break; + case FlatBufferManager::Float32: + os << "Float32"; + break; + case FlatBufferManager::Float64: + os << "Float64"; + break; + case FlatBufferManager::PointInt32: + os << "PointInt32"; + break; + case FlatBufferManager::PointFloat64: + os << "PointFloat64"; + break; + } + return os; +} + +inline std::string FlatBufferManager::toString(const FlatBufferManager::ValueType& type) { + std::ostringstream ss; + ss << type; + return ss.str(); +} + +inline std::string toString(const FlatBufferManager::ValueType& type) { + std::ostringstream ss; + ss << type; + return ss.str(); +} + inline std::ostream& operator<<(std::ostream& os, FlatBufferManager::Status const status) { switch (status) { @@ -1919,6 +3465,9 @@ inline std::ostream& operator<<(std::ostream& os, case FlatBufferManager::SizeError: os << "SizeError"; break; + case FlatBufferManager::FlatbufferSizeError: + os << "FlatbufferSizeError"; + break; case FlatBufferManager::ItemAlreadySpecifiedError: os << "ItemAlreadySpecifiedError"; break; @@ -1931,6 +3480,9 @@ inline std::ostream& operator<<(std::ostream& os, case FlatBufferManager::ValuesBufferTooSmallError: os << "ValuesBufferTooSmallError"; break; + case FlatBufferManager::SizesBufferTooSmallError: + os << "SizesBufferTooSmallError"; + break; case FlatBufferManager::CompressedIndices2BufferTooSmallError: os << "CompressedIndices2BufferTooSmallError"; break; @@ -1946,6 +3498,21 @@ inline std::ostream& operator<<(std::ostream& os, case FlatBufferManager::NotImplementedError: os << "NotImplementedError"; break; + case FlatBufferManager::InvalidUserDataError: + os << "InvalidUserDataError"; + break; + case FlatBufferManager::DimensionalityError: + os << "DimensionalityError"; + break; + case FlatBufferManager::UserDataError: + os << "UserDataError"; + break; + case FlatBufferManager::TypeError: + os << "TypeError"; + break; + case FlatBufferManager::InconsistentSizesError: + os << "InconsistentSizesError"; + break; default: os << "[Unknown FlatBufferManager::Status value]"; } @@ -1958,3 +3525,5 @@ inline std::string toString(const FlatBufferManager::Status& status) { return ss.str(); } #endif + +#undef RETURN_ERROR diff --git a/QueryEngine/heavydbTypes.h b/QueryEngine/heavydbTypes.h index 9ae3dcd024..b4bcbf77db 100644 --- a/QueryEngine/heavydbTypes.h +++ b/QueryEngine/heavydbTypes.h @@ -29,6 +29,8 @@ #include "DateAdd.h" +#include "../Shared/sqltypes_lite.h" + #if !(defined(__CUDACC__) || defined(NO_BOOST)) #include "../Shared/DateTimeParser.h" #endif @@ -725,7 +727,7 @@ struct GeoPolygonStruct { typedef struct GeoPolygonStruct GeoPolygon; -struct GeoMultiPolygon { +struct GeoMultiPolygonStruct { int8_t* ptr_coords; int32_t coords_size; int8_t* ring_sizes; @@ -752,6 +754,8 @@ struct GeoMultiPolygon { DEVICE int32_t getOutputSrid() const { return output_srid; } }; +typedef struct GeoMultiPolygonStruct GeoMultiPolygon; + // There are redundant #ifndef UDF_COMPILED inside // ifguard for StringDictionaryProxy to flag that // if we decide to adapt C++ UDF Compiler for table @@ -868,28 +872,56 @@ DEVICE inline double decompress_y_coord(const int8_t* data, } } -DEVICE inline Point2D get_point(const int8_t* data, - const int64_t index, - const int32_t input_srid, - const int32_t output_srid, - const bool is_geoint) { - Point2D point{decompress_x_coord(data, index, is_geoint), - decompress_y_coord(data, index, is_geoint)}; +DEVICE inline double decompress_x_coord(const int8_t* data, + const int64_t index, + const bool is_geoint, + const int32_t input_srid, + const int32_t output_srid) { + double x = decompress_x_coord(data, index, is_geoint); if (input_srid == output_srid || output_srid == 0) { - return point; + return x; } else if (input_srid == 4326 && output_srid == 900913) { // WGS 84 --> Web Mercator - point.x *= 111319.490778; - point.y = 6378136.99911 * log(tan(.00872664626 * point.y + .785398163397)); - return point; + x *= 111319.490778; + } else { +#ifndef __CUDACC__ + throw std::runtime_error("decompress_x_coord: unhandled geo transformation from " + + std::to_string(input_srid) + " to " + + std::to_string(output_srid) + '.'); +#endif } -#ifdef __CUDACC__ - return {}; // (NaN,NaN) -#else - throw std::runtime_error("Unhandled geo transformation from " + - std::to_string(input_srid) + " to " + - std::to_string(output_srid) + '.'); + return x; +} + +DEVICE inline double decompress_y_coord(const int8_t* data, + const int64_t index, + const bool is_geoint, + const int32_t input_srid, + const int32_t output_srid) { + double y = decompress_y_coord(data, index, is_geoint); + if (input_srid == output_srid || output_srid == 0) { + return y; + } else if (input_srid == 4326 && output_srid == 900913) { + // WGS 84 --> Web Mercator + y = 6378136.99911 * log(tan(.00872664626 * y + .785398163397)); + } else { +#ifndef __CUDACC__ + throw std::runtime_error("decompress_y_coord: unhandled geo transformation from " + + std::to_string(input_srid) + " to " + + std::to_string(output_srid) + '.'); #endif + } + return y; +} + +DEVICE inline Point2D get_point(const int8_t* data, + const int64_t index, + const int32_t input_srid, + const int32_t output_srid, + const bool is_geoint) { + Point2D point{decompress_x_coord(data, index, is_geoint, input_srid, output_srid), + decompress_y_coord(data, index, is_geoint, input_srid, output_srid)}; + return point; } #ifndef __CUDACC__ @@ -911,6 +943,7 @@ inline std::vector compress_coords(const int8_t* data, } return result; } + inline std::vector decompress_coords(const int8_t* data, const int64_t size, const bool is_geoint) { @@ -923,6 +956,71 @@ inline std::vector decompress_coords(const int8_t* data, } return result; } + +inline std::vector compress_coords(const std::vector& coords) { + std::vector result; + const size_t nofpoints = coords.size() / 2; + result.reserve(coords.size()); + const double* buf = coords.data(); + for (size_t i = 0; i < nofpoints; i++) { + result.push_back(compress_x_coord(buf, 2 * i)); + result.push_back(compress_y_coord(buf, 2 * i)); + } + return result; +} + +inline std::vector decompress_coords(const std::vector& coords) { + std::vector result; + const size_t nofpoints = coords.size() / 2; + result.reserve(coords.size()); + const int8_t* buf = reinterpret_cast(coords.data()); + for (size_t i = 0; i < nofpoints; i++) { + result.push_back(decompress_x_coord(buf, 2 * i, true)); + result.push_back(decompress_y_coord(buf, 2 * i, true)); + } + return result; +} + +inline std::vector> compress_coords( + const std::vector>& coords) { + std::vector> result; + result.reserve(coords.size()); + for (size_t i = 0; i < coords.size(); i++) { + result.push_back(compress_coords(coords[i])); + } + return result; +} + +inline std::vector> decompress_coords( + const std::vector>& coords) { + std::vector> result; + result.reserve(coords.size()); + for (size_t i = 0; i < coords.size(); i++) { + result.push_back(decompress_coords(coords[i])); + } + return result; +} + +inline std::vector>> compress_coords( + const std::vector>>& coords) { + std::vector>> result; + result.reserve(coords.size()); + for (size_t i = 0; i < coords.size(); i++) { + result.push_back(compress_coords(coords[i])); + } + return result; +} + +inline std::vector>> decompress_coords( + const std::vector>>& coords) { + std::vector>> result; + result.reserve(coords.size()); + for (size_t i = 0; i < coords.size(); i++) { + result.push_back(decompress_coords(coords[i])); + } + return result; +} + #endif inline bool get_is_geoint(const int8_t* flatbuffer) { @@ -983,7 +1081,7 @@ inline int32_t get_output_srid(const int8_t* flatbuffer) { } struct LineString { - int8_t* flatbuffer_; // FlatBuffer of GeoLineStrings or GeoPolygon + int8_t* flatbuffer_; // FlatBuffer of GeoLineStrings or GeoPolygon or GeoMultiPolygon int64_t index_[3]; // line string index within a // Column/Column/Column @@ -995,7 +1093,7 @@ struct LineString { status = m.getItem(index_[0], size, dest, is_null); break; case GeoPolygonFormatId: - status = m.getSubItem(index_[0], index_[1], size, dest, is_null); + status = m.getSubItem(index_[1], index_[0], size, dest, is_null); break; default: status = FlatBufferManager::Status::NotImplementedError; @@ -1005,10 +1103,49 @@ struct LineString { // Get the index-th point of the line string DEVICE Geo::Point2D getItem(const int64_t index, const int32_t output_srid = 0) const { + FlatBufferManager m{flatbuffer_}; + FlatBufferManager::Status status = FlatBufferManager::Status::NotImplementedError; + if (m.isNestedArray()) { + const SQLTypeInfoLite* ti = + reinterpret_cast(m.get_user_data_buffer()); + if (ti == nullptr) { + status = FlatBufferManager::UserDataError; + } else { + if (m.getDimensions() == 3) { + if (index_[2] >= 0) { + bool is_null = false; + int32_t nof_points; + int8_t* coords_ptr; + status = m.getItem( + index_[2], index_[1], index_[0], nof_points, coords_ptr, is_null); + if (status == FlatBufferManager::Status::Success) { + if (is_null) { + status = FlatBufferManager::Status::UnexpectedNullItemError; + } else if (index < 0 || index >= nof_points) { + status = FlatBufferManager::Status::IndexError; + } else { + return Geo::get_point( + coords_ptr, + 2 * index, + ti->get_input_srid(), + (output_srid < 0 ? ti->get_output_srid() : output_srid), + ti->is_geoint()); + } + } + } + } + } + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("LineString.getItem failed: " + ::toString(status)); +#endif + } + } + // to be deprecated in favor of NestedArray: int8_t* ptr; int64_t size; bool is_null; - auto status = getBuffer(size, ptr, is_null); + status = getBuffer(size, ptr, is_null); if (status != FlatBufferManager::Status::Success) { #ifndef __CUDACC__ throw std::runtime_error("LineString.getItem failed: " + ::toString(status)); @@ -1018,7 +1155,7 @@ struct LineString { int32_t this_input_srid = get_input_srid(flatbuffer_); int32_t this_output_srid = get_output_srid(flatbuffer_); return Geo::get_point(ptr, - index, + 2 * index, this_input_srid, (output_srid < 0 ? this_output_srid : output_srid), is_geoint); @@ -1032,14 +1169,39 @@ struct LineString { #ifndef __CUDACC__ std::vector toCoords() const { + FlatBufferManager m{flatbuffer_}; + FlatBufferManager::Status status = FlatBufferManager::Status::NotImplementedError; + if (m.isNestedArray()) { + const SQLTypeInfoLite* ti = + reinterpret_cast(m.get_user_data_buffer()); + if (ti == nullptr) { + status = FlatBufferManager::UserDataError; + } else { + if (m.getDimensions() == 3) { + if (index_[2] >= 0) { + bool is_null = false; + int32_t nof_points; + int8_t* coords_ptr; + FlatBufferManager::Status status = m.getItem( + index_[2], index_[1], index_[0], nof_points, coords_ptr, is_null); + if (status == FlatBufferManager::Status::Success) { + return decompress_coords( + coords_ptr, nof_points * m.getValueSize(), ti->is_geoint()); + } + } + } + } + if (status != FlatBufferManager::Status::Success) { + throw std::runtime_error("LineString.toCoords2 failed: " + ::toString(status)); + } + } int8_t* ptr; int64_t size; bool is_null; - auto status = getBuffer(size, ptr, is_null); + status = getBuffer(size, ptr, is_null); - std::vector result; if (status != FlatBufferManager::Status::Success) { - throw std::runtime_error("LineString.getBuffer failed: " + ::toString(status)); + throw std::runtime_error("LineString.toCoords failed: " + ::toString(status)); } if (is_null) { return {}; @@ -1051,19 +1213,49 @@ struct LineString { #endif // Return the number of points of the line string - int64_t size() const { + size_t size() const { FlatBufferManager m{flatbuffer_}; + FlatBufferManager::Status status = FlatBufferManager::Status::NotImplementedError; + if (m.isNestedArray()) { + size_t length = 0; + if (m.getDimensions() == 3) { + if (index_[2] >= 0) { + status = m.getLength(index_[2], index_[1], index_[0], length); + } else if (index_[1] >= 0) { + status = m.getLength(index_[1], index_[0], length); + } else { + status = m.getLength(index_[0], length); + } + } else { + status = FlatBufferManager::Status::NotImplementedError; + } + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("Geo::LineString size failed: " + ::toString(status)); +#endif + } + return length; + } + // to be deprecated in favor of NestedArray: int64_t length = 0; switch (m.format()) { case GeoLineStringFormatId: - m.getItemLength(index_[0], length); + status = m.getItemLength(index_[0], length); + break; + case GeoPolygonFormatId: + status = m.getSubItemLength(index_[1], index_[0], length); break; default: #ifndef __CUDACC__ - throw std::runtime_error("LineString::size: not implemented for format " + - ::toString(m.format())); + throw std::runtime_error("Geo::LineString::size: not implemented for format " + + ::toString(static_cast(m.format()))); #else ; +#endif + } + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("Geo::LineString size failed: " + ::toString(status)); #endif } return length; @@ -1169,9 +1361,21 @@ struct Polygon { int8_t* flatbuffer_; // FlatBuffer of GeoPolygons int64_t index_[2]; // polygon index in a Column/Column +#ifndef __CUDACC__ + std::vector> toCoords() const { + std::vector> result; + auto sz = size(); + result.reserve(sz); + for (size_t i = 0; i < sz; i++) { + result.push_back(getItem(i).toCoords()); + } + return result; + } +#endif + // Return the index-th linestring - Geo::LineString getItem(const int64_t index) const { - Geo::LineString linestring{flatbuffer_, {index_[0], index, -1}}; + Geo::LineString getItem(const int64_t index, const int32_t output_srid = 0) const { + Geo::LineString linestring{flatbuffer_, {index, index_[0], index_[1]}}; return linestring; } @@ -1244,19 +1448,71 @@ struct Polygon { } // Return the number of line strings in a polygon - int64_t size() const { + size_t size() const { FlatBufferManager m{flatbuffer_}; - int64_t length = 0; - m.getItemLength(index_[0], length); - return length; + FlatBufferManager::Status status; + if (m.isNestedArray()) { + size_t length = 0; + if (m.getDimensions() == 3) { + if (index_[1] >= 0) { + status = m.getLength(index_[1], index_[0], length); + } else { + status = m.getLength(index_[0], length); + } + } else { + status = FlatBufferManager::Status::NotImplementedError; + } + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("Geo::Polygon size failed: " + ::toString(status)); +#endif + } + return length; + } else { + int64_t length = 0; + status = m.getItemLength(index_[0], length); + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("Geo::Polygon size failed: " + ::toString(status)); +#endif + } + return length; + } } // Return the number of points in the index-th line string - int64_t size(const int64_t index) const { + size_t size(const int64_t index) const { FlatBufferManager m{flatbuffer_}; - int64_t length = 0; - m.getSubItemLength(index_[0], index, length); - return length; + FlatBufferManager::Status status; + if (m.isNestedArray()) { + size_t length = 0; + if (m.getDimensions() == 3) { + if (index_[1] >= 0) { + status = m.getLength(index_[1], index_[0], index, length); + } else { + status = m.getLength(index_[0], index, length); + } + } else { + status = FlatBufferManager::Status::NotImplementedError; + } + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("Geo::Polygon size(lineindex) failed: " + + ::toString(status)); +#endif + } + return length; + } else { + int64_t length = 0; + status = m.getSubItemLength(index_[0], index, length); + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("Geo::Polygon size(lineindex) failed: " + + ::toString(status)); +#endif + } + return length; + } } #ifndef __CUDACC__ @@ -1318,6 +1574,87 @@ struct Polygon { #endif }; +struct MultiPolygon { + int8_t* flatbuffer_; // FlatBuffer of GeoMultiPolygons + int64_t index_[1]; // multipolygon index of Column + + // Return the number of polygons in the multipolgon + size_t size() const { + FlatBufferManager m{flatbuffer_}; + FlatBufferManager::Status status; + size_t length; + // return the length of a multipolygon + status = m.getLength(index_[0], length); + if (status != FlatBufferManager::Status::Success) { +#ifndef __CUDACC__ + throw std::runtime_error("Geo::MultiPolygon size failed: " + ::toString(status)); +#endif + } + return length; + } + + // Return the index-th polygon + Geo::Polygon getItem(const int64_t index, const int32_t output_srid = 0) const { + Geo::Polygon polygon{flatbuffer_, {index, index_[0]}}; + return polygon; + } + + DEVICE inline Geo::Polygon operator[](const unsigned int index) const { + /* Use getItem(index, output_srid) to enable user-specified + transformation. */ + return getItem(index); + } + +#ifndef __CUDACC__ + // Construct a multipolygon from a vector of vector of coordinates vector + template + FlatBufferManager::Status fromCoords( + const std::vector>>& coords) { + FlatBufferManager m{flatbuffer_}; + const SQLTypeInfoLite* ti = + reinterpret_cast(m.get_user_data_buffer()); + if (ti == nullptr) { + return FlatBufferManager::UserDataError; + } + if (ti->compression == SQLTypeInfoLite::GEOINT) { + if constexpr (std::is_same::value) { + const auto ccoords = compress_coords(coords); + return m.setItem(index_[0], ccoords); + } else { + return m.setItem(index_[0], coords); + } + } else if (ti->compression == SQLTypeInfoLite::NONE) { + if constexpr (std::is_same::value) { + return m.setItem(index_[0], coords); + } else { + const auto dcoords = decompress_coords(coords); + return m.setItem(index_[0], dcoords); + } + } else { + UNREACHABLE(); + } + return FlatBufferManager::NotImplementedError; + } + + std::vector>> toCoords() const { + std::vector>> result; + auto sz = size(); + result.reserve(sz); + for (size_t i = 0; i < sz; i++) { + result.push_back(getItem(i).toCoords()); + } + return result; + } +#endif + +#ifdef HAVE_TOSTRING + std::string toString() const { + return ::typeName(this) + "(..., {" + std::to_string(index_[0]) + ", " + + std::to_string(index_[1]) + "})"; + } +#endif +}; + } // namespace Geo template <> @@ -1567,6 +1904,64 @@ struct Column { #endif } + DEVICE inline void setItem(int64_t index, const Geo::Polygon& polygon) { + FlatBufferManager this_m{flatbuffer_}; + FlatBufferManager m{polygon.flatbuffer_}; + FlatBufferManager::Status status; + if (polygon.index_[1] == -1) { + // flatbuffer contains Column + int32_t* ring_sizes; + int8_t* points; + int64_t nof_rings, size; + bool is_null; + status = m.getItemCountsAndData( + polygon.index_[0], ring_sizes, nof_rings, points, size, is_null); + if (status != FlatBufferManager::Status::Success) { + goto fail; + } + if (is_null) { + status = this_m.setNull(index); + } else { + status = this_m.setItemCountsAndData(index, ring_sizes, nof_rings, points); + } + if (status != FlatBufferManager::Status::Success) { + goto fail; + } + } else { + // flatbuffer contains Column + int32_t nof_points; + int32_t nof_rings; + int8_t* points; + int8_t* ring_sizes; + bool is_null; + status = m.getItem(polygon.index_[1], + polygon.index_[0], + nof_points, + points, + nof_rings, + ring_sizes, + is_null); + if (status != FlatBufferManager::Status::Success) { + goto fail; + } + if (is_null) { + status = this_m.setNull(index); + } else { + status = this_m.setItemCountsAndData( + index, reinterpret_cast(ring_sizes), nof_rings, points); + } + if (status != FlatBufferManager::Status::Success) { + goto fail; + } + } + return; + fail: +#ifndef __CUDACC__ + throw std::runtime_error("setItem failed: " + ::toString(status)); +#endif + ; + } + // Return the total number of points in a Column inline int64_t getNofValues() const { FlatBufferManager m{flatbuffer_}; @@ -1582,6 +1977,60 @@ struct Column { #endif }; +template <> +struct Column { + int8_t* flatbuffer_; + int64_t num_rows_; + + DEVICE Geo::MultiPolygon getItem(const int64_t index) const { + Geo::MultiPolygon mpolygon{flatbuffer_, {index}}; + return mpolygon; + } + + DEVICE inline Geo::MultiPolygon operator[](const unsigned int index) const { + return getItem(static_cast(index)); + } + + DEVICE int64_t size() const { return num_rows_; } + + DEVICE inline bool isNull(int64_t index) const { + FlatBufferManager m{flatbuffer_}; + bool is_null = false; + auto status = m.isNull(index, is_null); +#ifndef __CUDACC__ + if (status != FlatBufferManager::Status::Success) { + throw std::runtime_error("isNull failed: " + ::toString(status)); + } +#endif + return is_null; + } + + DEVICE inline void setNull(int64_t index) { + FlatBufferManager m{flatbuffer_}; + auto status = m.setNull(index); +#ifndef __CUDACC__ + if (status != FlatBufferManager::Status::Success) { + throw std::runtime_error("setNull(" + std::to_string(index) + + ") failed: " + ::toString(status)); + } +#endif + } + + // Return the total number of points in a Column + inline int64_t getNofValues() const { + FlatBufferManager m{flatbuffer_}; + return m.getValuesCount(); + } + +#ifdef HAVE_FLATBUFFER_TOSTRING + std::string toString() const { + FlatBufferManager m{flatbuffer_}; + return ::typeName(this) + "(" + m.toString() + + ", num_rows=" + std::to_string(num_rows_) + ")"; + } +#endif +}; + template struct Column> { // A type for a column of variable length arrays diff --git a/Shared/sqltypes.h b/Shared/sqltypes.h index 1e508843e8..ae71b638a9 100644 --- a/Shared/sqltypes.h +++ b/Shared/sqltypes.h @@ -29,6 +29,7 @@ #include "../Logger/Logger.h" #include "Datum.h" #include "funcannotations.h" +#include "sqltypes_lite.h" #include #include @@ -1015,12 +1016,119 @@ class SQLTypeInfo { case kPOINT: case kLINESTRING: case kPOLYGON: + case kMULTIPOLYGON: return true; default:; } return false; } + SQLTypeInfoLite toLite() const { + SQLTypeInfoLite ti_lite; + switch (type) { + case kPOINT: + ti_lite.type = SQLTypeInfoLite::POINT; + ti_lite.subtype = SQLTypeInfoLite::UNSPECIFIED; + break; + case kLINESTRING: + ti_lite.type = SQLTypeInfoLite::LINESTRING; + ti_lite.subtype = SQLTypeInfoLite::UNSPECIFIED; + break; + case kPOLYGON: + ti_lite.type = SQLTypeInfoLite::POLYGON; + ti_lite.subtype = SQLTypeInfoLite::UNSPECIFIED; + break; + case kMULTIPOINT: + ti_lite.type = SQLTypeInfoLite::MULTIPOINT; + ti_lite.subtype = SQLTypeInfoLite::UNSPECIFIED; + break; + case kMULTILINESTRING: + ti_lite.type = SQLTypeInfoLite::MULTILINESTRING; + ti_lite.subtype = SQLTypeInfoLite::UNSPECIFIED; + break; + case kMULTIPOLYGON: + ti_lite.type = SQLTypeInfoLite::MULTIPOLYGON; + ti_lite.subtype = SQLTypeInfoLite::UNSPECIFIED; + break; + case kTEXT: + ti_lite.type = SQLTypeInfoLite::TEXT; + ti_lite.subtype = SQLTypeInfoLite::UNSPECIFIED; + break; + case kARRAY: + ti_lite.type = SQLTypeInfoLite::ARRAY; + switch (subtype) { + case kBOOLEAN: + ti_lite.subtype = SQLTypeInfoLite::BOOLEAN; + break; + case kTINYINT: + ti_lite.subtype = SQLTypeInfoLite::TINYINT; + break; + case kSMALLINT: + ti_lite.subtype = SQLTypeInfoLite::SMALLINT; + break; + case kINT: + ti_lite.subtype = SQLTypeInfoLite::INT; + break; + case kBIGINT: + ti_lite.subtype = SQLTypeInfoLite::BIGINT; + break; + case kFLOAT: + ti_lite.subtype = SQLTypeInfoLite::FLOAT; + break; + case kDOUBLE: + ti_lite.subtype = SQLTypeInfoLite::DOUBLE; + break; + case kTEXT: + ti_lite.subtype = SQLTypeInfoLite::TEXT; + break; + default: + UNREACHABLE(); + } + break; + default: + UNREACHABLE(); + } + if (is_geometry()) { + switch (get_compression()) { + case kENCODING_NONE: + ti_lite.compression = SQLTypeInfoLite::NONE; + break; + case kENCODING_GEOINT: + ti_lite.compression = SQLTypeInfoLite::GEOINT; + break; + default: + UNREACHABLE(); + } + ti_lite.dimension = get_input_srid(); + ti_lite.scale = get_output_srid(); + ti_lite.db_id = 0; // unused + ti_lite.dict_id = 0; // unused + } else if (type == kTEXT) { + switch (get_compression()) { + case kENCODING_NONE: + ti_lite.compression = SQLTypeInfoLite::NONE; + break; + case kENCODING_DICT: + ti_lite.compression = SQLTypeInfoLite::DICT; + break; + default: + UNREACHABLE(); + } + ti_lite.dimension = 0; // unused + ti_lite.scale = 0; // unused + ti_lite.db_id = dict_key_.db_id; + ti_lite.dict_id = dict_key_.dict_id; + } else if (type == kARRAY) { + ti_lite.dimension = 0; // unused + ti_lite.scale = 0; // unused + ti_lite.db_id = dict_key_.db_id; + ti_lite.dict_id = dict_key_.dict_id; + } else { + UNREACHABLE(); + } + return ti_lite; + } + private: SQLTypes type; // type id SQLTypes subtype; // element type of arrays or columns @@ -1491,6 +1599,73 @@ DEVICE inline void VarlenArray_get_nth(int8_t* buf, inline int64_t getFlatBufferSize(int64_t items_count, int64_t max_nof_values, const SQLTypeInfo& ti) { + size_t dimensions = 0; + FlatBufferManager::ValueType value_type; + int64_t max_nof_sizes = 0; + switch (ti.get_type()) { + case kPOINT: + dimensions = 0; + break; + case kLINESTRING: + case kMULTIPOINT: + case kARRAY: + dimensions = 1; + max_nof_sizes = items_count; + break; + case kPOLYGON: + case kMULTILINESTRING: + dimensions = 2; + max_nof_sizes = items_count + max_nof_values / 3; + break; + case kMULTIPOLYGON: + dimensions = 3; + max_nof_sizes = items_count + 2 * (max_nof_values / 3 + 1); + break; + default: + UNREACHABLE(); + } + + if (ti.is_geometry()) { + if (ti.get_compression() == kENCODING_GEOINT) { + value_type = FlatBufferManager::PointInt32; + } else { + value_type = FlatBufferManager::PointFloat64; + } + } else if (ti.is_array()) { + switch (ti.get_subtype()) { + case kBOOLEAN: + value_type = FlatBufferManager::Bool8; + break; + case kTINYINT: + value_type = FlatBufferManager::Int8; + break; + case kSMALLINT: + value_type = FlatBufferManager::Int16; + break; + case kINT: + value_type = FlatBufferManager::Int32; + break; + case kTEXT: + CHECK_EQ(ti.get_compression(), kENCODING_DICT); + value_type = FlatBufferManager::Int32; + break; + case kBIGINT: + value_type = FlatBufferManager::Int64; + break; + case kFLOAT: + value_type = FlatBufferManager::Float32; + break; + case kDOUBLE: + value_type = FlatBufferManager::Float64; + break; + default: + UNREACHABLE(); + break; + } + } else { + UNREACHABLE(); + } + switch (ti.get_type()) { case kPOINT: { FlatBufferManager::GeoPoint metadata{items_count, @@ -1519,6 +1694,17 @@ inline int64_t getFlatBufferSize(int64_t items_count, return FlatBufferManager::compute_flatbuffer_size( GeoPolygonFormatId, reinterpret_cast(&metadata)); } + case kMULTIPOINT: + case kMULTILINESTRING: + case kMULTIPOLYGON: { + return FlatBufferManager::compute_flatbuffer_size( + /* dimensions= */ dimensions, + /* total_items_count= */ items_count, + /* total sizes count= */ max_nof_sizes, + /* total values count= */ max_nof_values, + value_type, + /* user data size= */ sizeof(SQLTypeInfoLite)); + } case kARRAY: { const size_t array_item_size = ti.get_elem_type().get_size(); const auto dict_key = ti.getStringDictKey(); @@ -1539,6 +1725,75 @@ inline void initializeFlatBuffer(FlatBufferManager& m, int64_t items_count, int64_t max_nof_values, const SQLTypeInfo& ti) { + size_t dimensions = 0; + FlatBufferManager::ValueType value_type; + int64_t max_nof_sizes = 0; + switch (ti.get_type()) { + case kPOINT: + dimensions = 0; + break; + case kLINESTRING: + case kMULTIPOINT: + case kARRAY: + dimensions = 1; + max_nof_sizes = items_count; + break; + case kPOLYGON: + case kMULTILINESTRING: + dimensions = 2; + max_nof_sizes = items_count + max_nof_values / 3; + break; + case kMULTIPOLYGON: + dimensions = 3; + max_nof_sizes = items_count + 2 * (max_nof_values / 3 + 1); + break; + default: + UNREACHABLE(); + } + + if (ti.is_geometry()) { + if (ti.get_compression() == kENCODING_GEOINT) { + value_type = FlatBufferManager::PointInt32; + } else { + value_type = FlatBufferManager::PointFloat64; + } + } else if (ti.is_array()) { + switch (ti.get_subtype()) { + case kBOOLEAN: + value_type = FlatBufferManager::Bool8; + break; + case kTINYINT: + value_type = FlatBufferManager::Int8; + break; + case kSMALLINT: + value_type = FlatBufferManager::Int16; + break; + case kINT: + value_type = FlatBufferManager::Int32; + break; + case kTEXT: + CHECK_EQ(ti.get_compression(), kENCODING_DICT); + value_type = FlatBufferManager::Int32; + break; + case kBIGINT: + value_type = FlatBufferManager::Int64; + break; + case kFLOAT: + value_type = FlatBufferManager::Float32; + break; + case kDOUBLE: + value_type = FlatBufferManager::Float64; + break; + default: + UNREACHABLE(); + break; + } + } else { + UNREACHABLE(); + } + + SQLTypeInfoLite ti_lite = ti.toLite(); + switch (ti.get_type()) { case kPOINT: { FlatBufferManager::GeoPoint metadata{items_count, @@ -1567,6 +1822,31 @@ inline void initializeFlatBuffer(FlatBufferManager& m, m.initialize(GeoPolygonFormatId, reinterpret_cast(&metadata)); break; } + case kMULTIPOINT: + case kMULTILINESTRING: + case kMULTIPOLYGON: { + int8_t* null_value_ptr = nullptr; + uint32_t geoint_null_value[2] = {0x80000000U, 0x80000000U}; + double null_point[2] = {2 * DBL_MIN, 2 * DBL_MIN}; + if (ti.get_compression() == kENCODING_GEOINT) { + null_value_ptr = reinterpret_cast(geoint_null_value); + } else { + null_value_ptr = reinterpret_cast(null_point); + } + auto status = + m.initialize(NestedArrayFormatId, + /* dimensions= */ dimensions, + /* total_items_count= */ items_count, + /* total_sizes_count= */ max_nof_sizes, + /* total_values_count= */ max_nof_values, + value_type, + /* null value buffer=*/null_value_ptr, // null value buffer size + // is defined by value type + /* user data buffer=*/reinterpret_cast(&ti_lite), + /* user data buffer size=*/sizeof(SQLTypeInfoLite)); + CHECK_EQ(status, FlatBufferManager::Success); + break; + } case kARRAY: { const size_t array_item_size = ti.get_elem_type().get_size(); const auto dict_key = ti.getStringDictKey(); diff --git a/Shared/sqltypes_lite.h b/Shared/sqltypes_lite.h new file mode 100644 index 0000000000..529351361b --- /dev/null +++ b/Shared/sqltypes_lite.h @@ -0,0 +1,66 @@ +/* + * Copyright 2023 HEAVY.AI, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + Provides a light-weight data structure SQLTypeInfoLite to serialize + SQLTypeInfo (from sqltypes.h) for the extension functions (in + heavydbTypes.h) by FlatBufferManager. + + Extend SQLTypeInfoLite struct as needed but keep it simple so that + both sqltypes.h and heavydbTypes.h are able to include it (recall, + the two header files cannot include each other). +*/ + +#pragma once + +#include + +struct SQLTypeInfoLite { + enum SQLTypes { + UNSPECIFIED = 0, + BOOLEAN, + TINYINT, + SMALLINT, + INT, + BIGINT, + FLOAT, + DOUBLE, + POINT, + LINESTRING, + POLYGON, + MULTIPOINT, + MULTILINESTRING, + MULTIPOLYGON, + TEXT, + ARRAY + }; + enum EncodingType { + NONE = 0, + DICT, // used by TEXT and ARRAY of TEXT + GEOINT // used by geotypes + }; + SQLTypes type; + SQLTypes subtype; // used by ARRAY + EncodingType compression; // used by geotypes and TEXT and ARRAY of TEXT + int32_t dimension; // input_srid + int32_t scale; // output_srid + int32_t db_id; // used by TEXT and ARRAY of TEXT + int32_t dict_id; // used by TEXT and ARRAY of TEXT + + inline bool is_geoint() const { return compression == GEOINT; } + inline int32_t get_input_srid() const { return dimension; } + inline int32_t get_output_srid() const { return scale; } +}; diff --git a/Tests/TableFunctionsTest.cpp b/Tests/TableFunctionsTest.cpp index ec66f41edb..4bac02dc0f 100644 --- a/Tests/TableFunctionsTest.cpp +++ b/Tests/TableFunctionsTest.cpp @@ -391,28 +391,38 @@ class TableFunctions : public ::testing::Test { "hh3 GEOMETRY(LINESTRING, 4326) ENCODING NONE, " "p4 GEOMETRY(POLYGON, 900913)," "r4 GEOMETRY(LINESTRING, 900913), h4 GEOMETRY(LINESTRING, 900913), hh4 " - "GEOMETRY(LINESTRING, 900913), sizes INT);"); + "GEOMETRY(LINESTRING, 900913), " + "mp1 MULTIPOLYGON, " + "mp2 GEOMETRY(MULTIPOLYGON, 4326), " + "mp3 GEOMETRY(MULTIPOLYGON, 4326) ENCODING NONE, " + "mp4 GEOMETRY(MULTIPOLYGON, 900913), " + "sizes INT);"); TestHelpers::ValuesGenerator gen("geo_polygon_test"); - run_multiple_agg(gen("'POLYGON((1 2,3 4,5 6,7 8,9 10),(2 3,3 4,1 2))'", - "'LINESTRING(1 2,3 4,5 6,7 8,9 10)'", - "'LINESTRING(2 3,3 4,1 2)'", - "'NULL'", - "'POLYGON((0 0,5 0,5 5,0 5,0 0),(2 2, 2 1,1 1,1 2,2 2))'", - "'LINESTRING(0 0,5 0,5 5,0 5)'", - "'LINESTRING(2 2,2 1,1 1,1 2)'", - "'NULL'", - "'POLYGON((0 0,6 0,6 6,0 6,0 0),(3 3,3 2,2 2,2 3,3 3))'", - "'LINESTRING(0 0,6 0,6 6,0 6))'", - "'LINESTRING(3 3,3 2,2 2,2 3)'", - "'NULL'", - "'POLYGON((0 0,7 0,7 7,0 7,0 0),(4 4,2 4, 2 3,4 2,4 4))'", - "'LINESTRING(0 0,7 0,7 7,0 7)'", - "'LINESTRING(4 4,4 2,2 3,2 4)'", - "'NULL'", - "8"), - ExecutorDeviceType::CPU); + run_multiple_agg( + gen("'POLYGON((1 2,3 4,5 6,7 8,9 10),(2 3,3 4,1 2))'", + "'LINESTRING(1 2,3 4,5 6,7 8,9 10)'", + "'LINESTRING(2 3,3 4,1 2)'", + "'NULL'", + "'POLYGON((0 0,5 0,5 5,0 5,0 0),(2 2, 2 1,1 1,1 2,2 2))'", + "'LINESTRING(0 0,5 0,5 5,0 5)'", + "'LINESTRING(2 2,2 1,1 1,1 2)'", + "'NULL'", + "'POLYGON((0 0,6 0,6 6,0 6,0 0),(3 3,3 2,2 2,2 3,3 3))'", + "'LINESTRING(0 0,6 0,6 6,0 6))'", + "'LINESTRING(3 3,3 2,2 2,2 3)'", + "'NULL'", + "'POLYGON((0 0,7 0,7 7,0 7,0 0),(4 4,2 4, 2 3,4 2,4 4))'", + "'LINESTRING(0 0,7 0,7 7,0 7)'", + "'LINESTRING(4 4,4 2,2 3,2 4)'", + "'NULL'", + "'MULTIPOLYGON(((1 2,3 4,5 6,7 8,9 10),(2 3,3 4,1 2)))'", + "'MULTIPOLYGON(((0 0,5 0,5 5,0 5,0 0),(2 2, 2 1,1 1,1 2,2 2)))'", + "'MULTIPOLYGON(((0 0,6 0,6 6,0 6,0 0),(3 3,3 2,2 2,2 3,3 3)))'", + "'MULTIPOLYGON(((0 0,7 0,7 7,0 7,0 0),(4 4,2 4, 2 3,4 2,4 4)))'", + "8"), + ExecutorDeviceType::CPU); run_multiple_agg(gen("'POLYGON((0 0,5 0,5 5,0 5,0 0))'", "'LINESTRING(0 0,5 0,5 5,0 5)'", @@ -430,6 +440,10 @@ class TableFunctions : public ::testing::Test { "'LINESTRING(0 0,4 0,4 4,0 4)'", "'NULL'", "'NULL'", + "'MULTIPOLYGON(((0 0,5 0,5 5,0 5,0 0)))'", + "'MULTIPOLYGON(((0 0,6 0,6 6,0 6,0 0)))'", + "'MULTIPOLYGON(((0 0,7 0,7 7,0 7,0 0)))'", + "'MULTIPOLYGON(((0 0,4 0,4 4,0 4,0 0)))'", "4"), ExecutorDeviceType::CPU); @@ -450,6 +464,13 @@ class TableFunctions : public ::testing::Test { "'LINESTRING(0 0,7 0,7 7,0 7)'", "'LINESTRING(4 4,4 2,2 3,2 4)'", "'LINESTRING(0 0,0 1,1 0)'", + "'MULTIPOLYGON(((1 2,3 4,5 6,7 8,9 10),(3 4,1 2,2 3),(5 6,7 8,9 10)))'", + "'MULTIPOLYGON(((0 0,5 0,5 5,0 5,0 0),(2 2,2 1,1 1,1 2,2 2),(0 0,0 1,1 " + "0)))'", + "'MULTIPOLYGON(((0 0,6 0,6 6,0 6,0 0),(3 3,3 2,2 2,2 3,3 3),(0 0,0 1,1 " + "0)))'", + "'MULTIPOLYGON(((0 0,7 0,7 7,0 7,0 0),(4 4,2 4, 2 3,4 2,4 4),(0 0,0 1,1 " + "0)))'", "11"), ExecutorDeviceType::CPU); run_multiple_agg(gen("'NULL'", @@ -468,6 +489,10 @@ class TableFunctions : public ::testing::Test { "'NULL'", "'NULL'", "'NULL'", + "'NULL'", + "'NULL'", + "'NULL'", + "'NULL'", "NULL"), ExecutorDeviceType::CPU); } @@ -2923,6 +2948,7 @@ void assert_equal(const TargetValue& val1, std::vector coords1, coords2; std::vector bounds1, bounds2; std::vector ring_sizes1, ring_sizes2; + std::vector poly_sizes1, poly_sizes2; switch (ti.get_type()) { case kLINESTRING: { const auto gdal_wkt_ls1 = Geospatial::GeoLineString(*s1); @@ -2940,9 +2966,20 @@ void assert_equal(const TargetValue& val1, gdal_wkt_poly2.getColumns(coords2, ring_sizes2, bounds2); break; } + case kMULTIPOLYGON: { + const auto gdal_wkt_mpoly1 = Geospatial::GeoMultiPolygon(*s1); + gdal_wkt_mpoly1.getColumns(coords1, ring_sizes1, poly_sizes1, bounds1); + const auto gdal_wkt_mpoly2 = Geospatial::GeoMultiPolygon(*s2); + gdal_wkt_mpoly2.getColumns(coords2, ring_sizes2, poly_sizes2, bounds2); + break; + } default: UNREACHABLE() << "ti=" << ti.to_string(); } + ASSERT_EQ(poly_sizes1.size(), poly_sizes2.size()); + for (size_t i = 0; i < poly_sizes1.size(); i++) { + ASSERT_EQ(poly_sizes1[i], poly_sizes2[i]); + } ASSERT_EQ(ring_sizes1.size(), ring_sizes2.size()); ASSERT_EQ(coords1.size(), coords2.size()); int64_t k = 0; @@ -2963,8 +3000,8 @@ void assert_equal(const TargetValue& val1, for (int32_t j = 0; j < sz; j++) { Point p1 = points1[j]; Point p2 = points2[j]; - ASSERT_NEAR(p1.x, p2.x, 1e-7); - ASSERT_NEAR(p1.y, p2.y, 1e-7); + ASSERT_NEAR(p1.x, p2.x, 1e-6); + ASSERT_NEAR(p1.y, p2.y, 1e-6); } } } @@ -3604,6 +3641,103 @@ TEST_F(TableFunctions, ColumnGeoPolygonOutput) { } } +TEST_F(TableFunctions, ColumnGeoPolygonInOutput) { + for (auto dt : {ExecutorDeviceType::CPU /*, ExecutorDeviceType::GPU*/}) { + SKIP_NO_GPU(); + for (int i = 1; i <= 4; i++) { + std::string pcol = "p" + std::to_string(i); + std::string rcol = "r" + std::to_string(i); + std::string hcol = "h" + std::to_string(i); + std::string hhcol = "hh" + std::to_string(i); + { + std::string q1 = "SELECT " + rcol + " FROM geo_polygon_test;"; + std::string q2 = + "SELECT linestrings FROM TABLE(CT_LINESTRINGN(CURSOR(" + "SELECT polygons FROM TABLE(CT_MAKE_POLYGON3(CURSOR(SELECT " + + rcol + ", " + hcol + ", " + hhcol + " FROM geo_polygon_test)))), 1));"; + const auto expected_rows = run_multiple_agg(q1, dt); + const auto rows = run_multiple_agg(q2, dt); + assert_equal(rows, expected_rows); + } + } + } +} + +TEST_F(TableFunctions, ColumnGeoMultiPolygonOutput) { + for (auto dt : {ExecutorDeviceType::CPU /*, ExecutorDeviceType::GPU*/}) { + SKIP_NO_GPU(); + for (int i = 1; i <= 4; i++) { + std::string pcol = "p" + std::to_string(i); + std::string mpcol = "mp" + std::to_string(i); + // Test Column output + { + std::string q1 = "SELECT " + mpcol + " FROM geo_polygon_test;"; + std::string q2 = + "SELECT mpolygons FROM TABLE(CT_MAKE_MULTIPOLYGON(CURSOR(SELECT " + pcol + + " FROM geo_polygon_test)));"; + const auto expected_rows = run_multiple_agg(q1, dt); + const auto rows = run_multiple_agg(q2, dt); + assert_equal(rows, expected_rows); + } + } + } +} + +TEST_F(TableFunctions, ColumnGeoMultiPolygonInput) { + for (auto dt : {ExecutorDeviceType::CPU /*, ExecutorDeviceType::GPU*/}) { + SKIP_NO_GPU(); + for (int i = 1; i <= 4; i++) { + std::string pcol = "p" + std::to_string(i); + std::string mpcol = "mp" + std::to_string(i); + // Test Column input + { + std::string q1 = "SELECT " + pcol + " FROM geo_polygon_test;"; + std::string q2 = "SELECT polygons FROM TABLE(CT_POLYGONN(CURSOR(SELECT " + mpcol + + " FROM geo_polygon_test), 1));"; + const auto expected_rows = run_multiple_agg(q1, dt); + const auto rows = run_multiple_agg(q2, dt); + assert_equal(rows, expected_rows); + } + } + } +} + +TEST_F(TableFunctions, ColumnGeoMultiPolygonInOutput) { + for (auto dt : {ExecutorDeviceType::CPU /*, ExecutorDeviceType::GPU*/}) { + SKIP_NO_GPU(); + for (int i = 1; i <= 4; i++) { + std::string pcol = "p" + std::to_string(i); + std::string mpcol = "mp" + std::to_string(i); + // Test Column input, Column + // output, Column input, Column + // output + { + std::string q1 = "SELECT " + pcol + " FROM geo_polygon_test;"; + std::string q2 = + "SELECT polygons FROM TABLE(CT_POLYGONN(CURSOR(SELECT" + " mpolygons FROM TABLE(CT_MAKE_MULTIPOLYGON(CURSOR(SELECT " + + pcol + " FROM geo_polygon_test)))), 1));"; + const auto expected_rows = run_multiple_agg(q1, dt); + const auto rows = run_multiple_agg(q2, dt); + assert_equal(rows, expected_rows); + } + // Test Column input, Column + // output, Column input, Column + // output: + { + std::string q1 = "SELECT " + mpcol + " FROM geo_polygon_test;"; + std::string q2 = + "SELECT mpolygons FROM TABLE(CT_MAKE_MULTIPOLYGON(" + "CURSOR(SELECT polygons FROM TABLE(CT_POLYGONN(CURSOR(SELECT " + + mpcol + " FROM geo_polygon_test), 1)))));"; + const auto expected_rows = run_multiple_agg(q1, dt); + const auto rows = run_multiple_agg(q2, dt); + assert_equal(rows, expected_rows); + } + } + } +} + TEST_F(TableFunctions, DefaultScalarValues) { for (auto dt : {ExecutorDeviceType::CPU, ExecutorDeviceType::GPU}) { SKIP_NO_GPU(); From 281c46e2f9dbcd7ffd8463a61c43a568c23484b0 Mon Sep 17 00:00:00 2001 From: yoonminnam <53632385+yoonminnam@users.noreply.github.com> Date: Fri, 13 Jan 2023 09:25:06 -0800 Subject: [PATCH 2/7] Server crash when processing sharded join query if at least one shard is an empty (#7146) * Do not throw an exception for sharded join on multi-gpus * Check hash table buffer to exit early if it is not a valid Signed-off-by: jack --- QueryEngine/GroupByRuntime.cpp | 4 +- .../JoinHashTable/PerfectJoinHashTable.cpp | 9 ----- Tests/ExecuteTest.cpp | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/QueryEngine/GroupByRuntime.cpp b/QueryEngine/GroupByRuntime.cpp index c79e1d4fb3..33a0645415 100644 --- a/QueryEngine/GroupByRuntime.cpp +++ b/QueryEngine/GroupByRuntime.cpp @@ -272,7 +272,7 @@ bucketized_hash_join_idx(int64_t hash_buff, int64_t const max_key, const int64_t translated_null_val, int64_t bucket_normalization) { - if (key >= min_key && key <= max_key) { + if (hash_buff && key >= min_key && key <= max_key) { return *SUFFIX(get_bucketized_hash_slot)(reinterpret_cast(hash_buff), key, min_key / bucket_normalization, @@ -354,7 +354,7 @@ hash_join_idx_sharded(int64_t hash_buff, const uint32_t entry_count_per_shard, const uint32_t num_shards, const uint32_t device_count) { - if (key >= min_key && key <= max_key) { + if (hash_buff && key >= min_key && key <= max_key) { return *SUFFIX(get_hash_slot_sharded)(reinterpret_cast(hash_buff), key, min_key, diff --git a/QueryEngine/JoinHashTable/PerfectJoinHashTable.cpp b/QueryEngine/JoinHashTable/PerfectJoinHashTable.cpp index e2cb46d983..07abd36b2d 100644 --- a/QueryEngine/JoinHashTable/PerfectJoinHashTable.cpp +++ b/QueryEngine/JoinHashTable/PerfectJoinHashTable.cpp @@ -243,15 +243,6 @@ std::shared_ptr PerfectJoinHashTable::getInstance( throw TooManyHashEntries(oss.str()); } - auto const shard_count = get_shard_count(qual_bin_oper.get(), executor); - if (device_count > 1 && shard_count > 1) { - // use baseline hash join to compute this case until resolving related hash join logic - // todd(yoonmin): relax this after fixing related hashtable build/probe logic is fixed - throw TooManyHashEntries( - "Use baseline hash join: multiple GPUs process the input sharded table via " - "perfect hash can cause a wrong result"); - } - if (qual_bin_oper->get_optype() == kBW_EQ && col_range.getIntMax() >= std::numeric_limits::max()) { throw HashJoinFail("Cannot translate null value for kBW_EQ"); diff --git a/Tests/ExecuteTest.cpp b/Tests/ExecuteTest.cpp index 7069bcb69c..d7e6f722fc 100644 --- a/Tests/ExecuteTest.cpp +++ b/Tests/ExecuteTest.cpp @@ -12820,6 +12820,45 @@ TEST_F(Select, Joins_Negative_ShardKey) { } } +TEST_F(Select, Joins_One_Shard_Is_Empty) { + SKIP_ALL_ON_AGGREGATOR(); + if (skip_tests(ExecutorDeviceType::GPU)) { + return; + } + size_t num_shards = choose_shard_count(); + if (num_shards == 1) { + return; + } + auto drop_tbls = [] { + run_ddl_statement("DROP TABLE IF EXISTS SJ1;"); + run_ddl_statement("DROP TABLE IF EXISTS SJ2;"); + }; + drop_tbls(); + for (auto tbl_name : {"SJ1", "SJ2"}) { + std::ostringstream oss; + oss << "CREATE TABLE " << tbl_name + << " (v INT, SHARD KEY(v)) WITH (SHARD_COUNT=" << num_shards << ");"; + run_ddl_statement(oss.str()); + } + for (auto v : {2, 10}) { + std::ostringstream oss; + oss << "INSERT INTO SJ1 VALUES(" << v << ");"; + run_multiple_agg(oss.str(), ExecutorDeviceType::CPU); + } + for (auto v : {30, 2, 2, 4, 1, 3}) { + std::ostringstream oss; + oss << "INSERT INTO SJ2 VALUES(" << v << ");"; + run_multiple_agg(oss.str(), ExecutorDeviceType::CPU); + } + ScopeGuard reset = [drop_tbls] { drop_tbls(); }; + for (auto dt : {ExecutorDeviceType::CPU, ExecutorDeviceType::GPU}) { + SKIP_NO_GPU(); + EXPECT_EQ(v(run_simple_agg( + "SELECT COUNT(1) FROM SJ1 R, SJ2 S WHERE R.v = S.v;", dt)), + int64_t(2)); + } +} + TEST_F(Select, Joins_InnerJoin_AtLeastThreeTables) { for (auto dt : {ExecutorDeviceType::CPU, ExecutorDeviceType::GPU}) { SKIP_NO_GPU(); From 39dd9097354d242d2d6568b8547900734e71a182 Mon Sep 17 00:00:00 2001 From: Andrew Do <34977845+AndrewVDo@users.noreply.github.com> Date: Fri, 13 Jan 2023 16:27:15 -0800 Subject: [PATCH 3/7] Fix get_rows() whitespace/quote parsing errors (#7140) * Delimited Files: fix issue where whitespace between quoted fields was not being properly parsed * When throwing InsufficientBufferSizeException, the parser will print an alternative message if the parser is still in quotes Signed-off-by: jack --- .../ForeignStorage/CsvFileBufferParser.cpp | 6 +- ImportExport/DelimitedParserUtils.cpp | 46 +++++++++---- ImportExport/DelimitedParserUtils.h | 5 ++ ImportExport/Importer.cpp | 2 +- Tests/Import/datafiles/geospatial.csv | 20 +++--- .../random_strings_with_line_endings.7z | Bin 45292 -> 45271 bytes .../with_quoted_fields_and_side_spaces.csv | 5 ++ ...ith_quoted_fields_unmatched_left_quote.csv | 4 ++ ...th_quoted_fields_unmatched_right_quote.csv | 4 ++ Tests/ImportExportTest.cpp | 61 +++++++++++++++++- 10 files changed, 121 insertions(+), 32 deletions(-) create mode 100644 Tests/Import/datafiles/with_quoted_fields_and_side_spaces.csv create mode 100644 Tests/Import/datafiles/with_quoted_fields_unmatched_left_quote.csv create mode 100644 Tests/Import/datafiles/with_quoted_fields_unmatched_right_quote.csv diff --git a/DataMgr/ForeignStorage/CsvFileBufferParser.cpp b/DataMgr/ForeignStorage/CsvFileBufferParser.cpp index 0937a4661f..58312016ee 100644 --- a/DataMgr/ForeignStorage/CsvFileBufferParser.cpp +++ b/DataMgr/ForeignStorage/CsvFileBufferParser.cpp @@ -168,6 +168,8 @@ ParseBufferResult CsvFileBufferParser::parseBuffer(ParseBufferRequest& request, std::vector> tmp_buffers; // holds string w/ removed escape chars, etc const char* line_start = p; + row_index_plus_one++; + bool incorrect_column_count = false; p = import_export::delimited_parser::get_row(p, thread_buf_end, buf_end, @@ -177,10 +179,6 @@ ParseBufferResult CsvFileBufferParser::parseBuffer(ParseBufferRequest& request, tmp_buffers, try_single_thread, !columns_are_pre_filtered); - - row_index_plus_one++; - - bool incorrect_column_count = false; try { validate_expected_column_count(row, num_cols, point_cols, file_path); } catch (const ForeignStorageException& e) { diff --git a/ImportExport/DelimitedParserUtils.cpp b/ImportExport/DelimitedParserUtils.cpp index 0231c12768..20cea36fd1 100644 --- a/ImportExport/DelimitedParserUtils.cpp +++ b/ImportExport/DelimitedParserUtils.cpp @@ -46,13 +46,18 @@ inline void trim_space(const char*& field_begin, const char*& field_end) { inline void trim_quotes(const char*& field_begin, const char*& field_end, const import_export::CopyParams& copy_params) { - if (copy_params.quoted && field_end - field_begin > 0 && - *field_begin == copy_params.quote) { - ++field_begin; + auto quote_begin = field_begin, quote_end = field_end; + if (copy_params.quoted) { + trim_space(quote_begin, quote_end); } - if (copy_params.quoted && field_end - field_begin > 0 && - *(field_end - 1) == copy_params.quote) { - --field_end; + if (copy_params.quoted && quote_end - quote_begin > 0) { + if (*quote_begin == copy_params.quote && *(quote_end - 1) == copy_params.quote) { + field_begin = ++quote_begin; + field_end = (quote_begin == quote_end) ? quote_end : --quote_end; + } else { + throw import_export::delimited_parser::DelimitedParserException( + "Unable to trim quotes."); + } } } } // namespace @@ -123,13 +128,26 @@ size_t find_end(const char* buffer, if (last_line_delim_pos <= 0) { size_t excerpt_length = std::min(50, size); std::string buffer_excerpt{buffer, buffer + excerpt_length}; - std::string error_message = - "Unable to find an end of line character after reading " + std::to_string(size) + - " characters. Please ensure that the correct \"line_delimiter\" option is " - "specified or update the \"buffer_size\" option appropriately. Row number: " + - std::to_string(buffer_first_row_index + 1) + - ". First few characters in row: " + buffer_excerpt; - throw InsufficientBufferSizeException{error_message}; + if (in_quote) { + std::string quote(1, copy_params.quote); + std::string error_message = + "Unable to find a matching end quote for the quote character '" + quote + + "' after reading " + std::to_string(size) + + " characters. Please ensure that all data fields are correctly formatted " + "or update the \"buffer_size\" option appropriately. Row number: " + + std::to_string(buffer_first_row_index + 1) + + ". First few characters in row: " + buffer_excerpt; + throw InsufficientBufferSizeException{error_message}; + } else { + std::string error_message = + "Unable to find an end of line character after reading " + + std::to_string(size) + + " characters. Please ensure that the correct \"line_delimiter\" option is " + "specified or update the \"buffer_size\" option appropriately. Row number: " + + std::to_string(buffer_first_row_index + 1) + + ". First few characters in row: " + buffer_excerpt; + throw InsufficientBufferSizeException{error_message}; + } } return last_line_delim_pos + 1; @@ -244,10 +262,10 @@ const char* get_row(const char* buf, } const char* field_begin = field_buf; const char* field_end = field_buf + j; + trim_quotes(field_begin, field_end, copy_params); if (copy_params.trim_spaces) { trim_space(field_begin, field_end); } - trim_quotes(field_begin, field_end, copy_params); row.emplace_back(field_begin, field_end - field_begin); } field = p + 1; diff --git a/ImportExport/DelimitedParserUtils.h b/ImportExport/DelimitedParserUtils.h index 032633d102..5aad86425f 100644 --- a/ImportExport/DelimitedParserUtils.h +++ b/ImportExport/DelimitedParserUtils.h @@ -37,6 +37,11 @@ class InsufficientBufferSizeException : public std::runtime_error { : std::runtime_error(message) {} }; +class DelimitedParserException : public std::runtime_error { + public: + DelimitedParserException(const std::string& message) : std::runtime_error(message) {} +}; + /** * @brief Finds the closest possible row beginning in the given buffer. * diff --git a/ImportExport/Importer.cpp b/ImportExport/Importer.cpp index 79f66ca0fa..f7029d1713 100644 --- a/ImportExport/Importer.cpp +++ b/ImportExport/Importer.cpp @@ -2020,6 +2020,7 @@ static ImportStatus import_thread_delimited( row.clear(); std::vector> tmp_buffers; // holds string w/ removed escape chars, etc + row_index_plus_one++; if (DEBUG_TIMING) { us = measure::execution([&]() { p = import_export::delimited_parser::get_row(p, @@ -2044,7 +2045,6 @@ static ImportStatus import_thread_delimited( try_single_thread, true); } - row_index_plus_one++; // Each POINT could consume two separate coords instead of a single WKT if (row.size() < num_cols || (num_cols + point_cols) < row.size()) { thread_import_status.rows_rejected++; diff --git a/Tests/Import/datafiles/geospatial.csv b/Tests/Import/datafiles/geospatial.csv index d965daa959..251427d752 100644 --- a/Tests/Import/datafiles/geospatial.csv +++ b/Tests/Import/datafiles/geospatial.csv @@ -1,11 +1,11 @@ "point", "linestring", "polygon", "multipolygon", "point2", "point3", "point4", "trip_distance" -"POINT(0 0)", "LINESTRING(0 0, 0 0)", "POLYGON((0 0, 1 0, 0 1, 0 0))", "MULTIPOLYGON(((0 0, 1 0, 0 1, 0 0)))", "POINT(0 0)", "POINT(0 0)", "POINT(0 0)", "0.0" -"POINT(1 1)", "LINESTRING(1 0, 2 2, 3 3)", "POLYGON((0 0, 2 0, 0 2, 0 0))", "MULTIPOLYGON(((0 0, 2 0, 0 2, 0 0)))", "POINT(1 1)", "POINT(1 1)", "POINT(1 1)", "1.0" -"POINT(2 2)", "LINESTRING(2 0, 4 4)", "POLYGON((0 0, 3 0, 0 3, 0 0))", "MULTIPOLYGON(((0 0, 3 0, 0 3, 0 0)))", "POINT(2 2)", "POINT(2 2)", "POINT(2 2)", "2.0" -"POINT(3 3)", "LINESTRING(3 0, 6 6, 7 7)", "POLYGON((0 0, 4 0, 0 4, 0 0))", "MULTIPOLYGON(((0 0, 4 0, 0 4, 0 0)))", "POINT(3 3)", "POINT(3 3)", "POINT(3 3)", "3.0" -"POINT(4 4)", "LINESTRING(4 0, 8 8)", "POLYGON((0 0, 5 0, 0 5, 0 0))", "MULTIPOLYGON(((0 0, 5 0, 0 5, 0 0)))", "POINT(4 4)", "POINT(4 4)", "POINT(4 4)", "4.0" -"POINT(5 5)", "LINESTRING(5 0, 10 10, 11 11)", "POLYGON((0 0, 6 0, 0 6, 0 0))", "MULTIPOLYGON(((0 0, 6 0, 0 6, 0 0)))", "POINT(5 5)", "POINT(5 5)", "POINT(5 5)", "5.0" -"POINT(6 6)", "LINESTRING(6 0, 12 12)", "POLYGON((0 0,7 0, 0 7, 0 0))", "MULTIPOLYGON(((0 0, 7 0, 0 7, 0 0)))", "POINT(6 6)", "POINT(6 6)", "POINT(6 6)", "6.0" -"POINT(7 7)", "LINESTRING(7 0, 14 14, 15 15)", "POLYGON((0 0, 8 0, 0 8, 0 0))", "MULTIPOLYGON(((0 0, 8 0, 0 8, 0 0)))", "POINT(7 7)", "POINT(7 7)", "POINT(7 7)", "7.0" -"POINT(8 8)", "LINESTRING(8 0, 16 16)", "POLYGON((0 0,9 0, 0 9, 0 0))", "MULTIPOLYGON(((0 0, 9 0, 0 9, 0 0)))", "POINT(8 8)", "POINT(8 8)", "POINT(8 8)", "8.0" -"POINT(9 9)", "LINESTRING(9 0, 18 18, 19 19)", "POLYGON((0 0, 10 0, 0 10, 0 0))", "MULTIPOLYGON(((0 0, 10 0, 0 10, 0 0)))", "POINT(9 9)", "POINT(9 9)", "POINT(9 9)", "9.0" +"POINT(0 0)", "LINESTRING(0 0, 0 0)", "POLYGON((0 0, 1 0, 0 1, 0 0))", "MULTIPOLYGON(((0 0, 1 0, 0 1, 0 0)))", "POINT(0 0)", "POINT(0 0)", "POINT(0 0)", 0.0 +"POINT(1 1)", "LINESTRING(1 0, 2 2, 3 3)", "POLYGON((0 0, 2 0, 0 2, 0 0))", "MULTIPOLYGON(((0 0, 2 0, 0 2, 0 0)))", "POINT(1 1)", "POINT(1 1)", "POINT(1 1)", 1.0 +"POINT(2 2)", "LINESTRING(2 0, 4 4)", "POLYGON((0 0, 3 0, 0 3, 0 0))", "MULTIPOLYGON(((0 0, 3 0, 0 3, 0 0)))", "POINT(2 2)", "POINT(2 2)", "POINT(2 2)", 2.0 +"POINT(3 3)", "LINESTRING(3 0, 6 6, 7 7)", "POLYGON((0 0, 4 0, 0 4, 0 0))", "MULTIPOLYGON(((0 0, 4 0, 0 4, 0 0)))", "POINT(3 3)", "POINT(3 3)", "POINT(3 3)", 3.0 +"POINT(4 4)", "LINESTRING(4 0, 8 8)", "POLYGON((0 0, 5 0, 0 5, 0 0))", "MULTIPOLYGON(((0 0, 5 0, 0 5, 0 0)))", "POINT(4 4)", "POINT(4 4)", "POINT(4 4)", 4.0 +"POINT(5 5)", "LINESTRING(5 0, 10 10, 11 11)", "POLYGON((0 0, 6 0, 0 6, 0 0))", "MULTIPOLYGON(((0 0, 6 0, 0 6, 0 0)))", "POINT(5 5)", "POINT(5 5)", "POINT(5 5)", 5.0 +"POINT(6 6)", "LINESTRING(6 0, 12 12)", "POLYGON((0 0,7 0, 0 7, 0 0))", "MULTIPOLYGON(((0 0, 7 0, 0 7, 0 0)))", "POINT(6 6)", "POINT(6 6)", "POINT(6 6)", 6.0 +"POINT(7 7)", "LINESTRING(7 0, 14 14, 15 15)", "POLYGON((0 0, 8 0, 0 8, 0 0))", "MULTIPOLYGON(((0 0, 8 0, 0 8, 0 0)))", "POINT(7 7)", "POINT(7 7)", "POINT(7 7)", 7.0 +"POINT(8 8)", "LINESTRING(8 0, 16 16)", "POLYGON((0 0,9 0, 0 9, 0 0))", "MULTIPOLYGON(((0 0, 9 0, 0 9, 0 0)))", "POINT(8 8)", "POINT(8 8)", "POINT(8 8)", 8.0 +"POINT(9 9)", "LINESTRING(9 0, 18 18, 19 19)", "POLYGON((0 0, 10 0, 0 10, 0 0))", "MULTIPOLYGON(((0 0, 10 0, 0 10, 0 0)))", "POINT(9 9)", "POINT(9 9)", "POINT(9 9)", 9.0 diff --git a/Tests/Import/datafiles/random_strings_with_line_endings.7z b/Tests/Import/datafiles/random_strings_with_line_endings.7z index b2a6db7fbcb18d4e37ee3125441cd32fa7ac9976..f307ab349986e13e118fef9e65aabc9fd54aaddf 100644 GIT binary patch literal 45271 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 literal 45292 zcmV(sK<&Rbdc3bE8~_8Q`zYinumAu60001_000000002iF-bE3cdpao5CLfMz8~() zW!1YVZyh%;`#|f}1=Mp#m?g4pzhh`TWcC;=sA_0l#LB58ucM+OuH$~S*NkxT5ks1| zFpCeS8q9y5&oSmB+|pVR?3Y=98-hXFT8@qH|uU>hf3vo)vCA??ybV=wfdSAS(3$ss9N-M zXU#u73kOf{|dyF*wU{qr1t5sOF=H+t7Y#=6%4_xXFPcTyerIs`tu?@DHa#JCCs5>#KS{>)F5_4CK{UCcl+AcSz{>nXhagkwk zA>Z)^t0=}b0IT3YWo11LKtUv*iwja@*#*~Jf$Jtnn^}49AyAu%52REIue&^vH0iUT zg4{Nvr*GOlXE2?A+m~`#3ib1I&NG$u+UIp;s40iNT09hH1p6VZQv0T2YWwS)IgCyS ztUA=+Z{eSb+rt2=w3~U!= z9=)_8BUqo+fK8cp0!rdeSaWVfY!z(d_qv{1Hd|Y%|I!fMhTi9+V&vEugTYA%1c>tM zRhd3=TA_X#=b4BC9Z@OYow1e9gME}TVv5_kd+ONvL+u3}xrsjXOhUZyPtFX;0U1?W ze;w5$PmI0S9NQ1f3AiRZ>iF&Yu2y|mHi-BN-`00j5j{lQd$d9M6C8G+cc|t4$H^JG zevAtW{gTex2H#K<1jv~|)1I>57J?_h5&IG;TrWDnbM&#L_2Bi^Jn`%0A4|q;?)AR4 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%0AHcX?UFFYSHN!}2|Wtmn4Yc1-j=9q@GF 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 Date: Sat, 14 Jan 2023 10:06:43 -0500 Subject: [PATCH 4/7] Don't compile unsupported compute capability (#7147) Signed-off-by: jack --- CMakeLists.txt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index be3cdd794e..4cb0749ce7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,8 +247,12 @@ if(ENABLE_CUDA) else() if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.18.0") message(STATUS "CMake 3.18+, Setting CUDA_ARCHITECTURES.") - set(CMAKE_CUDA_ARCHITECTURES - 35-virtual + if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 12.0.0) + set(CMAKE_CUDA_ARCHITECTURES 35-virtual) + else() + set(CMAKE_CUDA_ARCHITECTURES "") + endif() + list(APPEND CMAKE_CUDA_ARCHITECTURES 50-virtual 60-virtual 70-virtual @@ -258,8 +262,12 @@ if(ENABLE_CUDA) message(STATUS "CUDA_ARCHITECTURES: ${CMAKE_CUDA_ARCHITECTURES}") else() message(STATUS "CMake 3.17 or under, setting CUDA architecture flags manually.") - set(CUDA_COMPILATION_ARCH - -gencode=arch=compute_35,code=compute_35; + if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 12.0.0) + set(CUDA_COMPILATION_ARCH -gencode=arch=compute_35,code=compute_35;) + else() + set(CUDA_COMPILATION_ARCH "") + endif() + list(APPEND CUDA_COMPILATION_ARCH -gencode=arch=compute_50,code=compute_50; -gencode=arch=compute_60,code=compute_60; -gencode=arch=compute_70,code=compute_70; From 1ef76fd5ad374fde1d32317268e8ef627872f3c2 Mon Sep 17 00:00:00 2001 From: Simon Eves Date: Mon, 16 Jan 2023 10:08:29 -0800 Subject: [PATCH 5/7] Remove Render Groups (Step 7 of 8) (#7144) * Drop Render Group columns when restoring an older table dump * Add tests for restoring old dumps with render group columns (good and corrupt) * Dumps are now versioned * Check version on restore and only remove render group columns if version requires Co-authored-by: Paul Aiyedun Signed-off-by: jack --- TableArchiver/TableArchiver.cpp | 196 +++++++++++++++--- Tests/DumpRestoreTest.cpp | 26 +++ .../TableDump/dump_with_render_groups_bad.gz | Bin 0 -> 3575 bytes .../TableDump/dump_with_render_groups_good.gz | Bin 0 -> 3566 bytes .../TableDump/truncate_test_dump_file.py | 58 ++++++ 5 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 Tests/Export/TableDump/dump_with_render_groups_bad.gz create mode 100644 Tests/Export/TableDump/dump_with_render_groups_good.gz create mode 100644 Tests/Export/TableDump/truncate_test_dump_file.py diff --git a/TableArchiver/TableArchiver.cpp b/TableArchiver/TableArchiver.cpp index 14afc7533d..3180099ace 100644 --- a/TableArchiver/TableArchiver.cpp +++ b/TableArchiver/TableArchiver.cpp @@ -54,9 +54,13 @@ extern bool g_cluster; extern std::string g_base_path; bool g_test_rollback_dump_restore{false}; +constexpr static int kDumpVersion = 1; +constexpr static int kDumpVersion_remove_render_group_columns = 1; + constexpr static char const* table_schema_filename = "_table.sql"; constexpr static char const* table_oldinfo_filename = "_table.oldinfo"; constexpr static char const* table_epoch_filename = "_table.epoch"; +constexpr static char const* table_dumpversion_filename = "_table.dumpversion"; #if BOOST_VERSION < 107300 namespace std { @@ -79,7 +83,9 @@ inline std::string abs_path(const File_Namespace::GlobalFileMgr* global_file_mgr return boost::filesystem::canonical(global_file_mgr->getBasePath()).string(); } -inline std::string run(const std::string& cmd, const std::string& chdir = "") { +inline std::string run(const std::string& cmd, + const std::string& chdir = "", + const bool log_failure = true) { VLOG(3) << "running cmd: " << cmd; int rcode; std::error_code ec; @@ -99,11 +105,13 @@ inline std::string run(const std::string& cmd, const std::string& chdir = "") { errors = ss_errors.str(); }); if (rcode || ec) { - LOG(ERROR) << "failed cmd: " << cmd; - LOG(ERROR) << "exit code: " << rcode; - LOG(ERROR) << "error code: " << ec.value() << " - " << ec.message(); - LOG(ERROR) << "stdout: " << output; - LOG(ERROR) << "stderr: " << errors; + if (log_failure) { + LOG(ERROR) << "failed cmd: " << cmd; + LOG(ERROR) << "exit code: " << rcode; + LOG(ERROR) << "error code: " << ec.value() << " - " << ec.message(); + LOG(ERROR) << "stdout: " << output; + LOG(ERROR) << "stderr: " << errors; + } #if defined(__APPLE__) // osx bsdtar options "--use-compress-program" and "--fast-read" together // run into pipe write error after tar extracts the first occurrence of a @@ -150,7 +158,8 @@ inline std::string run(const std::string& cmd, const std::string& chdir = "") { inline std::string simple_file_cat(const std::string& archive_path, const std::string& file_name, - const std::string& compression) { + const std::string& compression, + const bool log_failure = true) { ddl_utils::validate_allowed_file_path(archive_path, ddl_utils::DataTransferType::IMPORT); #if defined(__APPLE__) @@ -163,7 +172,8 @@ inline std::string simple_file_cat(const std::string& archive_path, boost::filesystem::create_directories(temp_dir); run("tar " + compression + " -xvf " + get_quoted_string(archive_path) + " " + opt_occurrence + " " + file_name, - temp_dir.string()); + temp_dir.string(), + log_failure); const auto output = run("cat " + (temp_dir / file_name).string()); boost::filesystem::remove_all(temp_dir); return output; @@ -180,10 +190,11 @@ inline std::string get_table_schema(const std::string& archive_path, // If a table was altered there may be a mapping from old column ids to new ones these // values need to be replaced in the page headers. -void rewrite_column_ids_in_page_headers( +void update_or_drop_column_ids_in_page_headers( const boost::filesystem::path& path, const std::unordered_map& column_ids_map, - const int32_t table_epoch) { + const int32_t table_epoch, + const bool drop_not_update) { const std::string file_path = path.string(); const std::string file_name = path.filename().string(); std::vector tokens; @@ -230,11 +241,31 @@ void rewrite_column_ids_in_page_headers( continue; } auto column_map_it = column_ids_map.find(col_id); - CHECK(column_map_it != column_ids_map.end()) << "could not find " << col_id; - // If a header contains a column id that is remapped to new location - // then write that change to the file. - if (const auto dest_col_id = column_map_it->second; col_id != dest_col_id) { - col_id = dest_col_id; + bool rewrite_header = false; + if (drop_not_update) { + // if the header contains a column ID that is a key of the map + // erase the entire header so that column is effectively dropped + // the value of the map is ignored, thus allowing us to use the + // same function for both operations + if (column_map_it != column_ids_map.end()) { + // clear the entire header + std::memset(header_info, 0, sizeof(header_info)); + rewrite_header = true; + } + } else { + if (column_map_it == column_ids_map.end()) { + throw std::runtime_error("Page " + std::to_string(page) + " in " + file_path + + " has unexpected Column ID " + std::to_string(col_id) + + ". Dump may be corrupt."); + } + // If a header contains a column id that is remapped to new location + // then write that change to the file. + if (const auto dest_col_id = column_map_it->second; col_id != dest_col_id) { + col_id = dest_col_id; + rewrite_header = true; + } + } + if (rewrite_header) { if (0 != std::fseek(fp.get(), page * page_size, SEEK_SET)) { throw std::runtime_error("Failed to seek to page# " + std::to_string(page) + file_path + " for write: " + std::strerror(errno)); @@ -248,12 +279,15 @@ void rewrite_column_ids_in_page_headers( } } -// Adjust column ids in chunk keys in a table's data files under a temp_data_dir, +// Rewrite column ids in chunk keys in a table's data files under a temp_data_dir, // including files of all shards of the table. Can be slow for big files but should // be scale faster than refragmentizing. Table altering should be rare for olap. -void adjust_altered_table_files(const int32_t table_epoch, - const std::string& temp_data_dir, - const std::unordered_map& column_ids_map) { +// Also used to erase page headers for columns that must be dropped completely. +void update_or_drop_column_ids_in_table_files( + const int32_t table_epoch, + const std::string& temp_data_dir, + const std::unordered_map& column_ids_map, + const bool drop_not_update) { boost::filesystem::path base_path(temp_data_dir); boost::filesystem::recursive_directory_iterator end_it; ThreadController_NS::SimpleThreadController<> thread_controller(cpu_threads()); @@ -261,8 +295,11 @@ void adjust_altered_table_files(const int32_t table_epoch, ++fit) { if (!boost::filesystem::is_symlink(fit->path()) && boost::filesystem::is_regular_file(fit->status())) { - thread_controller.startThread( - rewrite_column_ids_in_page_headers, fit->path(), column_ids_map, table_epoch); + thread_controller.startThread(update_or_drop_column_ids_in_page_headers, + fit->path(), + column_ids_map, + table_epoch, + drop_not_update); thread_controller.checkThreadsStatus(); } } @@ -330,6 +367,73 @@ void rename_table_directories(const File_Namespace::GlobalFileMgr* global_file_m } } +std::unordered_map find_render_group_columns( + const std::list& src_columns, + std::vector& src_oldinfo_strs, + const std::string& archive_path) { + // scan for poly or mpoly columns and collect their names + std::vector poly_column_names; + for (auto const& src_column : src_columns) { + auto const sqltype = src_column.columnType.get_type(); + if (sqltype == kPOLYGON || sqltype == kMULTIPOLYGON) { + poly_column_names.push_back(src_column.columnName); + } + } + + // remove any matching render group columns from the source list + // and capture their IDs in the keys of a map (value is ignored) + std::unordered_map column_ids_to_drop; + auto last_itr = std::remove_if( + src_oldinfo_strs.begin(), + src_oldinfo_strs.end(), + [&](const std::string& v) -> bool { + // tokenize + std::vector tokens; + boost::algorithm::split( + tokens, v, boost::is_any_of(":"), boost::token_compress_on); + // extract name and ID + if (tokens.size() < 2) { + throw std::runtime_error( + "Dump " + archive_path + + " has invalid oldinfo file contents. Dump may be corrupt."); + } + auto const& column_name = tokens[0]; + auto const column_id = std::stoi(tokens[1]); + for (auto const& poly_column_name : poly_column_names) { + // is it a render group column? + auto const render_group_column_name = poly_column_name + "_render_group"; + if (column_name == render_group_column_name) { + LOG(INFO) << "RESTORE TABLE dropping render group column '" + << render_group_column_name << "' from dump " << archive_path; + // add to "set" + column_ids_to_drop[column_id] = -1; + return true; + } + } + return false; + }); + src_oldinfo_strs.erase(last_itr, src_oldinfo_strs.end()); + + return column_ids_to_drop; +} + +void drop_render_group_columns( + const std::unordered_map& render_group_column_ids, + const std::string& archive_path, + const std::string& temp_data_dir, + const std::string& compression) { + // rewrite page files to drop the columns with IDs that are the keys of the map + if (render_group_column_ids.size()) { + const auto epoch = boost::lexical_cast( + simple_file_cat(archive_path, table_epoch_filename, compression)); + const auto time_ms = measure<>::execution([&]() { + update_or_drop_column_ids_in_table_files( + epoch, temp_data_dir, render_group_column_ids, true /* drop */); + }); + VLOG(3) << "drop render group columns: " << time_ms << " ms"; + } +} + } // namespace void TableArchiver::dumpTable(const TableDescriptor* td, @@ -387,6 +491,10 @@ void TableArchiver::dumpTable(const TableDescriptor* td, const auto table_name = td->tableName; { + // - gen dumpversion file + const auto dumpversion_str = std::to_string(kDumpVersion); + file_writer( + uuid_dir / table_dumpversion_filename, "table dumpversion", dumpversion_str); // - gen schema file const auto schema_str = cat_->dumpSchema(td); file_writer(uuid_dir / table_schema_filename, "table schema", schema_str); @@ -512,6 +620,36 @@ void TableArchiver::restoreTable(const Catalog_Namespace::SessionInfo& session, all_src_oldinfo_str, boost::is_any_of(" "), boost::token_compress_on); + + // fetch dump version + int dump_version = -1; + try { + // attempt to read file, do not log if fail to read + auto const dump_version_str = + simple_file_cat(archive_path, table_dumpversion_filename, compression, false); + dump_version = std::stoi(dump_version_str); + } catch (std::runtime_error& e) { + // no dump version file found + dump_version = 0; + } + LOG(INFO) << "Dump Version: " << dump_version; + + // version-specific behavior + const bool do_drop_render_group_columns = + (dump_version < kDumpVersion_remove_render_group_columns); + + // remove any render group columns from the source columns so that the list of + // source columns matches the already-created table, and the removed ones will + // not have an entry in column_ids_map, and hence will not have their data + // mapped later (effectively dropping them), and return their IDs for when + // they are actually dropped later + std::unordered_map render_group_column_ids; + if (do_drop_render_group_columns) { + render_group_column_ids = + find_render_group_columns(src_columns, src_oldinfo_strs, archive_path); + } + + // compare with the destination columns auto all_dst_columns = cat_->getAllColumnMetadataForTable(td->tableId, true, true, true); if (src_oldinfo_strs.size() != all_dst_columns.size()) { @@ -559,20 +697,30 @@ void TableArchiver::restoreTable(const Catalog_Namespace::SessionInfo& session, was_table_altered = was_table_altered || it.first != it.second; }); VLOG(3) << "was_table_altered = " << was_table_altered; + // extract all data files to a temp dir. will swap with dst table dir after all set, // otherwise will corrupt table in case any bad thing happens in the middle. run("rm -rf " + temp_data_dir.string()); run("mkdir -p " + temp_data_dir.string()); run("tar " + compression + " -xvf " + get_quoted_string(archive_path), temp_data_dir); + // drop the render group columns here + if (do_drop_render_group_columns) { + drop_render_group_columns( + render_group_column_ids, archive_path, temp_data_dir, compression); + } + // if table was ever altered after it was created, update column ids in chunk headers. if (was_table_altered) { const auto epoch = boost::lexical_cast( simple_file_cat(archive_path, table_epoch_filename, compression)); - const auto time_ms = measure<>::execution( - [&]() { adjust_altered_table_files(epoch, temp_data_dir, column_ids_map); }); - VLOG(3) << "adjust_altered_table_files: " << time_ms << " ms"; + const auto time_ms = measure<>::execution([&]() { + update_or_drop_column_ids_in_table_files( + epoch, temp_data_dir, column_ids_map, false /* update */); + }); + VLOG(3) << "update_column_ids_table_files: " << time_ms << " ms"; } + // finally,,, swap table data/dict dirs! const auto data_file_dirs = cat_->getTableDataDirectories(td); const auto dict_file_dirs = cat_->getTableDictDirectories(td); diff --git a/Tests/DumpRestoreTest.cpp b/Tests/DumpRestoreTest.cpp index 2d58baf924..ff793a893b 100644 --- a/Tests/DumpRestoreTest.cpp +++ b/Tests/DumpRestoreTest.cpp @@ -250,6 +250,7 @@ class DumpAndRestoreTest : public ::testing::Test { boost::filesystem::remove_all(tar_ball_path); run_ddl_statement("DROP TABLE IF EXISTS test_table;"); run_ddl_statement("DROP TABLE IF EXISTS test_table_2;"); + run_ddl_statement("DROP TABLE IF EXISTS render_groups;"); g_test_rollback_dump_restore = false; } @@ -257,6 +258,7 @@ class DumpAndRestoreTest : public ::testing::Test { boost::filesystem::remove_all(tar_ball_path); run_ddl_statement("DROP TABLE IF EXISTS test_table;"); run_ddl_statement("DROP TABLE IF EXISTS test_table_2;"); + run_ddl_statement("DROP TABLE IF EXISTS render_groups;"); } void sqlAndCompareResult(const std::string& sql, @@ -581,6 +583,30 @@ TEST_F(DumpAndRestoreTest, DumpAlteredTable) { sqlAndCompareResult("SELECT * FROM test_table;", std::vector{1}); } +TEST_F(DumpAndRestoreTest, DropRenderGroupColumns) { + static constexpr int kNullInt = std::numeric_limits::min(); + auto file_path = boost::filesystem::canonical( + "../../Tests/Export/TableDump/dump_with_render_groups_good.gz") + .string(); + EXPECT_NO_THROW(run_ddl_statement("RESTORE TABLE render_groups FROM '" + file_path + + "' WITH (compression='gzip');")); + sqlAndCompareResult("SELECT id FROM render_groups;", + std::vector{1, 2, 3, 4, 5}); + sqlAndCompareResult("SELECT ST_NPOINTS(poly) FROM render_groups;", + std::vector{4, kNullInt, 3, 3, 0}); + sqlAndCompareResult("SELECT ST_NPOINTS(multipoly) FROM render_groups;", + std::vector{6, kNullInt, 9, 9, 0}); +} + +TEST_F(DumpAndRestoreTest, DropRenderGroupColumnsBad) { + auto file_path = boost::filesystem::canonical( + "../../Tests/Export/TableDump/dump_with_render_groups_bad.gz") + .string(); + EXPECT_THROW(run_ddl_statement("RESTORE TABLE render_groups FROM '" + file_path + + "' WITH (compression='gzip');"), + std::runtime_error); +} + #ifdef HAVE_AWS_S3 class S3RestoreTest : public DumpAndRestoreTest { protected: diff --git a/Tests/Export/TableDump/dump_with_render_groups_bad.gz b/Tests/Export/TableDump/dump_with_render_groups_bad.gz new file mode 100644 index 0000000000000000000000000000000000000000..b5d74b34b753ca6051b572ae93438bca2acb4f80 GIT binary patch literal 3575 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 literal 0 HcmV?d00001 diff --git a/Tests/Export/TableDump/dump_with_render_groups_good.gz b/Tests/Export/TableDump/dump_with_render_groups_good.gz new file mode 100644 index 0000000000000000000000000000000000000000..33b4147a1f4af21a2fc5dfa27e00ce981d2dbb5e GIT binary patch literal 3566 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 B=WqZ3 literal 0 HcmV?d00001 diff --git a/Tests/Export/TableDump/truncate_test_dump_file.py b/Tests/Export/TableDump/truncate_test_dump_file.py new file mode 100644 index 0000000000..1ffbac8c0e --- /dev/null +++ b/Tests/Export/TableDump/truncate_test_dump_file.py @@ -0,0 +1,58 @@ +import os +import re +import shutil +import struct +import tarfile + +from argparse import ArgumentParser + +output_file_suffix = "_truncated" +def truncate_dump_file(file_path: str): + extract_path = "./dump_file_extract" + with tarfile.open(file_path, "r") as old_dump_tar: + old_dump_tar.extractall(path=extract_path) + + for root, _, files in os.walk(extract_path): + for file in files: + match = re.match("\d+\\.(\d+)\\.data", file) + if match: + truncate_data_file(os.path.join(root, file), int(match.group(1))) + + file_name = os.path.basename(file_path) + with tarfile.open(f"{file_name}{output_file_suffix}", "w:gz") as new_dump_tar: + new_dump_tar.add(extract_path, arcname="") + + shutil.rmtree(extract_path) + +def truncate_data_file(file_path: str, page_size: int): + print(f"Truncating data file: {file_path}") + + file_size = os.path.getsize(file_path) + last_empty_header_offset = None + with open(file_path, "r+b") as file: + file_offet = 0 + while file_offet < file_size: + file.seek(file_offet) + header_size = struct.unpack('i', file.read(4))[0] + + # Capture the first free page that is followed by only free pages. + if header_size == 0 and last_empty_header_offset is None: + last_empty_header_offset = file_offet + elif header_size != 0 and last_empty_header_offset is not None: + last_empty_header_offset = None + file_offet += page_size + + if last_empty_header_offset is not None: + # Keep at least one free page in order to ensure that code branches that check for free pages are executed. + new_file_size = last_empty_header_offset + page_size + if new_file_size < file_size: + file.truncate(new_file_size) + +if __name__ == '__main__': + parser = ArgumentParser(description=f"""Truncate the dump file at the specified path for tests. Truncation involves + removal of excess free pages that do not add to test coverage. Output + dump file is created in the current directory with a file name that matches + the input file name along with a {output_file_suffix} suffix.""") + parser.add_argument("file_path", type=str, help="Path to file file") + args = parser.parse_args() + truncate_dump_file(file_path=args.file_path) From 1afa387654258792218d0b6a21afbe499446c57a Mon Sep 17 00:00:00 2001 From: Shawn Yarbrough <47542640+corecursion@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:36:58 -0800 Subject: [PATCH 6/7] fix four mislabelled command line options (#7150) Signed-off-by: jack --- ThriftHandler/CommandLineOptions.cpp | 1241 +++++++++++++------------- ThriftHandler/CommandLineOptions.h | 8 +- 2 files changed, 613 insertions(+), 636 deletions(-) diff --git a/ThriftHandler/CommandLineOptions.cpp b/ThriftHandler/CommandLineOptions.cpp index 5d06d450f5..67acfac33f 100644 --- a/ThriftHandler/CommandLineOptions.cpp +++ b/ThriftHandler/CommandLineOptions.cpp @@ -18,12 +18,12 @@ #include #include -#include #include #include using namespace std::string_literals; +#include #include "CommandLineOptions.h" #include "ImportExport/ForeignDataImporter.h" #include "LeafHostInfo.h" @@ -93,27 +93,28 @@ void CommandLineOptions::init_logging() { log_options_.set_base_path(base_path); logger::init(log_options_); } - void CommandLineOptions::fillOptions() { - help_desc.add_options()("help,h", "Show available options."); - help_desc.add_options()( + po::options_description& desc = help_desc_; + + desc.add_options()("help,h", "Show available options."); + desc.add_options()( "allow-cpu-retry", po::value(&g_allow_cpu_retry) ->default_value(g_allow_cpu_retry) ->implicit_value(true), R"(Allow the queries which failed on GPU to retry on CPU, even when watchdog is enabled.)"); - help_desc.add_options()("allow-loop-joins", - po::value(&allow_loop_joins) - ->default_value(allow_loop_joins) - ->implicit_value(true), - "Enable loop joins."); - help_desc.add_options()("bigint-count", - po::value(&g_bigint_count) - ->default_value(g_bigint_count) - ->implicit_value(true), - "Use 64-bit count."); - - help_desc.add_options()( + desc.add_options()("allow-loop-joins", + po::value(&allow_loop_joins) + ->default_value(allow_loop_joins) + ->implicit_value(true), + "Enable loop joins."); + desc.add_options()("bigint-count", + po::value(&g_bigint_count) + ->default_value(g_bigint_count) + ->implicit_value(true), + "Use 64-bit count."); + + desc.add_options()( "enable-executor-resource-mgr", po::value(&g_enable_executor_resource_mgr) ->default_value(g_enable_executor_resource_mgr) @@ -124,7 +125,7 @@ void CommandLineOptions::fillOptions() { // Note we allow executor-cpu-result-mem-ratio to have values > 0 to allow // oversubscription of memory when warranted, but user should be careful with this as // too high a value can cause OOM errors. - help_desc.add_options()( + desc.add_options()( "executor-cpu-result-mem-ratio", po::value(&g_executor_resource_mgr_cpu_result_mem_ratio) ->default_value(g_executor_resource_mgr_cpu_result_mem_ratio), @@ -134,7 +135,7 @@ void CommandLineOptions::fillOptions() { "warranted, but too high a value can cause out-of-memory errors. Requires " "--executor-resource-mgr to be set"); - help_desc.add_options()( + desc.add_options()( "executor-cpu-result-mem-bytes", po::value(&g_executor_resource_mgr_cpu_result_mem_bytes) ->default_value(g_executor_resource_mgr_cpu_result_mem_bytes), @@ -147,7 +148,7 @@ void CommandLineOptions::fillOptions() { // oversubscription of threads when warranted, given we may be overly pessimistic about // kernel core occupation for some classes of queries. Care should be taken however with // setting this value too high as thrashing and thread starvation can result. - help_desc.add_options()( + desc.add_options()( "executor-per-query-max-cpu-threads-ratio", po::value(&g_executor_resource_mgr_per_query_max_cpu_slots_ratio) ->default_value(g_executor_resource_mgr_per_query_max_cpu_slots_ratio), @@ -157,7 +158,7 @@ void CommandLineOptions::fillOptions() { // Note we allow executor-per-query-max-cpu-result-mem-ratio to have values > 0 to allow // oversubscription of memory when warranted, but user should be careful with this as // too high a value can cause OOM errors. - help_desc.add_options()( + desc.add_options()( "executor-per-query-max-cpu-result-mem-ratio", po::value(&g_executor_resource_mgr_per_query_max_cpu_result_mem_ratio) ->default_value(g_executor_resource_mgr_per_query_max_cpu_result_mem_ratio), @@ -165,7 +166,7 @@ void CommandLineOptions::fillOptions() { "that can be " "allocated for a single query. Requires --enable-executor-resource-mgr to be set."); - help_desc.add_options()( + desc.add_options()( "allow-cpu-kernel-concurrency", po::value(&g_executor_resource_mgr_allow_cpu_kernel_concurrency) ->default_value(g_executor_resource_mgr_allow_cpu_kernel_concurrency) @@ -173,7 +174,7 @@ void CommandLineOptions::fillOptions() { "Allow for multiple queries to run execution kernels concurrently on CPU. Requires " "--enable-executor-resource-mgr to be set."); - help_desc.add_options()( + desc.add_options()( "allow-cpu-gpu-kernel-concurrency", po::value(&g_executor_resource_mgr_allow_cpu_gpu_kernel_concurrency) ->default_value(g_executor_resource_mgr_allow_cpu_gpu_kernel_concurrency) @@ -185,7 +186,7 @@ void CommandLineOptions::fillOptions() { // CPU slots/threads Single query CPU slot oversubscription should be controlled with // --executor-per-query-max-cpu-threads-ratio (i.e. by setting it to > 1.0) - help_desc.add_options()( + desc.add_options()( "allow-cpu-thread-oversubscription-concurrency", po::value( &g_executor_resource_mgr_allow_cpu_slot_oversubscription_concurrency) @@ -202,7 +203,7 @@ void CommandLineOptions::fillOptions() { // controlled with // --executor-per-query-cpu-result-mem-ratio (i.e. by setting it to > 1.0) - help_desc.add_options()( + desc.add_options()( "allow-cpu-result-mem-oversubscription-concurrency", po::value( &g_executor_resource_mgr_allow_cpu_result_mem_oversubscription_concurrency) @@ -214,342 +215,331 @@ void CommandLineOptions::fillOptions() { "can lead to out-of-memory errors. Requires --enable-executor-resource-mgr to be " "set."); - help_desc.add_options()( + desc.add_options()( "executor-max-available-resource-use-ratio", po::value(&g_executor_resource_mgr_max_available_resource_use_ratio) ->default_value(g_executor_resource_mgr_max_available_resource_use_ratio), "Set max proportion (0 < ratio <= 1.0) of available resources that should be " "granted to a query. Requires --executor-resource-mgr to be set"); - help_desc.add_options()("calcite-max-mem", - po::value(&system_parameters.calcite_max_mem) - ->default_value(system_parameters.calcite_max_mem), - "Max memory available to calcite JVM."); + desc.add_options()("calcite-max-mem", + po::value(&system_parameters.calcite_max_mem) + ->default_value(system_parameters.calcite_max_mem), + "Max memory available to calcite JVM."); if (!dist_v5_) { - help_desc.add_options()("calcite-port", - po::value(&system_parameters.calcite_port) - ->default_value(system_parameters.calcite_port), - "Calcite port number."); + desc.add_options()("calcite-port", + po::value(&system_parameters.calcite_port) + ->default_value(system_parameters.calcite_port), + "Calcite port number."); } - help_desc.add_options()("config", - po::value(&system_parameters.config_file), - "Path to server configuration file."); - help_desc.add_options()("cpu-buffer-mem-bytes", - po::value(&system_parameters.cpu_buffer_mem_bytes) - ->default_value(system_parameters.cpu_buffer_mem_bytes), - "Size of memory reserved for CPU buffers, in bytes."); - - help_desc.add_options()("cpu-only", - po::value(&system_parameters.cpu_only) - ->default_value(system_parameters.cpu_only) - ->implicit_value(true), - "Run on CPU only, even if GPUs are available."); - help_desc.add_options()("cuda-block-size", - po::value(&system_parameters.cuda_block_size) - ->default_value(system_parameters.cuda_block_size), - "Size of block to use on NVIDIA GPU."); - help_desc.add_options()("cuda-grid-size", - po::value(&system_parameters.cuda_grid_size) - ->default_value(system_parameters.cuda_grid_size), - "Size of grid to use on NVIDIA GPU."); - help_desc.add_options()("optimize-cuda-block-and-grid-sizes", - po::value(&optimize_cuda_block_and_grid_sizes) - ->default_value(false) - ->implicit_value(true)); + desc.add_options()("config", + po::value(&system_parameters.config_file), + "Path to server configuration file."); + desc.add_options()("cpu-buffer-mem-bytes", + po::value(&system_parameters.cpu_buffer_mem_bytes) + ->default_value(system_parameters.cpu_buffer_mem_bytes), + "Size of memory reserved for CPU buffers, in bytes."); + + desc.add_options()("cpu-only", + po::value(&system_parameters.cpu_only) + ->default_value(system_parameters.cpu_only) + ->implicit_value(true), + "Run on CPU only, even if GPUs are available."); + desc.add_options()("cuda-block-size", + po::value(&system_parameters.cuda_block_size) + ->default_value(system_parameters.cuda_block_size), + "Size of block to use on NVIDIA GPU."); + desc.add_options()("cuda-grid-size", + po::value(&system_parameters.cuda_grid_size) + ->default_value(system_parameters.cuda_grid_size), + "Size of grid to use on NVIDIA GPU."); + desc.add_options()("optimize-cuda-block-and-grid-sizes", + po::value(&optimize_cuda_block_and_grid_sizes) + ->default_value(false) + ->implicit_value(true)); if (!dist_v5_) { - help_desc.add_options()( + desc.add_options()( "data", po::value(&base_path)->required()->default_value("storage"), "Directory path to HeavyDB data storage (catalogs, raw data, log files, etc)."); positional_options.add("data", 1); } - help_desc.add_options()("db-query-list", - po::value(&db_query_file), - "Path to file containing HeavyDB warmup queries."); - help_desc.add_options()( + desc.add_options()("db-query-list", + po::value(&db_query_file), + "Path to file containing HeavyDB warmup queries."); + desc.add_options()( "exit-after-warmup", po::value(&exit_after_warmup)->default_value(false)->implicit_value(true), "Exit after HeavyDB warmup queries."); - help_desc.add_options()("dynamic-watchdog-time-limit", - po::value(&dynamic_watchdog_time_limit) - ->default_value(dynamic_watchdog_time_limit) - ->implicit_value(10000), - "Dynamic watchdog time limit, in milliseconds."); - help_desc.add_options()("enable-data-recycler", - po::value(&enable_data_recycler) - ->default_value(enable_data_recycler) - ->implicit_value(true), - "Use data recycler."); - help_desc.add_options()("use-hashtable-cache", - po::value(&use_hashtable_cache) - ->default_value(use_hashtable_cache) - ->implicit_value(true), - "Use hashtable cache."); - help_desc.add_options()("use-query-resultset-cache", - po::value(&g_use_query_resultset_cache) - ->default_value(g_use_query_resultset_cache) - ->implicit_value(true), - "Use query resultset cache."); - help_desc.add_options()("use-chunk-metadata-cache", - po::value(&g_use_chunk_metadata_cache) - ->default_value(g_use_chunk_metadata_cache) - ->implicit_value(true), - "Use chunk metadata cache."); - help_desc.add_options()( + desc.add_options()("dynamic-watchdog-time-limit", + po::value(&dynamic_watchdog_time_limit) + ->default_value(dynamic_watchdog_time_limit) + ->implicit_value(10000), + "Dynamic watchdog time limit, in milliseconds."); + desc.add_options()("enable-data-recycler", + po::value(&enable_data_recycler) + ->default_value(enable_data_recycler) + ->implicit_value(true), + "Use data recycler."); + desc.add_options()("use-hashtable-cache", + po::value(&use_hashtable_cache) + ->default_value(use_hashtable_cache) + ->implicit_value(true), + "Use hashtable cache."); + desc.add_options()("use-query-resultset-cache", + po::value(&g_use_query_resultset_cache) + ->default_value(g_use_query_resultset_cache) + ->implicit_value(true), + "Use query resultset cache."); + desc.add_options()("use-chunk-metadata-cache", + po::value(&g_use_chunk_metadata_cache) + ->default_value(g_use_chunk_metadata_cache) + ->implicit_value(true), + "Use chunk metadata cache."); + desc.add_options()( "hashtable-cache-total-bytes", po::value(&hashtable_cache_total_bytes) ->default_value(hashtable_cache_total_bytes) ->implicit_value(4294967296), "Size of total memory space for hashtable cache, in bytes (default: 4GB)."); - help_desc.add_options()("max-cacheable-hashtable-size-bytes", - po::value(&max_cacheable_hashtable_size_bytes) - ->default_value(max_cacheable_hashtable_size_bytes) - ->implicit_value(2147483648), - "The maximum size of hashtable that is available to cache, in " - "bytes (default: 2GB)."); - help_desc.add_options()( + desc.add_options()("max-cacheable-hashtable-size-bytes", + po::value(&max_cacheable_hashtable_size_bytes) + ->default_value(max_cacheable_hashtable_size_bytes) + ->implicit_value(2147483648), + "The maximum size of hashtable that is available to cache, in " + "bytes (default: 2GB)."); + desc.add_options()( "query-resultset-cache-total-bytes", po::value(&g_query_resultset_cache_total_bytes) ->default_value(g_query_resultset_cache_total_bytes), "Size of total memory space for query resultset cache, in bytes (default: 4GB)."); - help_desc.add_options()( - "max-query-resultset-size-bytes", - po::value(&g_max_cacheable_query_resultset_size_bytes) - ->default_value(g_max_cacheable_query_resultset_size_bytes), - "The maximum size of query resultset that is available to cache, in " - "bytes (default: 2GB)."); - help_desc.add_options()("allow-auto-query-resultset-caching", - po::value(&g_allow_auto_resultset_caching) - ->default_value(g_allow_auto_resultset_caching) - ->implicit_value(true), - "Allow automatic query resultset caching when the size of " - "query resultset is smaller or equal to the threshold defined " - "by `auto-resultset-caching-threshold-bytes`, in bytes (to " - "enable this, query resultset recycler " - "should be enabled, default: 1048576 bytes (or 1MB))."); - help_desc.add_options()( + desc.add_options()("max-query-resultset-size-bytes", + po::value(&g_max_cacheable_query_resultset_size_bytes) + ->default_value(g_max_cacheable_query_resultset_size_bytes), + "The maximum size of query resultset that is available to cache, in " + "bytes (default: 2GB)."); + desc.add_options()("allow-auto-query-resultset-caching", + po::value(&g_allow_auto_resultset_caching) + ->default_value(g_allow_auto_resultset_caching) + ->implicit_value(true), + "Allow automatic query resultset caching when the size of " + "query resultset is smaller or equal to the threshold defined " + "by `auto-resultset-caching-threshold-bytes`, in bytes (to " + "enable this, query resultset recycler " + "should be enabled, default: 1048576 bytes (or 1MB))."); + desc.add_options()( "auto-resultset-caching-threshold-bytes", po::value(&g_auto_resultset_caching_threshold) ->default_value(g_auto_resultset_caching_threshold), "A threshold that allows caching query resultset automatically if the size of " "resultset is less than it, in bytes (default: 1MB)."); - help_desc.add_options()("allow-query-step-skipping", - po::value(&g_allow_query_step_skipping) - ->default_value(g_allow_query_step_skipping) - ->implicit_value(true), - "Allow query step skipping when multi-step query has at least " - "one cached query resultset."); - help_desc.add_options()("enable-debug-timer", - po::value(&g_enable_debug_timer) - ->default_value(g_enable_debug_timer) - ->implicit_value(true), - "Enable debug timer logging."); - help_desc.add_options()("enable-dynamic-watchdog", - po::value(&enable_dynamic_watchdog) - ->default_value(enable_dynamic_watchdog) - ->implicit_value(true), - "Enable dynamic watchdog."); - help_desc.add_options()("enable-filter-push-down", - po::value(&g_enable_filter_push_down) - ->default_value(g_enable_filter_push_down) - ->implicit_value(true), - "Enable filter push down through joins."); - help_desc.add_options()("enable-overlaps-hashjoin", - po::value(&g_enable_overlaps_hashjoin) - ->default_value(g_enable_overlaps_hashjoin) - ->implicit_value(true), - "Enable the overlaps hash join framework allowing for range " - "join (e.g. spatial overlaps) computation using a hash table."); - help_desc.add_options()("enable-hashjoin-many-to-many", - po::value(&g_enable_hashjoin_many_to_many) - ->default_value(g_enable_hashjoin_many_to_many) - ->implicit_value(true), - "Enable the overlaps hash join framework allowing for range " - "join (e.g. spatial overlaps) computation using a hash table."); - help_desc.add_options()("enable-distance-rangejoin", - po::value(&g_enable_distance_rangejoin) - ->default_value(g_enable_distance_rangejoin) - ->implicit_value(true), - "Enable accelerating point distance joins with a hash table. " - "This rewrites ST_Distance when using an upperbound (<= X)."); - help_desc.add_options()("enable-runtime-query-interrupt", - po::value(&enable_runtime_query_interrupt) - ->default_value(enable_runtime_query_interrupt) - ->implicit_value(true), - "Enable runtime query interrupt."); - help_desc.add_options()("enable-non-kernel-time-query-interrupt", - po::value(&enable_non_kernel_time_query_interrupt) - ->default_value(enable_non_kernel_time_query_interrupt) - ->implicit_value(true), - "Enable non-kernel time query interrupt."); - help_desc.add_options()("pending-query-interrupt-freq", - po::value(&pending_query_interrupt_freq) - ->default_value(pending_query_interrupt_freq) - ->implicit_value(1000), - "A frequency of checking the request of pending query " - "interrupt from user (in millisecond)."); - help_desc.add_options()( - "running-query-interrupt-freq", - po::value(&running_query_interrupt_freq) - ->default_value(running_query_interrupt_freq) - ->implicit_value(0.5), - "A frequency of checking the request of running query " - "interrupt from user (0.0 (less frequent) ~ (more frequent) 1.0)."); - help_desc.add_options()("use-estimator-result-cache", - po::value(&use_estimator_result_cache) - ->default_value(use_estimator_result_cache) - ->implicit_value(true), - "Use estimator result cache."); + + desc.add_options()("allow-query-step-skipping", + po::value(&g_allow_query_step_skipping) + ->default_value(g_allow_query_step_skipping) + ->implicit_value(true), + "Allow query step skipping when multi-step query has at least " + "one cached query resultset."); + desc.add_options()("enable-debug-timer", + po::value(&g_enable_debug_timer) + ->default_value(g_enable_debug_timer) + ->implicit_value(true), + "Enable debug timer logging."); + desc.add_options()("enable-dynamic-watchdog", + po::value(&enable_dynamic_watchdog) + ->default_value(enable_dynamic_watchdog) + ->implicit_value(true), + "Enable dynamic watchdog."); + desc.add_options()("enable-filter-push-down", + po::value(&g_enable_filter_push_down) + ->default_value(g_enable_filter_push_down) + ->implicit_value(true), + "Enable filter push down through joins."); + desc.add_options()("enable-overlaps-hashjoin", + po::value(&g_enable_overlaps_hashjoin) + ->default_value(g_enable_overlaps_hashjoin) + ->implicit_value(true), + "Enable the overlaps hash join framework allowing for range " + "join (e.g. spatial overlaps) computation using a hash table."); + desc.add_options()("enable-hashjoin-many-to-many", + po::value(&g_enable_hashjoin_many_to_many) + ->default_value(g_enable_hashjoin_many_to_many) + ->implicit_value(true), + "Enable the overlaps hash join framework allowing for range " + "join (e.g. spatial overlaps) computation using a hash table."); + desc.add_options()("enable-distance-rangejoin", + po::value(&g_enable_distance_rangejoin) + ->default_value(g_enable_distance_rangejoin) + ->implicit_value(true), + "Enable accelerating point distance joins with a hash table. " + "This rewrites ST_Distance when using an upperbound (<= X)."); + desc.add_options()("enable-runtime-query-interrupt", + po::value(&enable_runtime_query_interrupt) + ->default_value(enable_runtime_query_interrupt) + ->implicit_value(true), + "Enable runtime query interrupt."); + desc.add_options()("enable-non-kernel-time-query-interrupt", + po::value(&enable_non_kernel_time_query_interrupt) + ->default_value(enable_non_kernel_time_query_interrupt) + ->implicit_value(true), + "Enable non-kernel time query interrupt."); + desc.add_options()("pending-query-interrupt-freq", + po::value(&pending_query_interrupt_freq) + ->default_value(pending_query_interrupt_freq) + ->implicit_value(1000), + "A frequency of checking the request of pending query " + "interrupt from user (in millisecond)."); + desc.add_options()("running-query-interrupt-freq", + po::value(&running_query_interrupt_freq) + ->default_value(running_query_interrupt_freq) + ->implicit_value(0.5), + "A frequency of checking the request of running query " + "interrupt from user (0.0 (less frequent) ~ (more frequent) 1.0)."); + if (!dist_v5_) { - help_desc.add_options()( + desc.add_options()( "enable-string-dict-hash-cache", po::value(&g_cache_string_hash) ->default_value(g_cache_string_hash) ->implicit_value(true), "Cache string hash values in the string dictionary server during import."); } - help_desc.add_options()( - "enable-thrift-logs", - po::value(&g_enable_thrift_logs) - ->default_value(g_enable_thrift_logs) - ->implicit_value(true), - "Enable writing messages directly from thrift to stdout/stderr."); - help_desc.add_options()("enable-watchdog", - po::value(&enable_watchdog) - ->default_value(enable_watchdog) - ->implicit_value(true), - "Enable watchdog."); - help_desc.add_options()( - "watchdog-none-encoded-string-translation-limit", - po::value(&watchdog_none_encoded_string_translation_limit) - ->default_value(watchdog_none_encoded_string_translation_limit), - "Max number of none-encoded strings allowed to be translated " - "to dictionary-encoded with watchdog enabled"); - help_desc.add_options()( - "filter-push-down-low-frac", - po::value(&g_filter_push_down_low_frac) - ->default_value(g_filter_push_down_low_frac) - ->implicit_value(g_filter_push_down_low_frac), - "Lower threshold for selectivity of filters that are pushed down."); - help_desc.add_options()( - "filter-push-down-high-frac", - po::value(&g_filter_push_down_high_frac) - ->default_value(g_filter_push_down_high_frac) - ->implicit_value(g_filter_push_down_high_frac), - "Higher threshold for selectivity of filters that are pushed down."); - help_desc.add_options()("filter-push-down-passing-row-ubound", - po::value(&g_filter_push_down_passing_row_ubound) - ->default_value(g_filter_push_down_passing_row_ubound) - ->implicit_value(g_filter_push_down_passing_row_ubound), - "Upperbound on the number of rows that should pass the filter " - "if the selectivity is less than " - "the high fraction threshold."); - help_desc.add_options()("from-table-reordering", - po::value(&g_from_table_reordering) - ->default_value(g_from_table_reordering) - ->implicit_value(true), - "Enable automatic table reordering in FROM clause."); - help_desc.add_options()("gpu-buffer-mem-bytes", - po::value(&system_parameters.gpu_buffer_mem_bytes) - ->default_value(system_parameters.gpu_buffer_mem_bytes), - "Size of memory reserved for GPU buffers, in bytes, per GPU."); - help_desc.add_options()("gpu-input-mem-limit", - po::value(&system_parameters.gpu_input_mem_limit) - ->default_value(system_parameters.gpu_input_mem_limit), - "Force query to CPU when input data memory usage exceeds this " - "percentage of available GPU memory."); - help_desc.add_options()( + desc.add_options()("enable-thrift-logs", + po::value(&g_enable_thrift_logs) + ->default_value(g_enable_thrift_logs) + ->implicit_value(true), + "Enable writing messages directly from thrift to stdout/stderr."); + desc.add_options()("enable-watchdog", + po::value(&enable_watchdog) + ->default_value(enable_watchdog) + ->implicit_value(true), + "Enable watchdog."); + desc.add_options()("watchdog-none-encoded-string-translation-limit", + po::value(&watchdog_none_encoded_string_translation_limit) + ->default_value(watchdog_none_encoded_string_translation_limit), + "Max number of none-encoded strings allowed to be translated " + "to dictionary-encoded with watchdog enabled"); + desc.add_options()("filter-push-down-low-frac", + po::value(&g_filter_push_down_low_frac) + ->default_value(g_filter_push_down_low_frac) + ->implicit_value(g_filter_push_down_low_frac), + "Lower threshold for selectivity of filters that are pushed down."); + desc.add_options()("filter-push-down-high-frac", + po::value(&g_filter_push_down_high_frac) + ->default_value(g_filter_push_down_high_frac) + ->implicit_value(g_filter_push_down_high_frac), + "Higher threshold for selectivity of filters that are pushed down."); + desc.add_options()("filter-push-down-passing-row-ubound", + po::value(&g_filter_push_down_passing_row_ubound) + ->default_value(g_filter_push_down_passing_row_ubound) + ->implicit_value(g_filter_push_down_passing_row_ubound), + "Upperbound on the number of rows that should pass the filter " + "if the selectivity is less than " + "the high fraction threshold."); + desc.add_options()("from-table-reordering", + po::value(&g_from_table_reordering) + ->default_value(g_from_table_reordering) + ->implicit_value(true), + "Enable automatic table reordering in FROM clause."); + desc.add_options()("gpu-buffer-mem-bytes", + po::value(&system_parameters.gpu_buffer_mem_bytes) + ->default_value(system_parameters.gpu_buffer_mem_bytes), + "Size of memory reserved for GPU buffers, in bytes, per GPU."); + desc.add_options()("gpu-input-mem-limit", + po::value(&system_parameters.gpu_input_mem_limit) + ->default_value(system_parameters.gpu_input_mem_limit), + "Force query to CPU when input data memory usage exceeds this " + "percentage of available GPU memory."); + desc.add_options()( "hll-precision-bits", po::value(&g_hll_precision_bits) ->default_value(g_hll_precision_bits) ->implicit_value(g_hll_precision_bits), "Number of bits used from the hash value used to specify the bucket number."); if (!dist_v5_) { - help_desc.add_options()("http-port", - po::value(&http_port)->default_value(http_port), - "HTTP port number."); - help_desc.add_options()( - "http-binary-port", - po::value(&http_binary_port)->default_value(http_binary_port), - "HTTP binary port number."); + desc.add_options()("http-port", + po::value(&http_port)->default_value(http_port), + "HTTP port number."); + desc.add_options()("http-binary-port", + po::value(&http_binary_port)->default_value(http_binary_port), + "HTTP binary port number."); } - help_desc.add_options()( + desc.add_options()( "idle-session-duration", po::value(&idle_session_duration)->default_value(idle_session_duration), "Maximum duration of idle session."); - help_desc.add_options()("inner-join-fragment-skipping", - po::value(&g_inner_join_fragment_skipping) - ->default_value(g_inner_join_fragment_skipping) - ->implicit_value(true), - "Enable/disable inner join fragment skipping. This feature is " - "considered stable and is enabled by default. This " - "parameter will be removed in a future release."); - help_desc.add_options()( + desc.add_options()("inner-join-fragment-skipping", + po::value(&g_inner_join_fragment_skipping) + ->default_value(g_inner_join_fragment_skipping) + ->implicit_value(true), + "Enable/disable inner join fragment skipping. This feature is " + "considered stable and is enabled by default. This " + "parameter will be removed in a future release."); + desc.add_options()( "max-session-duration", po::value(&max_session_duration)->default_value(max_session_duration), "Maximum duration of active session."); - help_desc.add_options()("num-sessions", - po::value(&system_parameters.num_sessions) - ->default_value(system_parameters.num_sessions), - "Maximum number of active session."); - help_desc.add_options()( - "null-div-by-zero", - po::value(&g_null_div_by_zero) - ->default_value(g_null_div_by_zero) - ->implicit_value(true), - "Return null on division by zero instead of throwing an exception."); - help_desc.add_options()( + desc.add_options()("num-sessions", + po::value(&system_parameters.num_sessions) + ->default_value(system_parameters.num_sessions), + "Maximum number of active session."); + desc.add_options()("null-div-by-zero", + po::value(&g_null_div_by_zero) + ->default_value(g_null_div_by_zero) + ->implicit_value(true), + "Return null on division by zero instead of throwing an exception."); + desc.add_options()( "num-reader-threads", po::value(&num_reader_threads)->default_value(num_reader_threads), "Number of reader threads to use."); - help_desc.add_options()( + desc.add_options()( "max-import-threads", po::value(&g_max_import_threads)->default_value(g_max_import_threads), "Max number of default import threads to use (num hardware threads will be used " "instead if lower). Can be overriden with copy statement threads option)."); - help_desc.add_options()( + desc.add_options()( "overlaps-max-table-size-bytes", po::value(&g_overlaps_max_table_size_bytes) ->default_value(g_overlaps_max_table_size_bytes), "The maximum size in bytes of the hash table for an overlaps hash join."); - help_desc.add_options()("overlaps-target-entries-per-bin", - po::value(&g_overlaps_target_entries_per_bin) - ->default_value(g_overlaps_target_entries_per_bin), - "The target number of hash entries per bin for overlaps join"); + desc.add_options()("overlaps-target-entries-per-bin", + po::value(&g_overlaps_target_entries_per_bin) + ->default_value(g_overlaps_target_entries_per_bin), + "The target number of hash entries per bin for overlaps join"); if (!dist_v5_) { - help_desc.add_options()("port,p", - po::value(&system_parameters.omnisci_server_port) - ->default_value(system_parameters.omnisci_server_port), - "TCP Port number."); + desc.add_options()("port,p", + po::value(&system_parameters.omnisci_server_port) + ->default_value(system_parameters.omnisci_server_port), + "TCP Port number."); } - help_desc.add_options()("num-gpus", - po::value(&system_parameters.num_gpus) - ->default_value(system_parameters.num_gpus), - "Number of gpus to use."); - help_desc.add_options()( + desc.add_options()("num-gpus", + po::value(&system_parameters.num_gpus) + ->default_value(system_parameters.num_gpus), + "Number of gpus to use."); + desc.add_options()( "read-only", po::value(&read_only)->default_value(read_only)->implicit_value(true), "Enable read-only mode."); - help_desc.add_options()( + desc.add_options()( "res-gpu-mem", po::value(&reserved_gpu_mem)->default_value(reserved_gpu_mem), "Reduces GPU memory available to the HeavyDB allocator by this amount. Used for " "compiled code cache and ancillary GPU functions and other processes that may also " "be using the GPU concurrent with HeavyDB."); - help_desc.add_options()("start-gpu", - po::value(&system_parameters.start_gpu) - ->default_value(system_parameters.start_gpu), - "First gpu to use."); - help_desc.add_options()("trivial-loop-join-threshold", - po::value(&g_trivial_loop_join_threshold) - ->default_value(g_trivial_loop_join_threshold) - ->implicit_value(1000), - "The maximum number of rows in the inner table of a loop join " - "considered to be trivially small."); - help_desc.add_options()( + desc.add_options()("start-gpu", + po::value(&system_parameters.start_gpu) + ->default_value(system_parameters.start_gpu), + "First gpu to use."); + desc.add_options()("trivial-loop-join-threshold", + po::value(&g_trivial_loop_join_threshold) + ->default_value(g_trivial_loop_join_threshold) + ->implicit_value(1000), + "The maximum number of rows in the inner table of a loop join " + "considered to be trivially small."); + desc.add_options()( "uniform-request-ids-per-thrift-call", po::value(&g_uniform_request_ids_per_thrift_call) ->default_value(g_uniform_request_ids_per_thrift_call) @@ -557,19 +547,19 @@ void CommandLineOptions::fillOptions() { "If true (default) then assign the same request_id to thrift calls that were " "initiated by the same external thrift call. If false then assign different " "request_ids and log the parent/child relationships."); - help_desc.add_options()("verbose", - po::value(&verbose_logging) - ->default_value(verbose_logging) - ->implicit_value(true), - "Write additional debug log messages to server logs."); - help_desc.add_options()( + desc.add_options()("verbose", + po::value(&verbose_logging) + ->default_value(verbose_logging) + ->implicit_value(true), + "Write additional debug log messages to server logs."); + desc.add_options()( "enable-runtime-udf", po::value(&enable_runtime_udf) ->default_value(enable_runtime_udf) ->implicit_value(true), "DEPRECATED. Please use `enable-runtime-udfs` instead as this flag will be removed " "in the near future."); - help_desc.add_options()( + desc.add_options()( "enable-runtime-udfs", po::value(&enable_runtime_udfs) ->default_value(enable_runtime_udfs) @@ -577,70 +567,70 @@ void CommandLineOptions::fillOptions() { "Enable runtime UDF registration by passing signatures and corresponding LLVM IR " "to the `register_runtime_udf` endpoint. For use with the Python Remote Backend " "Compiler server, packaged separately."); - help_desc.add_options()("enable-udf-registration-for-all-users", - po::value(&enable_udf_registration_for_all_users) - ->default_value(enable_udf_registration_for_all_users) - ->implicit_value(true), - "Allow all users, not just superusers, to register runtime " - "UDFs/UDTFs. Option only valid if " - "`--enable-runtime-udfs` is set to true."); - help_desc.add_options()("version,v", "Print Version Number."); - help_desc.add_options()("enable-string-functions", - po::value(&g_enable_string_functions) - ->default_value(g_enable_string_functions) - ->implicit_value(true), - "Enable experimental string functions."); - help_desc.add_options()("enable-experimental-string-functions", - po::value(&g_enable_string_functions) - ->default_value(g_enable_string_functions) - ->implicit_value(true), - "DEPRECATED. String functions are now enabled by default, " - "but can still be controlled with --enable-string-functions."); - help_desc.add_options()( + desc.add_options()("enable-udf-registration-for-all-users", + po::value(&enable_udf_registration_for_all_users) + ->default_value(enable_udf_registration_for_all_users) + ->implicit_value(true), + "Allow all users, not just superusers, to register runtime " + "UDFs/UDTFs. Option only valid if " + "`--enable-runtime-udfs` is set to true."); + desc.add_options()("version,v", "Print Version Number."); + desc.add_options()("enable-string-functions", + po::value(&g_enable_string_functions) + ->default_value(g_enable_string_functions) + ->implicit_value(true), + "Enable experimental string functions."); + desc.add_options()("enable-experimental-string-functions", + po::value(&g_enable_string_functions) + ->default_value(g_enable_string_functions) + ->implicit_value(true), + "DEPRECATED. String functions are now enabled by default, " + "but can still be controlled with --enable-string-functions."); + desc.add_options()( "enable-fsi", po::value(&g_enable_fsi)->default_value(g_enable_fsi)->implicit_value(true), "Enable foreign storage interface."); - help_desc.add_options()("enable-legacy-delimited-import", - po::value(&g_enable_legacy_delimited_import) - ->default_value(g_enable_legacy_delimited_import) - ->implicit_value(true), - "Use legacy importer for delimited sources."); + desc.add_options()("enable-legacy-delimited-import", + po::value(&g_enable_legacy_delimited_import) + ->default_value(g_enable_legacy_delimited_import) + ->implicit_value(true), + "Use legacy importer for delimited sources."); #ifdef ENABLE_IMPORT_PARQUET - help_desc.add_options()("enable-legacy-parquet-import", - po::value(&g_enable_legacy_parquet_import) - ->default_value(g_enable_legacy_parquet_import) - ->implicit_value(true), - "Use legacy importer for parquet sources."); + desc.add_options()("enable-legacy-parquet-import", + po::value(&g_enable_legacy_parquet_import) + ->default_value(g_enable_legacy_parquet_import) + ->implicit_value(true), + "Use legacy importer for parquet sources."); #endif - help_desc.add_options()("enable-fsi-regex-import", - po::value(&g_enable_fsi_regex_import) - ->default_value(g_enable_fsi_regex_import) - ->implicit_value(true), - "Use FSI importer for regex parsed sources."); - - help_desc.add_options()("enable-add-metadata-columns", - po::value(&g_enable_add_metadata_columns) - ->default_value(g_enable_add_metadata_columns) - ->implicit_value(true), - "Enable add_metadata_columns COPY FROM WITH option (Beta)."); - - help_desc.add_options()("disk-cache-path", - po::value(&disk_cache_config.path), - "Specify the path for the disk cache."); - - help_desc.add_options()( + desc.add_options()("enable-fsi-regex-import", + po::value(&g_enable_fsi_regex_import) + ->default_value(g_enable_fsi_regex_import) + ->implicit_value(true), + "Use FSI importer for regex parsed sources."); + + desc.add_options()("enable-add-metadata-columns", + po::value(&g_enable_add_metadata_columns) + ->default_value(g_enable_add_metadata_columns) + ->implicit_value(true), + "Enable add_metadata_columns COPY FROM WITH option (Beta)."); + + desc.add_options()("disk-cache-path", + po::value(&disk_cache_config.path), + "Specify the path for the disk cache."); + + desc.add_options()( "disk-cache-level", po::value(&(disk_cache_level))->default_value("foreign_tables"), "Specify level of disk cache. Valid options are 'foreign_tables', " "'local_tables', 'none', and 'all'."); - help_desc.add_options()("disk-cache-size", - po::value(&(disk_cache_config.size_limit)), - "Specify a maximum size for the disk cache in bytes."); + desc.add_options()("disk-cache-size", + po::value(&(disk_cache_config.size_limit)), + "Specify a maximum size for the disk cache in bytes."); #ifdef HAVE_AWS_S3 - help_desc.add_options()( + desc.add_options()( "allow-s3-server-privileges", po::value(&g_allow_s3_server_privileges) ->default_value(g_allow_s3_server_privileges) @@ -651,141 +641,137 @@ void CommandLineOptions::fillOptions() { "an AWS credentials file, or when running on an EC2 instance, with an IAM role " "that is attached to the instance."); #endif // defined(HAVE_AWS_S3) - help_desc.add_options()( + desc.add_options()( "enable-interoperability", po::value(&g_enable_interop) ->default_value(g_enable_interop) ->implicit_value(true), "Enable offloading of query portions to an external execution engine."); - help_desc.add_options()("enable-union", - po::value(&g_enable_union) - ->default_value(g_enable_union) - ->implicit_value(true), - "DEPRECATED. UNION ALL is enabled by default. Please remove " - "use of this option, as it may be disabled in the future."); - help_desc.add_options()( + desc.add_options()("enable-union", + po::value(&g_enable_union) + ->default_value(g_enable_union) + ->implicit_value(true), + "DEPRECATED. UNION ALL is enabled by default. Please remove " + "use of this option, as it may be disabled in the future."); + desc.add_options()( "calcite-service-timeout", po::value(&system_parameters.calcite_timeout) ->default_value(system_parameters.calcite_timeout), "Calcite server timeout (milliseconds). Increase this on systems with frequent " "schema changes or when running large numbers of parallel queries."); - help_desc.add_options()("calcite-service-keepalive", - po::value(&system_parameters.calcite_keepalive) - ->default_value(system_parameters.calcite_keepalive) - ->implicit_value(true), - "Enable keepalive on Calcite connections."); - help_desc.add_options()( + desc.add_options()("calcite-service-keepalive", + po::value(&system_parameters.calcite_keepalive) + ->default_value(system_parameters.calcite_keepalive) + ->implicit_value(true), + "Enable keepalive on Calcite connections."); + desc.add_options()( "stringdict-parallelizm", po::value(&g_enable_stringdict_parallel) ->default_value(g_enable_stringdict_parallel) ->implicit_value(true), "Allow StringDictionary to parallelize loads using multiple threads"); - help_desc.add_options()( - "log-user-id", - po::value(&Catalog_Namespace::g_log_user_id) - ->default_value(Catalog_Namespace::g_log_user_id) - ->implicit_value(true), - "Log userId integer in place of the userName (when available)."); - help_desc.add_options()("log-user-origin", - po::value(&log_user_origin) - ->default_value(log_user_origin) - ->implicit_value(true), - "Lookup the origin of inbound connections by IP address/DNS " - "name, and print this information as part of stdlog."); - help_desc.add_options()( - "allowed-import-paths", - po::value(&allowed_import_paths), - "List of allowed root paths that can be used in import operations."); - help_desc.add_options()( - "allowed-export-paths", - po::value(&allowed_export_paths), - "List of allowed root paths that can be used in export operations."); - help_desc.add_options()("enable-system-tables", - po::value(&g_enable_system_tables) - ->default_value(g_enable_system_tables) - ->implicit_value(true), - "Enable use of system tables."); - help_desc.add_options()("enable-table-functions", - po::value(&g_enable_table_functions) - ->default_value(g_enable_table_functions) - ->implicit_value(true), - "Enable system table functions support."); - help_desc.add_options()("enable-logs-system-tables", - po::value(&g_enable_logs_system_tables) - ->default_value(g_enable_logs_system_tables) - ->implicit_value(true), - "Enable use of logs system tables."); - help_desc.add_options()( + desc.add_options()("log-user-id", + po::value(&Catalog_Namespace::g_log_user_id) + ->default_value(Catalog_Namespace::g_log_user_id) + ->implicit_value(true), + "Log userId integer in place of the userName (when available)."); + desc.add_options()("log-user-origin", + po::value(&log_user_origin) + ->default_value(log_user_origin) + ->implicit_value(true), + "Lookup the origin of inbound connections by IP address/DNS " + "name, and print this information as part of stdlog."); + desc.add_options()("allowed-import-paths", + po::value(&allowed_import_paths), + "List of allowed root paths that can be used in import operations."); + desc.add_options()("allowed-export-paths", + po::value(&allowed_export_paths), + "List of allowed root paths that can be used in export operations."); + desc.add_options()("enable-system-tables", + po::value(&g_enable_system_tables) + ->default_value(g_enable_system_tables) + ->implicit_value(true), + "Enable use of system tables."); + desc.add_options()("enable-table-functions", + po::value(&g_enable_table_functions) + ->default_value(g_enable_table_functions) + ->implicit_value(true), + "Enable system table functions support."); + desc.add_options()("enable-logs-system-tables", + po::value(&g_enable_logs_system_tables) + ->default_value(g_enable_logs_system_tables) + ->implicit_value(true), + "Enable use of logs system tables."); + desc.add_options()( "logs-system-tables-max-files-count", po::value(&g_logs_system_tables_max_files_count) ->default_value(g_logs_system_tables_max_files_count), "Maximum number of log files that will be processed by each logs system table."); #ifdef ENABLE_MEMKIND - help_desc.add_options()("enable-tiered-cpu-mem", - po::value(&g_enable_tiered_cpu_mem) - ->default_value(g_enable_tiered_cpu_mem) - ->implicit_value(true), - "Enable additional tiers of CPU memory (PMEM, etc...)"); - help_desc.add_options()("pmem-size", po::value(&g_pmem_size)->default_value(0)); - help_desc.add_options()("pmem-path", po::value(&g_pmem_path)); + desc.add_options()("enable-tiered-cpu-mem", + po::value(&g_enable_tiered_cpu_mem) + ->default_value(g_enable_tiered_cpu_mem) + ->implicit_value(true), + "Enable additional tiers of CPU memory (PMEM, etc...)"); + desc.add_options()("pmem-size", po::value(&g_pmem_size)->default_value(0)); + desc.add_options()("pmem-path", po::value(&g_pmem_path)); #endif - help_desc.add(log_options_.get_options()); + desc.add(log_options_.get_options()); } -void CommandLineOptions::fillAdvancedOptions() { - developer_desc.add_options()("dev-options", "Print internal developer options."); - developer_desc.add_options()( +void CommandLineOptions::fillDeveloperOptions() { + po::options_description& desc = developer_desc_; + + desc.add_options()("dev-options", "Print internal developer options."); + desc.add_options()( "enable-calcite-view-optimize", po::value(&system_parameters.enable_calcite_view_optimize) ->default_value(system_parameters.enable_calcite_view_optimize) ->implicit_value(true), "Enable additional calcite (query plan) optimizations when a view is part of the " "query."); - developer_desc.add_options()( - "enable-columnar-output", - po::value(&g_enable_columnar_output) - ->default_value(g_enable_columnar_output) - ->implicit_value(true), - "Enable columnar output for intermediate/final query steps."); - developer_desc.add_options()( - "enable-left-join-filter-hoisting", - po::value(&g_enable_left_join_filter_hoisting) - ->default_value(g_enable_left_join_filter_hoisting) - ->implicit_value(true), - "Enable hoisting left hand side filters through left joins."); - developer_desc.add_options()("optimize-row-init", - po::value(&g_optimize_row_initialization) - ->default_value(g_optimize_row_initialization) - ->implicit_value(true), - "Optimize row initialization."); - developer_desc.add_options()("enable-legacy-syntax", - po::value(&enable_legacy_syntax) - ->default_value(enable_legacy_syntax) - ->implicit_value(true), - "Enable legacy syntax."); - developer_desc.add_options()( + desc.add_options()("enable-columnar-output", + po::value(&g_enable_columnar_output) + ->default_value(g_enable_columnar_output) + ->implicit_value(true), + "Enable columnar output for intermediate/final query steps."); + desc.add_options()("enable-left-join-filter-hoisting", + po::value(&g_enable_left_join_filter_hoisting) + ->default_value(g_enable_left_join_filter_hoisting) + ->implicit_value(true), + "Enable hoisting left hand side filters through left joins."); + desc.add_options()("optimize-row-init", + po::value(&g_optimize_row_initialization) + ->default_value(g_optimize_row_initialization) + ->implicit_value(true), + "Optimize row initialization."); + desc.add_options()("enable-legacy-syntax", + po::value(&enable_legacy_syntax) + ->default_value(enable_legacy_syntax) + ->implicit_value(true), + "Enable legacy syntax."); + desc.add_options()( "enable-multifrag", po::value(&allow_multifrag) ->default_value(allow_multifrag) ->implicit_value(true), "Enable execution over multiple fragments in a single round-trip to GPU."); - developer_desc.add_options()("enable-lazy-fetch", - po::value(&g_enable_lazy_fetch) - ->default_value(g_enable_lazy_fetch) - ->implicit_value(true), - "Enable lazy fetch columns in query results."); - developer_desc.add_options()( - "enable-shared-mem-group-by", - po::value(&g_enable_smem_group_by) - ->default_value(g_enable_smem_group_by) - ->implicit_value(true), - "Enable using GPU shared memory for some GROUP BY queries."); - developer_desc.add_options()("num-executors", - po::value(&system_parameters.num_executors) - ->default_value(system_parameters.num_executors), - "Number of executors to run in parallel."); - developer_desc.add_options()( + desc.add_options()("enable-lazy-fetch", + po::value(&g_enable_lazy_fetch) + ->default_value(g_enable_lazy_fetch) + ->implicit_value(true), + "Enable lazy fetch columns in query results."); + desc.add_options()("enable-shared-mem-group-by", + po::value(&g_enable_smem_group_by) + ->default_value(g_enable_smem_group_by) + ->implicit_value(true), + "Enable using GPU shared memory for some GROUP BY queries."); + desc.add_options()("num-executors", + po::value(&system_parameters.num_executors) + ->default_value(system_parameters.num_executors), + "Number of executors to run in parallel."); + desc.add_options()( "num-tuple-threshold-switch-to-baseline", po::value(&g_num_tuple_threshold_switch_to_baseline) ->default_value(g_num_tuple_threshold_switch_to_baseline) @@ -796,7 +782,7 @@ void CommandLineOptions::fillAdvancedOptions() { "We switch hash table layout when this condition and the condition related to " "\'col-range-to-num-hash-entries-threshold-switch-to-baseline\' are satisfied " "together."); - developer_desc.add_options()( + desc.add_options()( "ratio-num-hash-entry-to-num-tuple-switch-to-baseline", po::value(&g_ratio_num_hash_entry_to_num_tuple_switch_to_baseline) ->default_value(g_ratio_num_hash_entry_to_num_tuple_switch_to_baseline) @@ -807,120 +793,114 @@ void CommandLineOptions::fillAdvancedOptions() { "{THIS_THRESHOLD}" "We switch hash table layout when this condition and the condition related to " "\'num-tuple-threshold-switch-to-baseline\' are satisfied together."); - developer_desc.add_options()( + desc.add_options()( "gpu-shared-mem-threshold", po::value(&g_gpu_smem_threshold)->default_value(g_gpu_smem_threshold), "GPU shared memory threshold (in bytes). If query requires larger buffers than " "this threshold, we disable those optimizations. 0 (default) means no static cap."); - developer_desc.add_options()( + desc.add_options()( "enable-shared-mem-grouped-non-count-agg", po::value(&g_enable_smem_grouped_non_count_agg) ->default_value(g_enable_smem_grouped_non_count_agg) ->implicit_value(true), "Enable using GPU shared memory for grouped non-count aggregate queries."); - developer_desc.add_options()( - "enable-shared-mem-non-grouped-agg", - po::value(&g_enable_smem_non_grouped_agg) - ->default_value(g_enable_smem_non_grouped_agg) - ->implicit_value(true), - "Enable using GPU shared memory for non-grouped aggregate queries."); - developer_desc.add_options()("enable-direct-columnarization", - po::value(&g_enable_direct_columnarization) - ->default_value(g_enable_direct_columnarization) - ->implicit_value(true), - "Enables/disables a more optimized columnarization method " - "for intermediate steps in multi-step queries."); - developer_desc.add_options()( + desc.add_options()("enable-shared-mem-non-grouped-agg", + po::value(&g_enable_smem_non_grouped_agg) + ->default_value(g_enable_smem_non_grouped_agg) + ->implicit_value(true), + "Enable using GPU shared memory for non-grouped aggregate queries."); + desc.add_options()("enable-direct-columnarization", + po::value(&g_enable_direct_columnarization) + ->default_value(g_enable_direct_columnarization) + ->implicit_value(true), + "Enables/disables a more optimized columnarization method " + "for intermediate steps in multi-step queries."); + desc.add_options()( "offset-device-by-table-id", po::value(&g_use_table_device_offset) ->default_value(g_use_table_device_offset) ->implicit_value(true), "Enables/disables offseting the chosen device ID by the table ID for a given " "fragment. This improves balance of fragments across GPUs."); - developer_desc.add_options()("enable-window-functions", - po::value(&g_enable_window_functions) - ->default_value(g_enable_window_functions) - ->implicit_value(true), - "Enable window function support."); - developer_desc.add_options()( - "enable-parallel-window-partition-compute", - po::value(&g_enable_parallel_window_partition_compute) - ->default_value(g_enable_parallel_window_partition_compute) - ->implicit_value(true), - "Enable parallel window function partition computation."); - developer_desc.add_options()( - "enable-parallel-window-partition-sort", - po::value(&g_enable_parallel_window_partition_sort) - ->default_value(g_enable_parallel_window_partition_sort) - ->implicit_value(true), - "Enable parallel window function partition sorting."); - developer_desc.add_options()( + desc.add_options()("enable-window-functions", + po::value(&g_enable_window_functions) + ->default_value(g_enable_window_functions) + ->implicit_value(true), + "Enable window function support."); + desc.add_options()("enable-parallel-window-partition-compute", + po::value(&g_enable_parallel_window_partition_compute) + ->default_value(g_enable_parallel_window_partition_compute) + ->implicit_value(true), + "Enable parallel window function partition computation."); + desc.add_options()("enable-parallel-window-partition-sort", + po::value(&g_enable_parallel_window_partition_sort) + ->default_value(g_enable_parallel_window_partition_sort) + ->implicit_value(true), + "Enable parallel window function partition sorting."); + desc.add_options()( "window-function-frame-aggregation-tree-fanout", po::value(&g_window_function_aggregation_tree_fanout)->default_value(8), "A tree fanout for aggregation tree used to compute aggregation over " "window frame"); - developer_desc.add_options()("enable-dev-table-functions", - po::value(&g_enable_dev_table_functions) - ->default_value(g_enable_dev_table_functions) - ->implicit_value(true), - "Enable dev (test or alpha) table functions. Also " - "requires --enable-table-functions to be turned on"); - - developer_desc.add_options()( - "enable-geo-ops-on-uncompressed-coords", - po::value(&g_enable_geo_ops_on_uncompressed_coords) - ->default_value(g_enable_geo_ops_on_uncompressed_coords) - ->implicit_value(true), - "Enable faster geo operations on uncompressed coords"); - developer_desc.add_options()( + desc.add_options()("enable-dev-table-functions", + po::value(&g_enable_dev_table_functions) + ->default_value(g_enable_dev_table_functions) + ->implicit_value(true), + "Enable dev (test or alpha) table functions. Also " + "requires --enable-table-functions to be turned on"); + + desc.add_options()("enable-geo-ops-on-uncompressed-coords", + po::value(&g_enable_geo_ops_on_uncompressed_coords) + ->default_value(g_enable_geo_ops_on_uncompressed_coords) + ->implicit_value(true), + "Enable faster geo operations on uncompressed coords"); + desc.add_options()( "jit-debug-ir", po::value(&jit_debug)->default_value(jit_debug)->implicit_value(true), "Enable runtime debugger support for the JIT. Note that this flag is " "incompatible " "with the `ENABLE_JIT_DEBUG` build flag. The generated code can be found at " "`/tmp/mapdquery`."); - developer_desc.add_options()( + desc.add_options()( "intel-jit-profile", po::value(&intel_jit_profile) ->default_value(intel_jit_profile) ->implicit_value(true), "Enable runtime support for the JIT code profiling using Intel VTune."); - developer_desc.add_options()( + desc.add_options()( "enable-cpu-sub-tasks", po::value(&g_enable_cpu_sub_tasks) ->default_value(g_enable_cpu_sub_tasks) ->implicit_value(true), "Enable parallel processing of a single data fragment on CPU. This can improve CPU " "load balance and decrease reduction overhead."); - developer_desc.add_options()( + desc.add_options()( "cpu-sub-task-size", po::value(&g_cpu_sub_task_size)->default_value(g_cpu_sub_task_size), "Set CPU sub-task size in rows."); - developer_desc.add_options()( + desc.add_options()( "cpu-threads", po::value(&g_cpu_threads_override)->default_value(g_cpu_threads_override), "Set max CPU concurrent threads. Values <= 0 will use default of 2X the number of " "hardware threads."); - developer_desc.add_options()( + desc.add_options()( "skip-intermediate-count", po::value(&g_skip_intermediate_count) ->default_value(g_skip_intermediate_count) ->implicit_value(true), "Skip pre-flight counts for intermediate projections with no filters."); - developer_desc.add_options()( - "strip-join-covered-quals", - po::value(&g_strip_join_covered_quals) - ->default_value(g_strip_join_covered_quals) - ->implicit_value(true), - "Remove quals from the filtered count if they are covered by a " - "join condition (currently only ST_Contains)."); - - developer_desc.add_options()( - "min-cpu-slab-size", - po::value(&system_parameters.min_cpu_slab_size) - ->default_value(system_parameters.min_cpu_slab_size), - "Min slab size (size of memory allocations) for CPU buffer pool."); - developer_desc.add_options()( + desc.add_options()("strip-join-covered-quals", + po::value(&g_strip_join_covered_quals) + ->default_value(g_strip_join_covered_quals) + ->implicit_value(true), + "Remove quals from the filtered count if they are covered by a " + "join condition (currently only ST_Contains)."); + + desc.add_options()("min-cpu-slab-size", + po::value(&system_parameters.min_cpu_slab_size) + ->default_value(system_parameters.min_cpu_slab_size), + "Min slab size (size of memory allocations) for CPU buffer pool."); + desc.add_options()( "max-cpu-slab-size", po::value(&system_parameters.max_cpu_slab_size) ->default_value(system_parameters.max_cpu_slab_size), @@ -928,12 +908,11 @@ void CommandLineOptions::fillAdvancedOptions() { "there is not enough free memory to accomodate the target slab size, smaller " "slabs will be allocated, down to the minimum size specified by " "min-cpu-slab-size."); - developer_desc.add_options()( - "min-gpu-slab-size", - po::value(&system_parameters.min_gpu_slab_size) - ->default_value(system_parameters.min_gpu_slab_size), - "Min slab size (size of memory allocations) for GPU buffer pools."); - developer_desc.add_options()( + desc.add_options()("min-gpu-slab-size", + po::value(&system_parameters.min_gpu_slab_size) + ->default_value(system_parameters.min_gpu_slab_size), + "Min slab size (size of memory allocations) for GPU buffer pools."); + desc.add_options()( "max-gpu-slab-size", po::value(&system_parameters.max_gpu_slab_size) ->default_value(system_parameters.max_gpu_slab_size), @@ -942,7 +921,7 @@ void CommandLineOptions::fillAdvancedOptions() { "slabs will be allocated, down to the minimum size speified by " "min-gpu-slab-size."); - developer_desc.add_options()( + desc.add_options()( "max-output-projection-allocation-bytes", po::value(&g_max_memory_allocation_size) ->default_value(g_max_memory_allocation_size), @@ -950,7 +929,7 @@ void CommandLineOptions::fillAdvancedOptions() { "queries with no pre-flight count. Default is the maximum slab size (sizes " "greater " "than the maximum slab size have no affect). Requires bump allocator."); - developer_desc.add_options()( + desc.add_options()( "min-output-projection-allocation-bytes", po::value(&g_min_memory_allocation_size) ->default_value(g_min_memory_allocation_size), @@ -959,216 +938,214 @@ void CommandLineOptions::fillAdvancedOptions() { "obtained, the query will be retried with different execution parameters and/or " "on " "CPU (if allow-cpu-retry is enabled). Requires bump allocator."); - developer_desc.add_options()("enable-bump-allocator", - po::value(&g_enable_bump_allocator) - ->default_value(g_enable_bump_allocator) - ->implicit_value(true), - "Enable the bump allocator for projection queries on " - "GPU. The bump allocator will " - "allocate a fixed size buffer for each query, track the " - "number of rows passing the " - "kernel during query execution, and copy back only the " - "rows that passed the kernel " - "to CPU after execution. When disabled, pre-flight " - "count queries are used to size " - "the output buffer for projection queries."); - developer_desc.add_options()( + desc.add_options()("enable-bump-allocator", + po::value(&g_enable_bump_allocator) + ->default_value(g_enable_bump_allocator) + ->implicit_value(true), + "Enable the bump allocator for projection queries on " + "GPU. The bump allocator will " + "allocate a fixed size buffer for each query, track the " + "number of rows passing the " + "kernel during query execution, and copy back only the " + "rows that passed the kernel " + "to CPU after execution. When disabled, pre-flight " + "count queries are used to size " + "the output buffer for projection queries."); + desc.add_options()( "code-cache-eviction-percent", po::value(&g_fraction_code_cache_to_evict) ->default_value(g_fraction_code_cache_to_evict), "Percentage of the GPU code cache to evict if an out of memory error is " "encountered while attempting to place generated code on the GPU."); - developer_desc.add_options()("ssl-cert", - po::value(&system_parameters.ssl_cert_file) - ->default_value(std::string("")), - "SSL Validated public certficate."); + desc.add_options()("ssl-cert", + po::value(&system_parameters.ssl_cert_file) + ->default_value(std::string("")), + "SSL Validated public certficate."); - developer_desc.add_options()("ssl-private-key", - po::value(&system_parameters.ssl_key_file) - ->default_value(std::string("")), - "SSL private key file."); + desc.add_options()("ssl-private-key", + po::value(&system_parameters.ssl_key_file) + ->default_value(std::string("")), + "SSL private key file."); // Note ssl_trust_store is passed through to Calcite via system_parameters // todo(jack): add ensure ssl-trust-store exists if cert and private key in use - developer_desc.add_options()("ssl-trust-store", - po::value(&system_parameters.ssl_trust_store) - ->default_value(std::string("")), - "SSL public CA certifcates (java trust store) to validate " - "TLS connections (passed through to the Calcite server)."); + desc.add_options()("ssl-trust-store", + po::value(&system_parameters.ssl_trust_store) + ->default_value(std::string("")), + "SSL public CA certifcates (java trust store) to validate " + "TLS connections (passed through to the Calcite server)."); - developer_desc.add_options()( + desc.add_options()( "ssl-trust-password", po::value(&system_parameters.ssl_trust_password) ->default_value(std::string("")), "SSL password for java trust store provided via --ssl-trust-store parameter."); - developer_desc.add_options()( + desc.add_options()( "ssl-trust-ca", po::value(&system_parameters.ssl_trust_ca_file) ->default_value(std::string("")), "SSL public CA certificates to validate TLS connection(as a client)."); - developer_desc.add_options()( + desc.add_options()( "ssl-trust-ca-server", po::value(&authMetadata.ca_file_name)->default_value(std::string("")), "SSL public CA certificates to validate TLS connection(as a server)."); - developer_desc.add_options()("ssl-keystore", - po::value(&system_parameters.ssl_keystore) - ->default_value(std::string("")), - "SSL server credentials as a java key store (passed " - "through to the Calcite server)."); + desc.add_options()("ssl-keystore", + po::value(&system_parameters.ssl_keystore) + ->default_value(std::string("")), + "SSL server credentials as a java key store (passed " + "through to the Calcite server)."); - developer_desc.add_options()( - "ssl-keystore-password", - po::value(&system_parameters.ssl_keystore_password) - ->default_value(std::string("")), - "SSL password for java keystore, provide by via --ssl-keystore."); + desc.add_options()("ssl-keystore-password", + po::value(&system_parameters.ssl_keystore_password) + ->default_value(std::string("")), + "SSL password for java keystore, provide by via --ssl-keystore."); - developer_desc.add_options()( + desc.add_options()( "udf", po::value(&udf_file_name), "Load user defined extension functions from this file at startup. The file is " "expected to be a C/C++ file with extension .cpp."); - developer_desc.add_options()( - "udf-compiler-path", - po::value(&udf_compiler_path), - "Provide absolute path to clang++ used in udf compilation."); + desc.add_options()("udf-compiler-path", + po::value(&udf_compiler_path), + "Provide absolute path to clang++ used in udf compilation."); - developer_desc.add_options()("udf-compiler-options", - po::value>(&udf_compiler_options), - "Specify compiler options to tailor udf compilation."); + desc.add_options()("udf-compiler-options", + po::value>(&udf_compiler_options), + "Specify compiler options to tailor udf compilation."); #ifdef ENABLE_GEOS - developer_desc.add_options()("libgeos-so-filename", - po::value(&libgeos_so_filename), - "Specify libgeos shared object filename to be used for " - "geos-backed geo opertations."); + desc.add_options()("libgeos-so-filename", + po::value(&libgeos_so_filename), + "Specify libgeos shared object filename to be used for " + "geos-backed geo opertations."); #endif - developer_desc.add_options()( + desc.add_options()( "large-ndv-threshold", po::value(&g_large_ndv_threshold)->default_value(g_large_ndv_threshold)); - developer_desc.add_options()( + desc.add_options()( "large-ndv-multiplier", po::value(&g_large_ndv_multiplier)->default_value(g_large_ndv_multiplier)); - developer_desc.add_options()("approx_quantile_buffer", - po::value(&g_approx_quantile_buffer) - ->default_value(g_approx_quantile_buffer)); - developer_desc.add_options()("approx_quantile_centroids", - po::value(&g_approx_quantile_centroids) - ->default_value(g_approx_quantile_centroids)); - developer_desc.add_options()( + desc.add_options()("approx_quantile_buffer", + po::value(&g_approx_quantile_buffer) + ->default_value(g_approx_quantile_buffer)); + desc.add_options()("approx_quantile_centroids", + po::value(&g_approx_quantile_centroids) + ->default_value(g_approx_quantile_centroids)); + desc.add_options()( "bitmap-memory-limit", po::value(&g_bitmap_memory_limit)->default_value(g_bitmap_memory_limit), "Limit for count distinct bitmap memory use. The limit is computed by taking the " "size of the group by buffer (entry count in Query Memory Descriptor) and " "multiplying it by the number of count distinct expression and the size of bitmap " "required for each. For approx_count_distinct this is typically 8192 bytes."); - developer_desc.add_options()( + desc.add_options()( "enable-filter-function", po::value(&g_enable_filter_function) ->default_value(g_enable_filter_function) ->implicit_value(true), "Enable the filter function protection feature for the SQL JIT compiler. " "Normally should be on but techs might want to disable for troubleshooting."); - developer_desc.add_options()( + desc.add_options()( "enable-idp-temporary-users", po::value(&g_enable_idp_temporary_users) ->default_value(g_enable_idp_temporary_users) ->implicit_value(true), "Enable temporary users for SAML and LDAP logins on read-only servers. " "Normally should be on but techs might want to disable for troubleshooting."); - developer_desc.add_options()( + desc.add_options()( "enable-seconds-refresh-interval", po::value(&g_enable_seconds_refresh) ->default_value(g_enable_seconds_refresh) ->implicit_value(true), "Enable foreign table seconds refresh interval for testing purposes."); - developer_desc.add_options()("enable-auto-metadata-update", - po::value(&g_enable_auto_metadata_update) - ->default_value(g_enable_auto_metadata_update) - ->implicit_value(true), - "Enable automatic metadata update."); - developer_desc.add_options()( + desc.add_options()("enable-auto-metadata-update", + po::value(&g_enable_auto_metadata_update) + ->default_value(g_enable_auto_metadata_update) + ->implicit_value(true), + "Enable automatic metadata update."); + desc.add_options()( "parallel-top-min", po::value(&g_parallel_top_min)->default_value(g_parallel_top_min), "For ResultSets requiring a heap sort, the number of rows necessary to trigger " "parallelTop() to sort."); - developer_desc.add_options()( + desc.add_options()( "parallel-top-max", po::value(&g_parallel_top_max)->default_value(g_parallel_top_max), "For ResultSets requiring a heap sort, the maximum number of rows allowed by " "watchdog."); - developer_desc.add_options()( + desc.add_options()( "streaming-top-n-max", po::value(&g_streaming_topn_max)->default_value(g_streaming_topn_max), "The maximum number of rows allowing streaming top-N sorting."); - developer_desc.add_options()("vacuum-min-selectivity", - po::value(&g_vacuum_min_selectivity) - ->default_value(g_vacuum_min_selectivity), - "Minimum selectivity for automatic vacuuming. " - "This specifies the percentage (with a value of 0 " - "implying 0% and a value of 1 implying 100%) of " - "deleted rows in a fragment at which to perform " - "automatic vacuuming. A number greater than 1 can " - "be used to disable automatic vacuuming."); - developer_desc.add_options()("enable-automatic-ir-metadata", - po::value(&g_enable_automatic_ir_metadata) - ->default_value(g_enable_automatic_ir_metadata) - ->implicit_value(true), - "Enable automatic IR metadata (debug builds only)."); - developer_desc.add_options()( + desc.add_options()("vacuum-min-selectivity", + po::value(&g_vacuum_min_selectivity) + ->default_value(g_vacuum_min_selectivity), + "Minimum selectivity for automatic vacuuming. " + "This specifies the percentage (with a value of 0 " + "implying 0% and a value of 1 implying 100%) of " + "deleted rows in a fragment at which to perform " + "automatic vacuuming. A number greater than 1 can " + "be used to disable automatic vacuuming."); + desc.add_options()("enable-automatic-ir-metadata", + po::value(&g_enable_automatic_ir_metadata) + ->default_value(g_enable_automatic_ir_metadata) + ->implicit_value(true), + "Enable automatic IR metadata (debug builds only)."); + desc.add_options()( "max-log-length", po::value(&g_max_log_length)->default_value(g_max_log_length), "The maximum number of characters that a log message can has. If the log message " "is longer than this, we only record \'g_max_log_message_length\' characters."); - developer_desc.add_options()( + desc.add_options()( "estimator-failure-max-groupby-size", po::value(&g_estimator_failure_max_groupby_size) ->default_value(g_estimator_failure_max_groupby_size), "Maximum size of the groupby buffer if the estimator fails. By default we use the " "number of tuples in the table up to this value."); - developer_desc.add_options()("columnar-large-projections", - po::value(&g_columnar_large_projections) - ->default_value(g_columnar_large_projections) - ->implicit_value(true), - "Prefer columnar output if projection size is >= " - "threshold set by --columnar-large-projections-threshold " - "(default 1,000,000 rows)."); - developer_desc.add_options()( + desc.add_options()("columnar-large-projections", + po::value(&g_columnar_large_projections) + ->default_value(g_columnar_large_projections) + ->implicit_value(true), + "Prefer columnar output if projection size is >= " + "threshold set by --columnar-large-projections-threshold " + "(default 1,000,000 rows)."); + desc.add_options()( "columnar-large-projections-threshold", po::value(&g_columnar_large_projections_threshold) ->default_value(g_columnar_large_projections_threshold), "Threshold (in minimum number of rows) to prefer columnar output for projections. " "Requires --columnar-large-projections to be set."); - help_desc.add_options()( + desc.add_options()( "allow-query-step-cpu-retry", po::value(&g_allow_query_step_cpu_retry) ->default_value(g_allow_query_step_cpu_retry) ->implicit_value(true), R"(Allow certain query steps to retry on CPU, even when allow-cpu-retry is disabled)"); - help_desc.add_options()("enable-http-binary-server", - po::value(&g_enable_http_binary_server) - ->default_value(g_enable_http_binary_server) - ->implicit_value(true), - "Enable binary over HTTP Thrift server"); - - help_desc.add_options()("enable-query-engine-cuda-streams", - po::value(&g_query_engine_cuda_streams) - ->default_value(g_query_engine_cuda_streams) - ->implicit_value(true), - "Enable Query Engine CUDA streams"); - - help_desc.add_options()( + desc.add_options()("enable-http-binary-server", + po::value(&g_enable_http_binary_server) + ->default_value(g_enable_http_binary_server) + ->implicit_value(true), + "Enable binary over HTTP Thrift server"); + + desc.add_options()("enable-query-engine-cuda-streams", + po::value(&g_query_engine_cuda_streams) + ->default_value(g_query_engine_cuda_streams) + ->implicit_value(true), + "Enable Query Engine CUDA streams"); + + desc.add_options()( "allow-invalid-literal-buffer-reads", po::value(&g_allow_invalid_literal_buffer_reads) ->default_value(g_allow_invalid_literal_buffer_reads) ->implicit_value(true), "For backwards compatibility. Enabling may cause invalid query results."); - developer_desc.add_options()( + desc.add_options()( "enable-drop-render-group-columns-migration", po::value(&enable_drop_render_group_columns_migration) ->default_value(false) @@ -1607,7 +1584,7 @@ boost::optional CommandLineOptions::parse_command_line( char const* const* argv, const bool should_init_logging) { po::options_description all_desc("All options"); - all_desc.add(help_desc).add(developer_desc); + all_desc.add(help_desc_).add(developer_desc_); try { po::store(po::command_line_parser(argc, argv) @@ -1622,7 +1599,7 @@ boost::optional CommandLineOptions::parse_command_line( "[--http-port ] [--flush-log] [--version|-v]" << std::endl << std::endl; - std::cout << help_desc << std::endl; + std::cout << help_desc_ << std::endl; return 0; } if (vm.count("dev-options")) { @@ -1630,7 +1607,7 @@ boost::optional CommandLineOptions::parse_command_line( "[--http-port ] [--flush-log] [--version|-v]" << std::endl << std::endl; - std::cout << developer_desc << std::endl; + std::cout << developer_desc_ << std::endl; return 0; } if (vm.count("version")) { diff --git a/ThriftHandler/CommandLineOptions.h b/ThriftHandler/CommandLineOptions.h index 1b145d1e9e..8203ca572b 100644 --- a/ThriftHandler/CommandLineOptions.h +++ b/ThriftHandler/CommandLineOptions.h @@ -42,7 +42,7 @@ class CommandLineOptions { CommandLineOptions(char const* argv0, bool dist_v5_ = false) : log_options_(argv0), exe_name(argv0), dist_v5_(dist_v5_) { fillOptions(); - fillAdvancedOptions(); + fillDeveloperOptions(); } int http_port = 6278; int http_binary_port = 6276; @@ -126,12 +126,12 @@ class CommandLineOptions { bool enable_drop_render_group_columns_migration = false; void fillOptions(); - void fillAdvancedOptions(); + void fillDeveloperOptions(); std::string compressor = std::string(BLOSC_LZ4HC_COMPNAME); - po::options_description help_desc; - po::options_description developer_desc; + po::options_description help_desc_; + po::options_description developer_desc_; logger::LogOptions log_options_; std::string exe_name; po::positional_options_description positional_options; From 0f632be0989eabdd04cd81a45f03ca3cf1c1a07b Mon Sep 17 00:00:00 2001 From: yoonminnam <53632385+yoonminnam@users.noreply.github.com> Date: Tue, 17 Jan 2023 18:56:04 -0800 Subject: [PATCH 7/7] Server crashes when query rewriter analyzes `ST_DISTANCE` operation having constant point object (#7149) * Fixup the logic related to the invalid qual checking * Add tests * Fixup col_id assignment logic for the col having multiple rte_idxs * Address comment #1: assert test queries' result * improve function's readability * Use std::optional instead of hard-coded invalid val Signed-off-by: jack --- QueryEngine/Descriptors/InputDescriptors.h | 8 +++++ QueryEngine/ExpressionRewrite.cpp | 4 +-- QueryEngine/PlanState.cpp | 42 ++++++++++++++++++---- Tests/OverlapsJoinTest.cpp | 30 ++++++++++++++++ 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/QueryEngine/Descriptors/InputDescriptors.h b/QueryEngine/Descriptors/InputDescriptors.h index ceca307d2d..34aa935a5b 100644 --- a/QueryEngine/Descriptors/InputDescriptors.h +++ b/QueryEngine/Descriptors/InputDescriptors.h @@ -69,6 +69,14 @@ class InputColDescriptor final { const InputDescriptor& getScanDesc() const { return input_desc_; } + shared::TableKey getTableKey() const { + return shared::TableKey{input_desc_.getTableKey()}; + } + + shared::ColumnKey getColumnKey() const { + return shared::ColumnKey{getTableKey(), col_id_}; + } + size_t hash() const { return input_desc_.hash() ^ (static_cast(col_id_) << 16); } diff --git a/QueryEngine/ExpressionRewrite.cpp b/QueryEngine/ExpressionRewrite.cpp index 1aba6e4c71..a85c276c0f 100644 --- a/QueryEngine/ExpressionRewrite.cpp +++ b/QueryEngine/ExpressionRewrite.cpp @@ -870,11 +870,9 @@ OverlapsJoinTranslationResult translate_overlaps_conjunction_with_reordering( // ordering for overlaps, the join builder will fail. std::set lhs_rte_idx; lhs->collect_rte_idx(lhs_rte_idx); - CHECK(!lhs_rte_idx.empty()); std::set rhs_rte_idx; rhs->collect_rte_idx(rhs_rte_idx); - CHECK(!rhs_rte_idx.empty()); - auto has_invalid_num_join_cols = lhs_rte_idx.size() > 1 || rhs_rte_idx.size() > 1; + auto has_invalid_num_join_cols = lhs_rte_idx.size() != 1 || rhs_rte_idx.size() != 1; auto has_invalid_rte_idx = lhs_rte_idx > rhs_rte_idx; return std::make_pair(has_invalid_num_join_cols || has_invalid_rte_idx, has_invalid_rte_idx); diff --git a/QueryEngine/PlanState.cpp b/QueryEngine/PlanState.cpp index 56d0ec6b48..aaf15b8956 100644 --- a/QueryEngine/PlanState.cpp +++ b/QueryEngine/PlanState.cpp @@ -67,17 +67,45 @@ void PlanState::allocateLocalColumnIds( int PlanState::getLocalColumnId(const Analyzer::ColumnVar* col_var, const bool fetch_column) { + // Previously, we consider `rte_idx` of `col_var` w/ its column key together + // to specify columns in the `global_to_local_col_ids_`. + // But there is a case when the same col has multiple 'rte_idx's + // For instance, the same geometry col is used not only as input col of the geo join op, + // but also included as input col of filter predicate + // In such a case, the same geometry col has two rte_idxs (the one defined by the filter + // predicate and the other determined by the geo join operator) + // The previous logic cannot cover this case b/c it allows only one `rte_idx` per col + // But it is safe to share `rte_idx` of among all use cases of the same col CHECK(col_var); const auto& global_col_key = col_var->getColumnKey(); - const int scan_idx = col_var->get_rte_idx(); - InputColDescriptor scan_col_desc( - global_col_key.column_id, global_col_key.table_id, global_col_key.db_id, scan_idx); + InputColDescriptor scan_col_desc(global_col_key.column_id, + global_col_key.table_id, + global_col_key.db_id, + col_var->get_rte_idx()); + std::optional col_id{std::nullopt}; + // let's try to find col_id w/ considering `rte_idx` const auto it = global_to_local_col_ids_.find(scan_col_desc); - CHECK(it != global_to_local_col_ids_.end()) << "Expected to find " << scan_col_desc; - if (fetch_column) { - columns_to_fetch_.insert(global_col_key); + if (it != global_to_local_col_ids_.end()) { + // we have a valid col_id + col_id = it->second; + } else { + // otherwise, let's try to find col_id for the same col + // (but have different 'rte_idx') to share it w/ `col_var` + for (auto const& kv : global_to_local_col_ids_) { + if (kv.first.getColumnKey() == global_col_key) { + col_id = kv.second; + break; + } + } + } + if (col_id && *col_id >= 0) { + if (fetch_column) { + columns_to_fetch_.insert(global_col_key); + } + return *col_id; } - return it->second; + CHECK(false) << "Expected to find " << global_col_key; + return {}; } void PlanState::addNonHashtableQualForLeftJoin(size_t idx, diff --git a/Tests/OverlapsJoinTest.cpp b/Tests/OverlapsJoinTest.cpp index b12fddbb96..de1cc96b65 100644 --- a/Tests/OverlapsJoinTest.cpp +++ b/Tests/OverlapsJoinTest.cpp @@ -2338,6 +2338,36 @@ TEST_F(OverlapsJoinRewriteTest, ArgumentOrderingAfterTableReordering) { } } +TEST_F(OverlapsJoinRewriteTest, ArgumentReorderingNonPointCol) { + // test logic is different compared with the previous test, + // so we do not use performTest function here + QR::get()->clearCpuMemory(); + g_enable_distance_rangejoin = true; + ScopeGuard reset_flag = [orig = g_from_table_reordering] { + g_from_table_reordering = orig; + }; + auto q1 = + "SELECT COUNT(*) FROM TEST_GEOPT R, TEST_GEOPT2 S WHERE ST_DISTANCE( " + "ST_GeomFromText('POINT(-87.653800724 41.839365536)', 4326), R.pt4326 ) <= 1.0 AND " + "ST_DISTANCE(R.pt4326, S.pt4326) < 0.01;"; + auto q2 = + "SELECT COUNT(*) FROM TEST_GEOPT R, TEST_GEOPT2 S WHERE ST_DISTANCE( S.pt4326, " + "ST_GeomFromText('POINT(-87.653800724 41.839365536)', 4326) ) <= 1.0 AND " + "ST_DISTANCE(R.pt4326, S.pt4326) < 0.01;"; + auto q3 = + "SELECT COUNT(*) FROM TEST_GEOPT R, TEST_GEOPT2 S WHERE ST_DISTANCE( R.pt4326, " + "ST_GeomFromText('POINT(-87.653800724 41.839365536)', 4326) ) <= 1.0 AND " + "ST_DISTANCE( S.pt4326, ST_GeomFromText('POINT(-87.653800724 41.839365536)', 4326) " + ") <= 1.0;"; + for (bool const table_reordering : {true, false}) { + g_from_table_reordering = table_reordering; + // check whether the query finishes without a crash + EXPECT_EQ((int64_t)0, v(execSQL(q1, ExecutorDeviceType::CPU))); + EXPECT_EQ((int64_t)0, v(execSQL(q2, ExecutorDeviceType::CPU))); + EXPECT_EQ((int64_t)0, v(execSQL(q3, ExecutorDeviceType::CPU))); + } +} + TEST_F(OverlapsJoinRewriteTest, TemporaryTable) { QR::get()->runDDLStatement("DROP TABLE IF EXISTS tp1;"); QR::get()->runDDLStatement("DROP TABLE IF EXISTS tp2;");