diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e4a73176cb9..24ca161f0d83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1065,7 +1065,7 @@ endfunction() macro(register_external_extension NAME URL COMMIT DONT_LINK DONT_BUILD LOAD_TESTS PATH INCLUDE_PATH TEST_PATH APPLY_PATCHES LINKED_LIBS SUBMODULES EXTENSION_VERSION) include(FetchContent) if (${APPLY_PATCHES}) - set(PATCH_COMMAND python3 ${CMAKE_SOURCE_DIR}/scripts/apply_extension_patches.py ${CMAKE_SOURCE_DIR}/.github/patches/extensions/${NAME}/) + set(PATCH_COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/apply_extension_patches.py ${CMAKE_SOURCE_DIR}/.github/patches/extensions/${NAME}/) endif() FETCHCONTENT_DECLARE( ${NAME}_extension_fc @@ -1389,7 +1389,7 @@ if(${EXTENSION_CONFIG_BUILD}) add_custom_target( duckdb_merge_vcpkg_manifests ALL - COMMAND python3 scripts/merge_vcpkg_deps.py ${VCPKG_PATHS} ${EXT_NAMES} + COMMAND ${Python3_EXECUTABLE} scripts/merge_vcpkg_deps.py ${VCPKG_PATHS} ${EXT_NAMES} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT Generates a shared vcpkg manifest from the individual extensions) string(REPLACE ";" ", " VCPKG_NAMES_COMMAS "${VCPKG_NAMES}") @@ -1420,6 +1420,15 @@ if(BUILD_PYTHON) set(ALL_COMPILE_FLAGS "${CMAKE_CXX_FLAGS}") endif() + # Check for MSVC compiler and set the correct C++ standard flag + if(MSVC) + # MSVC does not support `-std=c++11` or `-std=c++14`, use `/std:c++14` + set(ALL_COMPILE_FLAGS "${ALL_COMPILE_FLAGS} /std:c++14") + else() + # For non-MSVC compilers, use the `-std=c++11` + set(ALL_COMPILE_FLAGS "${ALL_COMPILE_FLAGS} -std=c++11") + endif() + get_target_property(duckdb_libs duckdb LINK_LIBRARIES) set(PIP_COMMAND @@ -1432,9 +1441,9 @@ if(BUILD_PYTHON) ) if(PYTHON_EDITABLE_BUILD) - set(PIP_COMMAND ${PIP_COMMAND} python3 -m pip install --editable .) + set(PIP_COMMAND ${PIP_COMMAND} ${Python3_EXECUTABLE} -m pip install --editable .) else() - set(PIP_COMMAND ${PIP_COMMAND} python3 -m pip install .) + set(PIP_COMMAND ${PIP_COMMAND} ${Python3_EXECUTABLE} -m pip install .) endif() if(USER_SPACE) diff --git a/benchmark/parquet/clickbench_write.benchmark b/benchmark/parquet/clickbench_write.benchmark new file mode 100644 index 000000000000..2a4f3bc6340e --- /dev/null +++ b/benchmark/parquet/clickbench_write.benchmark @@ -0,0 +1,23 @@ +# name: benchmark/parquet/clickbench_write.benchmark +# description: Write ClickBench data to Parquet +# group: [parquet] + +require httpfs + +require parquet + +name ClickBench Write Parquet +group Clickbench + +cache clickbench.duckdb + +load benchmark/clickbench/queries/load.sql + +init +set preserve_insertion_order=false; + +run +COPY hits TO '${BENCHMARK_DIR}/hits.parquet'; + +result I +10000000 diff --git a/data/csv/afl/20250211_csv_fuzz_crash/case_53.csv b/data/csv/afl/20250211_csv_fuzz_crash/case_53.csv new file mode 100644 index 000000000000..869b03087ab2 Binary files /dev/null and b/data/csv/afl/20250211_csv_fuzz_crash/case_53.csv differ diff --git a/data/csv/afl/4172/case_4.csv b/data/csv/afl/4172/case_4.csv new file mode 100644 index 000000000000..c3c65cce9923 Binary files /dev/null and b/data/csv/afl/4172/case_4.csv differ diff --git a/data/csv/flights.csv b/data/csv/flights.csv new file mode 100644 index 000000000000..7e8e451da4d1 --- /dev/null +++ b/data/csv/flights.csv @@ -0,0 +1,4 @@ +FlightDate|UniqueCarrier|OriginCityName|DestCityName +1988-01-01|AA|New York, NY|Los Angeles, CA +1988-01-02|AA|New York, NY|Los Angeles, CA +1988-01-03|AA|New York, NY|Los Angeles, CA diff --git a/extension/core_functions/aggregate/distributive/arg_min_max.cpp b/extension/core_functions/aggregate/distributive/arg_min_max.cpp index 63c112b3ce3c..edb6c77c5371 100644 --- a/extension/core_functions/aggregate/distributive/arg_min_max.cpp +++ b/extension/core_functions/aggregate/distributive/arg_min_max.cpp @@ -545,8 +545,8 @@ class ArgMinMaxNState { BinaryAggregateHeap heap; bool is_initialized = false; - void Initialize(idx_t nval) { - heap.Initialize(nval); + void Initialize(ArenaAllocator &allocator, idx_t nval) { + heap.Initialize(allocator, nval); is_initialized = true; } }; @@ -601,7 +601,7 @@ static void ArgMinMaxNUpdate(Vector inputs[], AggregateInputData &aggr_input, id if (nval >= MAX_N) { throw InvalidInputException("Invalid input for arg_min/arg_max: n value must be < %d", MAX_N); } - state.Initialize(UnsafeNumericCast(nval)); + state.Initialize(aggr_input.allocator, UnsafeNumericCast(nval)); } // Now add the input to the heap diff --git a/extension/core_functions/scalar/generic/least.cpp b/extension/core_functions/scalar/generic/least.cpp index 40a943101f94..886e909ca557 100644 --- a/extension/core_functions/scalar/generic/least.cpp +++ b/extension/core_functions/scalar/generic/least.cpp @@ -2,6 +2,7 @@ #include "core_functions/scalar/generic_functions.hpp" #include "duckdb/function/create_sort_key.hpp" #include "duckdb/planner/expression/bound_function_expression.hpp" +#include "duckdb/planner/expression_binder.hpp" namespace duckdb { diff --git a/extension/json/include/json_scan.hpp b/extension/json/include/json_scan.hpp index 4fd7bc0a0768..0504c21b8542 100644 --- a/extension/json/include/json_scan.hpp +++ b/extension/json/include/json_scan.hpp @@ -360,6 +360,8 @@ struct JSONScan { const TableFunction &function); static unique_ptr Deserialize(Deserializer &deserializer, TableFunction &function); + static virtual_column_map_t GetVirtualColumns(ClientContext &context, optional_ptr bind_data); + static void TableFunctionDefaults(TableFunction &table_function); }; diff --git a/extension/json/json_scan.cpp b/extension/json/json_scan.cpp index 6fd732a0b102..11777fc3eea2 100644 --- a/extension/json/json_scan.cpp +++ b/extension/json/json_scan.cpp @@ -171,7 +171,7 @@ unique_ptr JSONGlobalTableFunctionState::Init(ClientCo const auto &col_id = input.column_ids[col_idx]; // Skip any multi-file reader / row id stuff - if (col_id == bind_data.reader_bind.filename_idx || IsRowIdColumnId(col_id)) { + if (col_id == bind_data.reader_bind.filename_idx || IsVirtualColumn(col_id)) { continue; } bool skip = false; @@ -1025,6 +1025,14 @@ unique_ptr JSONScan::Deserialize(Deserializer &deserializer, Table return std::move(result); } +virtual_column_map_t JSONScan::GetVirtualColumns(ClientContext &context, optional_ptr bind_data) { + auto &csv_bind = bind_data->Cast(); + virtual_column_map_t result; + MultiFileReader::GetVirtualColumns(context, csv_bind.reader_bind, result); + result.insert(make_pair(COLUMN_IDENTIFIER_EMPTY, TableColumn("", LogicalType::BOOLEAN))); + return result; +} + void JSONScan::TableFunctionDefaults(TableFunction &table_function) { MultiFileReader().AddParameters(table_function); @@ -1039,6 +1047,7 @@ void JSONScan::TableFunctionDefaults(TableFunction &table_function) { table_function.serialize = Serialize; table_function.deserialize = Deserialize; + table_function.get_virtual_columns = GetVirtualColumns; table_function.projection_pushdown = true; table_function.filter_pushdown = false; diff --git a/extension/parquet/column_reader.cpp b/extension/parquet/column_reader.cpp index 9e135e8b43ef..06f3b7a827a8 100644 --- a/extension/parquet/column_reader.cpp +++ b/extension/parquet/column_reader.cpp @@ -304,7 +304,8 @@ void ColumnReader::PreparePageV2(PageHeader &page_hdr) { auto compressed_bytes = page_hdr.compressed_page_size - uncompressed_bytes; - AllocateCompressed(compressed_bytes); + ResizeableBuffer compressed_buffer; + compressed_buffer.resize(GetAllocator(), compressed_bytes); reader.ReadData(*protocol, compressed_buffer.ptr, compressed_bytes); DecompressInternal(chunk->meta_data.codec, compressed_buffer.ptr, compressed_bytes, block->ptr + uncompressed_bytes, @@ -319,10 +320,6 @@ void ColumnReader::AllocateBlock(idx_t size) { } } -void ColumnReader::AllocateCompressed(idx_t size) { - compressed_buffer.resize(GetAllocator(), size); -} - void ColumnReader::PreparePage(PageHeader &page_hdr) { AllocateBlock(page_hdr.uncompressed_page_size + 1); if (chunk->meta_data.codec == CompressionCodec::UNCOMPRESSED) { @@ -333,7 +330,8 @@ void ColumnReader::PreparePage(PageHeader &page_hdr) { return; } - AllocateCompressed(page_hdr.compressed_page_size + 1); + ResizeableBuffer compressed_buffer; + compressed_buffer.resize(GetAllocator(), page_hdr.compressed_page_size + 1); reader.ReadData(*protocol, compressed_buffer.ptr, page_hdr.compressed_page_size); DecompressInternal(chunk->meta_data.codec, compressed_buffer.ptr, page_hdr.compressed_page_size, block->ptr, diff --git a/extension/parquet/column_writer.cpp b/extension/parquet/column_writer.cpp index 9693724d2c35..f774a8c681eb 100644 --- a/extension/parquet/column_writer.cpp +++ b/extension/parquet/column_writer.cpp @@ -91,7 +91,7 @@ ColumnWriterState::~ColumnWriterState() { } void ColumnWriter::CompressPage(MemoryStream &temp_writer, size_t &compressed_size, data_ptr_t &compressed_data, - unique_ptr &compressed_buf) { + AllocatedData &compressed_buf) { switch (writer.GetCodec()) { case CompressionCodec::UNCOMPRESSED: compressed_size = temp_writer.GetPosition(); @@ -100,7 +100,7 @@ void ColumnWriter::CompressPage(MemoryStream &temp_writer, size_t &compressed_si case CompressionCodec::SNAPPY: { compressed_size = duckdb_snappy::MaxCompressedLength(temp_writer.GetPosition()); - compressed_buf = unique_ptr(new data_t[compressed_size]); + compressed_buf = BufferAllocator::Get(writer.GetContext()).Allocate(compressed_size); duckdb_snappy::RawCompress(const_char_ptr_cast(temp_writer.GetData()), temp_writer.GetPosition(), char_ptr_cast(compressed_buf.get()), &compressed_size); compressed_data = compressed_buf.get(); @@ -109,7 +109,7 @@ void ColumnWriter::CompressPage(MemoryStream &temp_writer, size_t &compressed_si } case CompressionCodec::LZ4_RAW: { compressed_size = duckdb_lz4::LZ4_compressBound(UnsafeNumericCast(temp_writer.GetPosition())); - compressed_buf = unique_ptr(new data_t[compressed_size]); + compressed_buf = BufferAllocator::Get(writer.GetContext()).Allocate(compressed_size); compressed_size = duckdb_lz4::LZ4_compress_default( const_char_ptr_cast(temp_writer.GetData()), char_ptr_cast(compressed_buf.get()), UnsafeNumericCast(temp_writer.GetPosition()), UnsafeNumericCast(compressed_size)); @@ -119,7 +119,7 @@ void ColumnWriter::CompressPage(MemoryStream &temp_writer, size_t &compressed_si case CompressionCodec::GZIP: { MiniZStream s; compressed_size = s.MaxCompressedLength(temp_writer.GetPosition()); - compressed_buf = unique_ptr(new data_t[compressed_size]); + compressed_buf = BufferAllocator::Get(writer.GetContext()).Allocate(compressed_size); s.Compress(const_char_ptr_cast(temp_writer.GetData()), temp_writer.GetPosition(), char_ptr_cast(compressed_buf.get()), &compressed_size); compressed_data = compressed_buf.get(); @@ -127,7 +127,7 @@ void ColumnWriter::CompressPage(MemoryStream &temp_writer, size_t &compressed_si } case CompressionCodec::ZSTD: { compressed_size = duckdb_zstd::ZSTD_compressBound(temp_writer.GetPosition()); - compressed_buf = unique_ptr(new data_t[compressed_size]); + compressed_buf = BufferAllocator::Get(writer.GetContext()).Allocate(compressed_size); compressed_size = duckdb_zstd::ZSTD_compress((void *)compressed_buf.get(), compressed_size, (const void *)temp_writer.GetData(), temp_writer.GetPosition(), UnsafeNumericCast(writer.CompressionLevel())); @@ -135,15 +135,12 @@ void ColumnWriter::CompressPage(MemoryStream &temp_writer, size_t &compressed_si break; } case CompressionCodec::BROTLI: { - compressed_size = duckdb_brotli::BrotliEncoderMaxCompressedSize(temp_writer.GetPosition()); - compressed_buf = unique_ptr(new data_t[compressed_size]); - + compressed_buf = BufferAllocator::Get(writer.GetContext()).Allocate(compressed_size); duckdb_brotli::BrotliEncoderCompress(BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, temp_writer.GetPosition(), temp_writer.GetData(), &compressed_size, compressed_buf.get()); compressed_data = compressed_buf.get(); - break; } default: @@ -176,6 +173,7 @@ void ColumnWriter::HandleDefineLevels(ColumnWriterState &state, ColumnWriterStat idx_t current_index = state.definition_levels.size(); if (parent->definition_levels[current_index] != PARQUET_DEFINE_VALID) { state.definition_levels.push_back(parent->definition_levels[current_index]); + state.parent_null_count++; } else if (validity.RowIsValid(vector_index)) { state.definition_levels.push_back(define_value); } else { @@ -191,10 +189,14 @@ void ColumnWriter::HandleDefineLevels(ColumnWriterState &state, ColumnWriterStat } } else { // no parent: set definition levels only from this validity mask - for (idx_t i = 0; i < count; i++) { - const auto is_null = !validity.RowIsValid(i); - state.definition_levels.emplace_back(is_null ? null_value : define_value); - state.null_count += is_null; + if (validity.AllValid()) { + state.definition_levels.insert(state.definition_levels.end(), count, define_value); + } else { + for (idx_t i = 0; i < count; i++) { + const auto is_null = !validity.RowIsValid(i); + state.definition_levels.emplace_back(is_null ? null_value : define_value); + state.null_count += is_null; + } } if (!can_have_nulls && state.null_count != 0) { throw IOException("Parquet writer: map key column is not allowed to contain NULL values"); @@ -207,10 +209,10 @@ void ColumnWriter::HandleDefineLevels(ColumnWriterState &state, ColumnWriterStat //===--------------------------------------------------------------------===// // Used to store the metadata for a WKB-encoded geometry column when writing // GeoParquet files. -class WKBColumnWriterState final : public StandardColumnWriterState { +class WKBColumnWriterState final : public StandardColumnWriterState { public: - WKBColumnWriterState(ClientContext &context, duckdb_parquet::RowGroup &row_group, idx_t col_idx) - : StandardColumnWriterState(row_group, col_idx), geo_data(), geo_data_writer(context) { + WKBColumnWriterState(ParquetWriter &writer, duckdb_parquet::RowGroup &row_group, idx_t col_idx) + : StandardColumnWriterState(writer, row_group, col_idx), geo_data(), geo_data_writer(writer.GetContext()) { } GeoParquetColumnMetadata geo_data; @@ -219,16 +221,16 @@ class WKBColumnWriterState final : public StandardColumnWriterState { class WKBColumnWriter final : public StandardColumnWriter { public: - WKBColumnWriter(ClientContext &context_p, ParquetWriter &writer, idx_t schema_idx, vector schema_path_p, - idx_t max_repeat, idx_t max_define, bool can_have_nulls, string name) + WKBColumnWriter(ParquetWriter &writer, idx_t schema_idx, vector schema_path_p, idx_t max_repeat, + idx_t max_define, bool can_have_nulls, string name) : StandardColumnWriter(writer, schema_idx, std::move(schema_path_p), max_repeat, max_define, can_have_nulls), - column_name(std::move(name)), context(context_p) { + column_name(std::move(name)) { this->writer.GetGeoParquetData().RegisterGeometryColumn(column_name); } unique_ptr InitializeWriteState(duckdb_parquet::RowGroup &row_group) override { - auto result = make_uniq(context, row_group, row_group.columns.size()); + auto result = make_uniq(writer, row_group, row_group.columns.size()); result->encoding = Encoding::RLE_DICTIONARY; RegisterToRowGroup(row_group); return std::move(result); @@ -253,7 +255,6 @@ class WKBColumnWriter final : public StandardColumnWriter ColumnWriter::CreateWriterRecursive(ClientContext &cont schema_path.push_back(name); if (type.id() == LogicalTypeId::BLOB && type.GetAlias() == "WKB_BLOB" && GeoParquetFileMetadata::IsGeoParquetConversionEnabled(context)) { - return make_uniq(context, writer, schema_idx, std::move(schema_path), max_repeat, max_define, + return make_uniq(writer, schema_idx, std::move(schema_path), max_repeat, max_define, can_have_nulls, name); } @@ -584,41 +596,30 @@ struct NumericLimits { } }; -} // namespace duckdb - -namespace std { template <> -struct hash { - size_t operator()(const duckdb::ParquetIntervalTargetType &val) const { - return duckdb::Hash(duckdb::const_char_ptr_cast(val.bytes), - duckdb::ParquetIntervalTargetType::PARQUET_INTERVAL_SIZE); - } -}; +hash_t Hash(ParquetIntervalTargetType val) { + return Hash(const_char_ptr_cast(val.bytes), ParquetIntervalTargetType::PARQUET_INTERVAL_SIZE); +} template <> -struct hash { - size_t operator()(const duckdb::ParquetUUIDTargetType &val) const { - return duckdb::Hash(duckdb::const_char_ptr_cast(val.bytes), duckdb::ParquetUUIDTargetType::PARQUET_UUID_SIZE); - } -}; +hash_t Hash(ParquetUUIDTargetType val) { + return Hash(const_char_ptr_cast(val.bytes), ParquetUUIDTargetType::PARQUET_UUID_SIZE); +} template <> -struct hash { - size_t operator()(const duckdb::float_na_equal &val) const { - if (std::isnan(val.val)) { - return duckdb::Hash(std::numeric_limits::quiet_NaN()); - } - return duckdb::Hash(val.val); +hash_t Hash(float_na_equal val) { + if (std::isnan(val.val)) { + return Hash(std::numeric_limits::quiet_NaN()); } -}; + return Hash(val.val); +} template <> -struct hash { - inline size_t operator()(const duckdb::double_na_equal &val) const { - if (std::isnan(val.val)) { - return duckdb::Hash(std::numeric_limits::quiet_NaN()); - } - return duckdb::Hash(val.val); +hash_t Hash(double_na_equal val) { + if (std::isnan(val.val)) { + return Hash(std::numeric_limits::quiet_NaN()); } -}; -} // namespace std + return Hash(val.val); +} + +} // namespace duckdb diff --git a/extension/parquet/include/column_reader.hpp b/extension/parquet/include/column_reader.hpp index 0e77e2a51a84..35544de8e900 100644 --- a/extension/parquet/include/column_reader.hpp +++ b/extension/parquet/include/column_reader.hpp @@ -289,7 +289,6 @@ class ColumnReader { private: void AllocateBlock(idx_t size); - void AllocateCompressed(idx_t size); void PrepareRead(optional_ptr filter); void PreparePage(PageHeader &page_hdr); void PrepareDataPage(PageHeader &page_hdr); @@ -305,8 +304,6 @@ class ColumnReader { shared_ptr block; - ResizeableBuffer compressed_buffer; - ColumnEncoding encoding = ColumnEncoding::INVALID; unique_ptr defined_decoder; unique_ptr repeated_decoder; diff --git a/extension/parquet/include/column_writer.hpp b/extension/parquet/include/column_writer.hpp index dba6788e7cec..09f17c7eeb37 100644 --- a/extension/parquet/include/column_writer.hpp +++ b/extension/parquet/include/column_writer.hpp @@ -27,6 +27,7 @@ class ColumnWriterState { unsafe_vector definition_levels; unsafe_vector repetition_levels; vector is_empty; + idx_t parent_null_count = 0; idx_t null_count = 0; public: @@ -112,7 +113,7 @@ class ColumnWriter { void HandleRepeatLevels(ColumnWriterState &state_p, ColumnWriterState *parent, idx_t count, idx_t max_repeat) const; void CompressPage(MemoryStream &temp_writer, size_t &compressed_size, data_ptr_t &compressed_data, - unique_ptr &compressed_buf); + AllocatedData &compressed_buf); }; } // namespace duckdb diff --git a/extension/parquet/include/parquet_bss_encoder.hpp b/extension/parquet/include/parquet_bss_encoder.hpp index 49b1ab05b4ae..78f75a0c7572 100644 --- a/extension/parquet/include/parquet_bss_encoder.hpp +++ b/extension/parquet/include/parquet_bss_encoder.hpp @@ -30,7 +30,6 @@ class BssEncoder { } void FinishWrite(WriteStream &writer) { - D_ASSERT(count == total_value_count); writer.WriteData(buffer.get(), total_value_count * bit_width); } diff --git a/extension/parquet/include/parquet_dlba_encoder.hpp b/extension/parquet/include/parquet_dlba_encoder.hpp index 5e39c5e1fea2..897dc9c5fead 100644 --- a/extension/parquet/include/parquet_dlba_encoder.hpp +++ b/extension/parquet/include/parquet_dlba_encoder.hpp @@ -33,9 +33,8 @@ class DlbaEncoder { } void FinishWrite(WriteStream &writer) { - D_ASSERT(stream->GetPosition() == total_string_size); dbp_encoder.FinishWrite(writer); - writer.WriteData(buffer.get(), total_string_size); + writer.WriteData(buffer.get(), stream->GetPosition()); } private: @@ -69,7 +68,7 @@ void WriteValue(DlbaEncoder &encoder, WriteStream &writer, const string_t &value // helpers to get size from strings template -static idx_t GetDlbaStringSize(const SRC &src_value) { +static idx_t GetDlbaStringSize(const SRC &) { return 0; } diff --git a/extension/parquet/include/parquet_rle_bp_encoder.hpp b/extension/parquet/include/parquet_rle_bp_encoder.hpp index af321c160c17..f29c85edcf79 100644 --- a/extension/parquet/include/parquet_rle_bp_encoder.hpp +++ b/extension/parquet/include/parquet_rle_bp_encoder.hpp @@ -8,103 +8,142 @@ #pragma once -#include "parquet_types.h" -#include "thrift_tools.hpp" -#include "resizable_buffer.hpp" +#include "decode_utils.hpp" namespace duckdb { class RleBpEncoder { public: - explicit RleBpEncoder(uint32_t bit_width) - : byte_width((bit_width + 7) / 8), byte_count(idx_t(-1)), run_count(idx_t(-1)) { + explicit RleBpEncoder(uint32_t bit_width_p) : bit_width(bit_width_p), byte_width((bit_width + 7) / 8) { } public: - //! NOTE: Prepare is only required if a byte count is required BEFORE writing - //! This is the case with e.g. writing repetition/definition levels - //! If GetByteCount() is not required, prepare can be safely skipped - void BeginPrepare(uint32_t first_value) { - byte_count = 0; - run_count = 1; - current_run_count = 1; - last_value = first_value; + void BeginWrite() { + rle_count = 0; + bp_block_count = 0; } - void PrepareValue(uint32_t value) { - if (value != last_value) { - FinishRun(); - last_value = value; - } else { - current_run_count++; + + void WriteValue(WriteStream &writer, const uint32_t &value) { + if (bp_block_count != 0) { + // We already committed to a BP run + D_ASSERT(rle_count == 0); + bp_block[bp_block_count++] = value; + if (bp_block_count == BP_BLOCK_SIZE) { + WriteRun(writer); + } + return; } - } - void FinishPrepare() { - FinishRun(); - } - void BeginWrite(WriteStream &writer, uint32_t first_value) { - // start the RLE runs - last_value = first_value; - current_run_count = 1; - } - void WriteValue(WriteStream &writer, uint32_t value) { - if (value != last_value) { + if (rle_count == 0) { + // Starting fresh, try for an RLE run first + rle_value = value; + rle_count = 1; + return; + } + + // We're trying for an RLE run + if (rle_value == value) { + // Same as current RLE value + rle_count++; + return; + } + + // Value differs from current RLE value + if (rle_count >= MINIMUM_RLE_COUNT) { + // We have enough values for an RLE run WriteRun(writer); - last_value = value; - } else { - current_run_count++; + rle_value = value; + rle_count = 1; + return; + } + + // Not enough values, convert and commit to a BP run + D_ASSERT(bp_block_count == 0); + for (idx_t i = 0; i < rle_count; i++) { + bp_block[bp_block_count++] = rle_value; } + bp_block[bp_block_count++] = value; + rle_count = 0; } - void FinishWrite(WriteStream &writer) { - WriteRun(writer); + + void WriteMany(WriteStream &writer, uint32_t value, idx_t count) { + if (rle_count != 0) { + // If an RLE run is going on, write a single value to either finish it or convert to BP + WriteValue(writer, value); + count--; + } + + if (bp_block_count != 0) { + // If a BP run is going on, finish it + while (bp_block_count != 0 && count > 0) { + WriteValue(writer, value); + count--; + } + } + + // Set remaining as current RLE run + rle_value = value; + rle_count += count; } - idx_t GetByteCount() { - D_ASSERT(byte_count != idx_t(-1)); - return byte_count; + void FinishWrite(WriteStream &writer) { + WriteRun(writer); } private: - //! meta information + //! Meta information + uint32_t bit_width; uint32_t byte_width; - //! RLE run information - idx_t byte_count; - idx_t run_count; - idx_t current_run_count; - uint32_t last_value; + + //! RLE stuff + static constexpr idx_t MINIMUM_RLE_COUNT = 4; + uint32_t rle_value; + idx_t rle_count; + + //! BP stuff + static constexpr idx_t BP_BLOCK_SIZE = BitpackingPrimitives::BITPACKING_ALGORITHM_GROUP_SIZE; + uint32_t bp_block[BP_BLOCK_SIZE] = {0}; + uint32_t bp_block_packed[BP_BLOCK_SIZE] = {0}; + idx_t bp_block_count; private: - void FinishRun() { - // last value, or value has changed - // write out the current run - byte_count += ParquetDecodeUtils::GetVarintSize(current_run_count << 1) + byte_width; - current_run_count = 1; - run_count++; - } void WriteRun(WriteStream &writer) { - // write the header of the run - ParquetDecodeUtils::VarintEncode(current_run_count << 1, writer); - // now write the value - D_ASSERT(last_value >> (byte_width * 8) == 0); + if (rle_count != 0) { + WriteCurrentBlockRLE(writer); + } else { + WriteCurrentBlockBP(writer); + } + } + + void WriteCurrentBlockRLE(WriteStream &writer) { + ParquetDecodeUtils::VarintEncode(rle_count << 1 | 0, writer); // (... | 0) signals RLE run + D_ASSERT(rle_value >> (byte_width * 8) == 0); switch (byte_width) { case 1: - writer.Write(last_value); + writer.Write(rle_value); break; case 2: - writer.Write(last_value); + writer.Write(rle_value); break; case 3: - writer.Write(last_value & 0xFF); - writer.Write((last_value >> 8) & 0xFF); - writer.Write((last_value >> 16) & 0xFF); + writer.Write(rle_value & 0xFF); + writer.Write((rle_value >> 8) & 0xFF); + writer.Write((rle_value >> 16) & 0xFF); break; case 4: - writer.Write(last_value); + writer.Write(rle_value); break; default: throw InternalException("unsupported byte width for RLE encoding"); } - current_run_count = 1; + rle_count = 0; + } + + void WriteCurrentBlockBP(WriteStream &writer) { + ParquetDecodeUtils::VarintEncode(BP_BLOCK_SIZE / 8 << 1 | 1, writer); // (... | 1) signals BP run + ParquetDecodeUtils::BitPackAligned(bp_block, data_ptr_cast(bp_block_packed), BP_BLOCK_SIZE, bit_width); + writer.WriteData(data_ptr_cast(bp_block_packed), BP_BLOCK_SIZE * bit_width / 8); + bp_block_count = 0; } }; diff --git a/extension/parquet/include/parquet_writer.hpp b/extension/parquet/include/parquet_writer.hpp index 1ad586489067..b16a43fab8cb 100644 --- a/extension/parquet/include/parquet_writer.hpp +++ b/extension/parquet/include/parquet_writer.hpp @@ -36,7 +36,6 @@ class Deserializer; struct PreparedRowGroup { duckdb_parquet::RowGroup row_group; vector> states; - vector> heaps; }; struct FieldID; @@ -79,8 +78,8 @@ class ParquetWriter { vector names, duckdb_parquet::CompressionCodec::type codec, ChildFieldIDs field_ids, const vector> &kv_metadata, shared_ptr encryption_config, idx_t dictionary_size_limit, - double bloom_filter_false_positive_ratio, int64_t compression_level, bool debug_use_openssl, - ParquetVersion parquet_version); + idx_t string_dictionary_page_size_limit, double bloom_filter_false_positive_ratio, + int64_t compression_level, bool debug_use_openssl, ParquetVersion parquet_version); public: void PrepareRowGroup(ColumnDataCollection &buffer, PreparedRowGroup &result); @@ -116,6 +115,9 @@ class ParquetWriter { idx_t DictionarySizeLimit() const { return dictionary_size_limit; } + idx_t StringDictionaryPageSizeLimit() const { + return string_dictionary_page_size_limit; + } double BloomFilterFalsePositiveRatio() const { return bloom_filter_false_positive_ratio; } @@ -149,6 +151,7 @@ class ParquetWriter { ChildFieldIDs field_ids; shared_ptr encryption_config; idx_t dictionary_size_limit; + idx_t string_dictionary_page_size_limit; double bloom_filter_false_positive_ratio; int64_t compression_level; bool debug_use_openssl; diff --git a/extension/parquet/include/writer/boolean_column_writer.hpp b/extension/parquet/include/writer/boolean_column_writer.hpp index f513f15a578f..6c650cb0308f 100644 --- a/extension/parquet/include/writer/boolean_column_writer.hpp +++ b/extension/parquet/include/writer/boolean_column_writer.hpp @@ -24,7 +24,7 @@ class BooleanColumnWriter : public PrimitiveColumnWriter { void WriteVector(WriteStream &temp_writer, ColumnWriterStatistics *stats_p, ColumnWriterPageState *state_p, Vector &input_column, idx_t chunk_start, idx_t chunk_end) override; - unique_ptr InitializePageState(PrimitiveColumnWriterState &state) override; + unique_ptr InitializePageState(PrimitiveColumnWriterState &state, idx_t page_idx) override; void FlushPageState(WriteStream &temp_writer, ColumnWriterPageState *state_p) override; idx_t GetRowSize(const Vector &vector, const idx_t index, const PrimitiveColumnWriterState &state) const override; diff --git a/extension/parquet/include/writer/enum_column_writer.hpp b/extension/parquet/include/writer/enum_column_writer.hpp index 724bfab6def6..ab4772eb23a1 100644 --- a/extension/parquet/include/writer/enum_column_writer.hpp +++ b/extension/parquet/include/writer/enum_column_writer.hpp @@ -28,7 +28,7 @@ class EnumColumnWriter : public PrimitiveColumnWriter { void WriteVector(WriteStream &temp_writer, ColumnWriterStatistics *stats_p, ColumnWriterPageState *page_state_p, Vector &input_column, idx_t chunk_start, idx_t chunk_end) override; - unique_ptr InitializePageState(PrimitiveColumnWriterState &state) override; + unique_ptr InitializePageState(PrimitiveColumnWriterState &state, idx_t page_idx) override; void FlushPageState(WriteStream &temp_writer, ColumnWriterPageState *state_p) override; diff --git a/extension/parquet/include/writer/parquet_write_operators.hpp b/extension/parquet/include/writer/parquet_write_operators.hpp index d63acba17e80..8bef3067a05e 100644 --- a/extension/parquet/include/writer/parquet_write_operators.hpp +++ b/extension/parquet/include/writer/parquet_write_operators.hpp @@ -20,6 +20,11 @@ struct BaseParquetOperator { ser.WriteData(const_data_ptr_cast(&input), sizeof(TGT)); } + template + static constexpr idx_t WriteSize(const TGT &input) { + return sizeof(TGT); + } + template static uint64_t XXHash64(const TGT &target_value) { return duckdb_zstd::XXH64(&target_value, sizeof(target_value), 0); @@ -99,6 +104,11 @@ struct ParquetStringOperator : public BaseParquetOperator { ser.WriteData(const_data_ptr_cast(target_value.GetData()), target_value.GetSize()); } + template + static idx_t WriteSize(const TGT &target_value) { + return sizeof(uint32_t) + target_value.GetSize(); + } + template static uint64_t XXHash64(const TGT &target_value) { return duckdb_zstd::XXH64(target_value.GetData(), target_value.GetSize(), 0); @@ -118,7 +128,6 @@ struct ParquetIntervalTargetType { struct ParquetIntervalOperator : public BaseParquetOperator { template static TGT Operation(SRC input) { - if (input.days < 0 || input.months < 0 || input.micros < 0) { throw IOException("Parquet files do not support negative intervals"); } @@ -134,6 +143,11 @@ struct ParquetIntervalOperator : public BaseParquetOperator { ser.WriteData(target_value.bytes, ParquetIntervalTargetType::PARQUET_INTERVAL_SIZE); } + template + static constexpr idx_t WriteSize(const TGT &target_value) { + return ParquetIntervalTargetType::PARQUET_INTERVAL_SIZE; + } + template static uint64_t XXHash64(const TGT &target_value) { return duckdb_zstd::XXH64(target_value.bytes, ParquetIntervalTargetType::PARQUET_INTERVAL_SIZE, 0); @@ -167,6 +181,11 @@ struct ParquetUUIDOperator : public BaseParquetOperator { ser.WriteData(target_value.bytes, ParquetUUIDTargetType::PARQUET_UUID_SIZE); } + template + static constexpr idx_t WriteSize(const TGT &target_value) { + return ParquetUUIDTargetType::PARQUET_UUID_SIZE; + } + template static uint64_t XXHash64(const TGT &target_value) { return duckdb_zstd::XXH64(target_value.bytes, ParquetUUIDTargetType::PARQUET_UUID_SIZE, 0); diff --git a/extension/parquet/include/writer/primitive_column_writer.hpp b/extension/parquet/include/writer/primitive_column_writer.hpp index 0a97064e918a..f3bea0323b1f 100644 --- a/extension/parquet/include/writer/primitive_column_writer.hpp +++ b/extension/parquet/include/writer/primitive_column_writer.hpp @@ -31,17 +31,18 @@ struct PageWriteInformation { idx_t max_write_count = 0; size_t compressed_size; data_ptr_t compressed_data; - unique_ptr compressed_buf; + AllocatedData compressed_buf; }; class PrimitiveColumnWriterState : public ColumnWriterState { public: - PrimitiveColumnWriterState(duckdb_parquet::RowGroup &row_group, idx_t col_idx) - : row_group(row_group), col_idx(col_idx) { + PrimitiveColumnWriterState(ParquetWriter &writer_p, duckdb_parquet::RowGroup &row_group, idx_t col_idx) + : writer(writer_p), row_group(row_group), col_idx(col_idx) { page_info.emplace_back(); } ~PrimitiveColumnWriterState() override = default; + ParquetWriter &writer; duckdb_parquet::RowGroup &row_group; idx_t col_idx; vector page_info; @@ -81,7 +82,7 @@ class PrimitiveColumnWriter : public ColumnWriter { protected: static void WriteLevels(WriteStream &temp_writer, const unsafe_vector &levels, idx_t max_value, - idx_t start_offset, idx_t count); + idx_t start_offset, idx_t count, optional_idx null_count = optional_idx()); virtual duckdb_parquet::Encoding::type GetEncoding(PrimitiveColumnWriterState &state); @@ -92,7 +93,7 @@ class PrimitiveColumnWriter : public ColumnWriter { virtual unique_ptr InitializeStatsState(); //! Initialize the writer for a specific page. Only used for scalar types. - virtual unique_ptr InitializePageState(PrimitiveColumnWriterState &state); + virtual unique_ptr InitializePageState(PrimitiveColumnWriterState &state, idx_t page_idx); //! Flushes the writer for a specific page. Only used for scalar types. virtual void FlushPageState(WriteStream &temp_writer, ColumnWriterPageState *state); diff --git a/extension/parquet/include/writer/templated_column_writer.hpp b/extension/parquet/include/writer/templated_column_writer.hpp index a4324a70b48a..7bf1fa205f9b 100644 --- a/extension/parquet/include/writer/templated_column_writer.hpp +++ b/extension/parquet/include/writer/templated_column_writer.hpp @@ -13,29 +13,63 @@ #include "parquet_dbp_encoder.hpp" #include "parquet_dlba_encoder.hpp" #include "parquet_rle_bp_encoder.hpp" +#include "duckdb/common/primitive_dictionary.hpp" namespace duckdb { -template +template static void TemplatedWritePlain(Vector &col, ColumnWriterStatistics *stats, const idx_t chunk_start, const idx_t chunk_end, const ValidityMask &mask, WriteStream &ser) { + static constexpr bool COPY_DIRECTLY_FROM_VECTOR = + ALL_VALID && std::is_same::value && std::is_arithmetic::value; + + const auto *const ptr = FlatVector::GetData(col); + + TGT local_write[STANDARD_VECTOR_SIZE]; + idx_t local_write_count = 0; - const auto *ptr = FlatVector::GetData(col); for (idx_t r = chunk_start; r < chunk_end; r++) { - if (!mask.RowIsValid(r)) { + if (!ALL_VALID && !mask.RowIsValid(r)) { continue; } + TGT target_value = OP::template Operation(ptr[r]); OP::template HandleStats(stats, target_value); - OP::template WriteToStream(target_value, ser); + + if (COPY_DIRECTLY_FROM_VECTOR) { + continue; + } + + if (std::is_arithmetic::value) { + local_write[local_write_count++] = target_value; + if (local_write_count == STANDARD_VECTOR_SIZE) { + ser.WriteData(data_ptr_cast(local_write), local_write_count * sizeof(TGT)); + local_write_count = 0; + } + } else { + OP::template WriteToStream(target_value, ser); + } + } + + if (COPY_DIRECTLY_FROM_VECTOR) { + ser.WriteData(const_data_ptr_cast(&ptr[chunk_start]), (chunk_end - chunk_start) * sizeof(TGT)); + return; + } + + if (std::is_arithmetic::value) { + ser.WriteData(data_ptr_cast(local_write), local_write_count * sizeof(TGT)); } + // Else we already wrote to stream } -template +template class StandardColumnWriterState : public PrimitiveColumnWriterState { public: - StandardColumnWriterState(duckdb_parquet::RowGroup &row_group, idx_t col_idx) - : PrimitiveColumnWriterState(row_group, col_idx) { + StandardColumnWriterState(ParquetWriter &writer, duckdb_parquet::RowGroup &row_group, idx_t col_idx) + : PrimitiveColumnWriterState(writer, row_group, col_idx), + dictionary(BufferAllocator::Get(writer.GetContext()), writer.DictionarySizeLimit(), + writer.StringDictionaryPageSizeLimit()), + encoding(duckdb_parquet::Encoding::PLAIN) { } ~StandardColumnWriterState() override = default; @@ -44,20 +78,20 @@ class StandardColumnWriterState : public PrimitiveColumnWriterState { idx_t total_string_size = 0; uint32_t key_bit_width = 0; - unordered_map dictionary; + PrimitiveDictionary dictionary; duckdb_parquet::Encoding::type encoding; }; -template +template class StandardWriterPageState : public ColumnWriterPageState { public: explicit StandardWriterPageState(const idx_t total_value_count, const idx_t total_string_size, duckdb_parquet::Encoding::type encoding_p, - const unordered_map &dictionary_p) + const PrimitiveDictionary &dictionary_p) : encoding(encoding_p), dbp_initialized(false), dbp_encoder(total_value_count), dlba_initialized(false), dlba_encoder(total_value_count, total_string_size), bss_encoder(total_value_count, sizeof(TGT)), dictionary(dictionary_p), dict_written_value(false), - dict_bit_width(RleBpDecoder::ComputeBitWidth(dictionary.size())), dict_encoder(dict_bit_width) { + dict_bit_width(RleBpDecoder::ComputeBitWidth(dictionary.GetSize())), dict_encoder(dict_bit_width) { } duckdb_parquet::Encoding::type encoding; @@ -69,7 +103,7 @@ class StandardWriterPageState : public ColumnWriterPageState { BssEncoder bss_encoder; - const unordered_map &dictionary; + const PrimitiveDictionary &dictionary; bool dict_written_value; uint32_t dict_bit_width; RleBpEncoder dict_encoder; @@ -86,22 +120,23 @@ class StandardColumnWriter : public PrimitiveColumnWriter { public: unique_ptr InitializeWriteState(duckdb_parquet::RowGroup &row_group) override { - auto result = make_uniq>(row_group, row_group.columns.size()); + auto result = make_uniq>(writer, row_group, row_group.columns.size()); result->encoding = duckdb_parquet::Encoding::RLE_DICTIONARY; RegisterToRowGroup(row_group); return std::move(result); } - unique_ptr InitializePageState(PrimitiveColumnWriterState &state_p) override { - auto &state = state_p.Cast>(); - - auto result = make_uniq>(state.total_value_count, state.total_string_size, - state.encoding, state.dictionary); + unique_ptr InitializePageState(PrimitiveColumnWriterState &state_p, + idx_t page_idx) override { + auto &state = state_p.Cast>(); + const auto &page_info = state_p.page_info[page_idx]; + auto result = make_uniq>( + page_info.row_count - page_info.empty_count, state.total_string_size, state.encoding, state.dictionary); return std::move(result); } void FlushPageState(WriteStream &temp_writer, ColumnWriterPageState *state_p) override { - auto &page_state = state_p->Cast>(); + auto &page_state = state_p->Cast>(); switch (page_state.encoding) { case duckdb_parquet::Encoding::DELTA_BINARY_PACKED: if (!page_state.dbp_initialized) { @@ -136,7 +171,7 @@ class StandardColumnWriter : public PrimitiveColumnWriter { } duckdb_parquet::Encoding::type GetEncoding(PrimitiveColumnWriterState &state_p) override { - auto &state = state_p.Cast>(); + auto &state = state_p.Cast>(); return state.encoding; } @@ -145,11 +180,10 @@ class StandardColumnWriter : public PrimitiveColumnWriter { } void Analyze(ColumnWriterState &state_p, ColumnWriterState *parent, Vector &vector, idx_t count) override { - auto &state = state_p.Cast>(); + auto &state = state_p.Cast>(); auto data_ptr = FlatVector::GetData(vector); idx_t vector_index = 0; - uint32_t new_value_index = state.dictionary.size(); const bool check_parent_empty = parent && !parent->is_empty.empty(); const idx_t parent_index = state.definition_levels.size(); @@ -159,30 +193,35 @@ class StandardColumnWriter : public PrimitiveColumnWriter { const auto &validity = FlatVector::Validity(vector); - for (idx_t i = 0; i < vcount; i++) { - if (check_parent_empty && parent->is_empty[parent_index + i]) { - continue; - } - if (validity.RowIsValid(vector_index)) { + if (!check_parent_empty && validity.AllValid()) { + // Fast path + for (; vector_index < vcount; vector_index++) { const auto &src_value = data_ptr[vector_index]; - if (state.dictionary.size() <= writer.DictionarySizeLimit()) { - if (state.dictionary.find(src_value) == state.dictionary.end()) { - state.dictionary[src_value] = new_value_index; - new_value_index++; - } - } + state.dictionary.Insert(src_value); state.total_value_count++; state.total_string_size += dlba_encoder::GetDlbaStringSize(src_value); } - vector_index++; + } else { + for (idx_t i = 0; i < vcount; i++) { + if (check_parent_empty && parent->is_empty[parent_index + i]) { + continue; + } + if (validity.RowIsValid(vector_index)) { + const auto &src_value = data_ptr[vector_index]; + state.dictionary.Insert(src_value); + state.total_value_count++; + state.total_string_size += dlba_encoder::GetDlbaStringSize(src_value); + } + vector_index++; + } } } void FinalizeAnalyze(ColumnWriterState &state_p) override { const auto type = writer.GetType(schema_idx); - auto &state = state_p.Cast>(); - if (state.dictionary.size() == 0 || state.dictionary.size() > writer.DictionarySizeLimit()) { + auto &state = state_p.Cast>(); + if (state.dictionary.GetSize() == 0 || state.dictionary.IsFull()) { if (writer.GetParquetVersion() == ParquetVersion::V1) { // Can't do the cool stuff for V1 state.encoding = duckdb_parquet::Encoding::PLAIN; @@ -204,9 +243,8 @@ class StandardColumnWriter : public PrimitiveColumnWriter { state.encoding = duckdb_parquet::Encoding::PLAIN; } } - state.dictionary.clear(); } else { - state.key_bit_width = RleBpDecoder::ComputeBitWidth(state.dictionary.size()); + state.key_bit_width = RleBpDecoder::ComputeBitWidth(state.dictionary.GetSize()); } } @@ -215,41 +253,90 @@ class StandardColumnWriter : public PrimitiveColumnWriter { } bool HasDictionary(PrimitiveColumnWriterState &state_p) override { - auto &state = state_p.Cast>(); + auto &state = state_p.Cast>(); return state.encoding == duckdb_parquet::Encoding::RLE_DICTIONARY; } idx_t DictionarySize(PrimitiveColumnWriterState &state_p) override { - auto &state = state_p.Cast>(); - return state.dictionary.size(); + auto &state = state_p.Cast>(); + return state.dictionary.GetSize(); } void WriteVector(WriteStream &temp_writer, ColumnWriterStatistics *stats, ColumnWriterPageState *page_state_p, Vector &input_column, idx_t chunk_start, idx_t chunk_end) override { - auto &page_state = page_state_p->Cast>(); + const auto &mask = FlatVector::Validity(input_column); + if (mask.AllValid()) { + WriteVectorInternal(temp_writer, stats, page_state_p, input_column, chunk_start, chunk_end); + } else { + WriteVectorInternal(temp_writer, stats, page_state_p, input_column, chunk_start, chunk_end); + } + } + + void FlushDictionary(PrimitiveColumnWriterState &state_p, ColumnWriterStatistics *stats) override { + auto &state = state_p.Cast>(); + D_ASSERT(state.encoding == duckdb_parquet::Encoding::RLE_DICTIONARY); + + state.bloom_filter = + make_uniq(state.dictionary.GetSize(), writer.BloomFilterFalsePositiveRatio()); + + state.dictionary.IterateValues([&](const SRC &src_value, const TGT &tgt_value) { + // update the statistics + OP::template HandleStats(stats, tgt_value); + // update the bloom filter + auto hash = OP::template XXHash64(tgt_value); + state.bloom_filter->FilterInsert(hash); + }); + + // flush the dictionary page and add it to the to-be-written pages + WriteDictionary(state, state.dictionary.GetTargetMemoryStream(), state.dictionary.GetSize()); + // bloom filter will be queued for writing in ParquetWriter::BufferBloomFilter one level up + } + + idx_t GetRowSize(const Vector &vector, const idx_t index, + const PrimitiveColumnWriterState &state_p) const override { + auto &state = state_p.Cast>(); + if (state.encoding == duckdb_parquet::Encoding::RLE_DICTIONARY) { + return (state.key_bit_width + 7) / 8; + } else { + return OP::template GetRowSize(vector, index); + } + } + +private: + template + void WriteVectorInternal(WriteStream &temp_writer, ColumnWriterStatistics *stats, + ColumnWriterPageState *page_state_p, Vector &input_column, idx_t chunk_start, + idx_t chunk_end) { + auto &page_state = page_state_p->Cast>(); const auto &mask = FlatVector::Validity(input_column); const auto *data_ptr = FlatVector::GetData(input_column); switch (page_state.encoding) { case duckdb_parquet::Encoding::RLE_DICTIONARY: { - for (idx_t r = chunk_start; r < chunk_end; r++) { - if (!mask.RowIsValid(r)) { - continue; - } - auto &src_val = data_ptr[r]; - auto value_index = page_state.dictionary.at(src_val); - if (!page_state.dict_written_value) { - // first value - // write the bit-width as a one-byte entry + idx_t r = chunk_start; + if (!page_state.dict_written_value) { + // find first non-null value + for (; r < chunk_end; r++) { + if (!mask.RowIsValid(r)) { + continue; + } + // write the bit-width as a one-byte entry and initialize writer temp_writer.Write(page_state.dict_bit_width); - // now begin writing the actual value - page_state.dict_encoder.BeginWrite(temp_writer, value_index); + page_state.dict_encoder.BeginWrite(); page_state.dict_written_value = true; - } else { - page_state.dict_encoder.WriteValue(temp_writer, value_index); + break; } } + + for (; r < chunk_end; r++) { + if (!ALL_VALID && !mask.RowIsValid(r)) { + continue; + } + const auto &src_value = data_ptr[r]; + const auto value_index = page_state.dictionary.GetIndex(src_value); + page_state.dict_encoder.WriteValue(temp_writer, value_index); + } break; } case duckdb_parquet::Encoding::DELTA_BINARY_PACKED: { @@ -270,7 +357,7 @@ class StandardColumnWriter : public PrimitiveColumnWriter { } for (; r < chunk_end; r++) { - if (!mask.RowIsValid(r)) { + if (!ALL_VALID && !mask.RowIsValid(r)) { continue; } const TGT target_value = OP::template Operation(data_ptr[r]); @@ -297,7 +384,7 @@ class StandardColumnWriter : public PrimitiveColumnWriter { } for (; r < chunk_end; r++) { - if (!mask.RowIsValid(r)) { + if (!ALL_VALID && !mask.RowIsValid(r)) { continue; } const TGT target_value = OP::template Operation(data_ptr[r]); @@ -308,7 +395,7 @@ class StandardColumnWriter : public PrimitiveColumnWriter { } case duckdb_parquet::Encoding::BYTE_STREAM_SPLIT: { for (idx_t r = chunk_start; r < chunk_end; r++) { - if (!mask.RowIsValid(r)) { + if (!ALL_VALID && !mask.RowIsValid(r)) { continue; } const TGT target_value = OP::template Operation(data_ptr[r]); @@ -319,56 +406,18 @@ class StandardColumnWriter : public PrimitiveColumnWriter { } case duckdb_parquet::Encoding::PLAIN: { D_ASSERT(page_state.encoding == duckdb_parquet::Encoding::PLAIN); - TemplatedWritePlain(input_column, stats, chunk_start, chunk_end, mask, temp_writer); + if (mask.AllValid()) { + TemplatedWritePlain(input_column, stats, chunk_start, chunk_end, mask, temp_writer); + } else { + TemplatedWritePlain(input_column, stats, chunk_start, chunk_end, mask, + temp_writer); + } break; } default: throw InternalException("Unknown encoding"); } } - - void FlushDictionary(PrimitiveColumnWriterState &state_p, ColumnWriterStatistics *stats) override { - auto &state = state_p.Cast>(); - - D_ASSERT(state.encoding == duckdb_parquet::Encoding::RLE_DICTIONARY); - - // first we need to sort the values in index order - auto values = vector(state.dictionary.size()); - for (const auto &entry : state.dictionary) { - values[entry.second] = entry.first; - } - - state.bloom_filter = - make_uniq(state.dictionary.size(), writer.BloomFilterFalsePositiveRatio()); - - // first write the contents of the dictionary page to a temporary buffer - auto temp_writer = make_uniq( - Allocator::Get(writer.GetContext()), MaxValue(NextPowerOfTwo(state.dictionary.size() * sizeof(TGT)), - MemoryStream::DEFAULT_INITIAL_CAPACITY)); - for (idx_t r = 0; r < values.size(); r++) { - const TGT target_value = OP::template Operation(values[r]); - // update the statistics - OP::template HandleStats(stats, target_value); - // update the bloom filter - auto hash = OP::template XXHash64(target_value); - state.bloom_filter->FilterInsert(hash); - // actually write the dictionary value - OP::template WriteToStream(target_value, *temp_writer); - } - // flush the dictionary page and add it to the to-be-written pages - WriteDictionary(state, std::move(temp_writer), values.size()); - // bloom filter will be queued for writing in ParquetWriter::BufferBloomFilter one level up - } - - idx_t GetRowSize(const Vector &vector, const idx_t index, - const PrimitiveColumnWriterState &state_p) const override { - auto &state = state_p.Cast>(); - if (state.encoding == duckdb_parquet::Encoding::RLE_DICTIONARY) { - return (state.key_bit_width + 7) / 8; - } else { - return OP::template GetRowSize(vector, index); - } - } }; } // namespace duckdb diff --git a/extension/parquet/parquet_extension.cpp b/extension/parquet/parquet_extension.cpp index f93b3f04acbe..ab0593b02179 100644 --- a/extension/parquet/parquet_extension.cpp +++ b/extension/parquet/parquet_extension.cpp @@ -49,6 +49,7 @@ #include "duckdb/planner/operator/logical_get.hpp" #include "duckdb/storage/statistics/base_statistics.hpp" #include "duckdb/storage/table/row_group.hpp" +#include "duckdb/common/primitive_dictionary.hpp" #endif namespace duckdb { @@ -61,6 +62,7 @@ struct ParquetReadBindData : public TableFunctionData { atomic chunk_count; vector names; vector types; + virtual_column_map_t virtual_columns; vector columns; //! Table column names - set when using COPY tbl FROM file.parquet vector table_columns; @@ -192,7 +194,14 @@ struct ParquetWriteBindData : public TableFunctionData { bool debug_use_openssl = true; //! After how many distinct values should we abandon dictionary compression and bloom filters? - idx_t dictionary_size_limit = row_group_size / 100; + idx_t dictionary_size_limit = row_group_size / 20; + + void SetToDefaultDictionarySizeLimit() { + // This depends on row group size so we should "reset" if the row group size is changed + dictionary_size_limit = row_group_size / 20; + } + + idx_t string_dictionary_page_size_limit = 1048576; //! What false positive rate are we willing to accept for bloom filters double bloom_filter_false_positive_ratio = 0.01; @@ -213,8 +222,8 @@ struct ParquetWriteGlobalState : public GlobalFunctionData { }; struct ParquetWriteLocalState : public LocalFunctionData { - explicit ParquetWriteLocalState(ClientContext &context, const vector &types) - : buffer(BufferAllocator::Get(context), types) { + explicit ParquetWriteLocalState(ClientContext &context, const vector &types) : buffer(context, types) { + buffer.SetPartitionIndex(0); // Makes the buffer manager less likely to spill this data buffer.InitializeAppend(append_state); } @@ -359,6 +368,14 @@ TablePartitionInfo ParquetGetPartitionInfo(ClientContext &context, TableFunction return parquet_bind.multi_file_reader->GetPartitionInfo(context, parquet_bind.reader_bind, input); } +virtual_column_map_t ParquetGetVirtualColumns(ClientContext &context, optional_ptr bind_data) { + auto &parquet_bind = bind_data->Cast(); + virtual_column_map_t result; + MultiFileReader::GetVirtualColumns(context, parquet_bind.reader_bind, result); + parquet_bind.virtual_columns = result; + return result; +} + class ParquetScanFunction { public: static TableFunctionSet GetFunctionSet() { @@ -384,6 +401,7 @@ class ParquetScanFunction { table_function.filter_prune = true; table_function.pushdown_complex_filter = ParquetComplexFilterPushdown; table_function.get_partition_info = ParquetGetPartitionInfo; + table_function.get_virtual_columns = ParquetGetVirtualColumns; MultiFileReader::AddParameters(table_function); @@ -431,7 +449,7 @@ class ParquetScanFunction { column_t column_index) { auto &bind_data = bind_data_p->Cast(); - if (IsRowIdColumnId(column_index)) { + if (IsVirtualColumn(column_index)) { return nullptr; } @@ -746,12 +764,17 @@ class ParquetScanFunction { iota(begin(result->projection_ids), end(result->projection_ids), 0); } - const auto table_types = bind_data.types; + const auto &table_types = bind_data.types; for (const auto &col_idx : input.column_indexes) { - if (col_idx.IsRowIdColumn()) { - result->scanned_types.emplace_back(LogicalType::ROW_TYPE); + auto column_id = col_idx.GetPrimaryIndex(); + if (col_idx.IsVirtualColumn()) { + auto entry = bind_data.virtual_columns.find(column_id); + if (entry == bind_data.virtual_columns.end()) { + throw InternalException("Parquet - virtual column definition not found"); + } + result->scanned_types.emplace_back(entry->second.type); } else { - result->scanned_types.push_back(table_types[col_idx.GetPrimaryIndex()]); + result->scanned_types.push_back(table_types[column_id]); } } } @@ -1185,6 +1208,7 @@ unique_ptr ParquetWriteBind(ClientContext &context, CopyFunctionBi D_ASSERT(names.size() == sql_types.size()); bool row_group_size_bytes_set = false; bool compression_level_set = false; + bool dictionary_size_limit_set = false; auto bind_data = make_uniq(); for (auto &option : input.info.options) { const auto loption = StringUtil::Lower(option.first); @@ -1194,6 +1218,9 @@ unique_ptr ParquetWriteBind(ClientContext &context, CopyFunctionBi } if (loption == "row_group_size" || loption == "chunk_size") { bind_data->row_group_size = option.second[0].GetValue(); + if (!dictionary_size_limit_set) { + bind_data->SetToDefaultDictionarySizeLimit(); + } } else if (loption == "row_group_size_bytes") { auto roption = option.second[0]; if (roption.GetTypeMutable().id() == LogicalTypeId::VARCHAR) { @@ -1269,6 +1296,14 @@ unique_ptr ParquetWriteBind(ClientContext &context, CopyFunctionBi throw BinderException("dictionary_size_limit must be greater than 0 or 0 to disable"); } bind_data->dictionary_size_limit = val; + dictionary_size_limit_set = true; + } else if (loption == "string_dictionary_page_size_limit") { + auto val = option.second[0].GetValue(); + if (val > PrimitiveDictionary::MAXIMUM_POSSIBLE_SIZE) { + throw BinderException("string_dictionary_page_size_limit must be less than or equal to %llu", + PrimitiveDictionary::MAXIMUM_POSSIBLE_SIZE); + } + bind_data->string_dictionary_page_size_limit = val; } else if (loption == "bloom_filter_false_positive_ratio") { auto val = option.second[0].GetValue(); if (val <= 0) { @@ -1331,8 +1366,9 @@ unique_ptr ParquetWriteInitializeGlobal(ClientContext &conte global_state->writer = make_uniq( context, fs, file_path, parquet_bind.sql_types, parquet_bind.column_names, parquet_bind.codec, parquet_bind.field_ids.Copy(), parquet_bind.kv_metadata, parquet_bind.encryption_config, - parquet_bind.dictionary_size_limit, parquet_bind.bloom_filter_false_positive_ratio, - parquet_bind.compression_level, parquet_bind.debug_use_openssl, parquet_bind.parquet_version); + parquet_bind.dictionary_size_limit, parquet_bind.string_dictionary_page_size_limit, + parquet_bind.bloom_filter_false_positive_ratio, parquet_bind.compression_level, parquet_bind.debug_use_openssl, + parquet_bind.parquet_version); return std::move(global_state); } @@ -1510,6 +1546,9 @@ static void ParquetCopySerialize(Serializer &serializer, const FunctionData &bin default_value.bloom_filter_false_positive_ratio); serializer.WritePropertyWithDefault(114, "parquet_version", bind_data.parquet_version, default_value.parquet_version); + serializer.WritePropertyWithDefault(115, "string_dictionary_page_size_limit", + bind_data.string_dictionary_page_size_limit, + default_value.string_dictionary_page_size_limit); } static unique_ptr ParquetCopyDeserialize(Deserializer &deserializer, CopyFunction &function) { @@ -1540,6 +1579,8 @@ static unique_ptr ParquetCopyDeserialize(Deserializer &deserialize 113, "bloom_filter_false_positive_ratio", default_value.bloom_filter_false_positive_ratio); data->parquet_version = deserializer.ReadPropertyWithExplicitDefault(114, "parquet_version", default_value.parquet_version); + data->string_dictionary_page_size_limit = deserializer.ReadPropertyWithExplicitDefault( + 115, "string_dictionary_page_size_limit", default_value.string_dictionary_page_size_limit); return std::move(data); } diff --git a/extension/parquet/parquet_writer.cpp b/extension/parquet/parquet_writer.cpp index 9977f67ecd27..4a8e38bba44b 100644 --- a/extension/parquet/parquet_writer.cpp +++ b/extension/parquet/parquet_writer.cpp @@ -320,11 +320,12 @@ ParquetWriter::ParquetWriter(ClientContext &context, FileSystem &fs, string file vector names_p, CompressionCodec::type codec, ChildFieldIDs field_ids_p, const vector> &kv_metadata, shared_ptr encryption_config_p, idx_t dictionary_size_limit_p, - double bloom_filter_false_positive_ratio_p, int64_t compression_level_p, - bool debug_use_openssl_p, ParquetVersion parquet_version) + idx_t string_dictionary_page_size_limit_p, double bloom_filter_false_positive_ratio_p, + int64_t compression_level_p, bool debug_use_openssl_p, ParquetVersion parquet_version) : context(context), file_name(std::move(file_name_p)), sql_types(std::move(types_p)), column_names(std::move(names_p)), codec(codec), field_ids(std::move(field_ids_p)), encryption_config(std::move(encryption_config_p)), dictionary_size_limit(dictionary_size_limit_p), + string_dictionary_page_size_limit(string_dictionary_page_size_limit_p), bloom_filter_false_positive_ratio(bloom_filter_false_positive_ratio_p), compression_level(compression_level_p), debug_use_openssl(debug_use_openssl_p), parquet_version(parquet_version) { @@ -388,9 +389,8 @@ void ParquetWriter::PrepareRowGroup(ColumnDataCollection &buffer, PreparedRowGro // We write 8 columns at a time so that iterating over ColumnDataCollection is more efficient static constexpr idx_t COLUMNS_PER_PASS = 8; - // We want these to be in-memory/hybrid so we don't have to copy over strings to the dictionary - D_ASSERT(buffer.GetAllocatorType() == ColumnDataAllocatorType::IN_MEMORY_ALLOCATOR || - buffer.GetAllocatorType() == ColumnDataAllocatorType::HYBRID); + // We want these to be buffer-managed + D_ASSERT(buffer.GetAllocatorType() == ColumnDataAllocatorType::BUFFER_MANAGER_ALLOCATOR); // set up a new row group for this chunk collection auto &row_group = result.row_group; @@ -450,7 +450,6 @@ void ParquetWriter::PrepareRowGroup(ColumnDataCollection &buffer, PreparedRowGro states.push_back(std::move(write_state)); } } - result.heaps = buffer.GetHeapReferences(); } // Validation code adapted from Impala @@ -508,8 +507,6 @@ void ParquetWriter::FlushRowGroup(PreparedRowGroup &prepared) { // append the row group to the file meta data file_meta_data.row_groups.push_back(row_group); file_meta_data.num_rows += row_group.num_rows; - - prepared.heaps.clear(); } void ParquetWriter::Flush(ColumnDataCollection &buffer) { diff --git a/extension/parquet/writer/boolean_column_writer.cpp b/extension/parquet/writer/boolean_column_writer.cpp index b7a3ee01856b..c9b0e96618a9 100644 --- a/extension/parquet/writer/boolean_column_writer.cpp +++ b/extension/parquet/writer/boolean_column_writer.cpp @@ -49,22 +49,33 @@ void BooleanColumnWriter::WriteVector(WriteStream &temp_writer, ColumnWriterStat idx_t chunk_end) { auto &stats = stats_p->Cast(); auto &state = state_p->Cast(); - auto &mask = FlatVector::Validity(input_column); - - auto *ptr = FlatVector::GetData(input_column); - for (idx_t r = chunk_start; r < chunk_end; r++) { - if (mask.RowIsValid(r)) { - // only encode if non-null - if (ptr[r]) { - stats.max = true; - state.byte |= 1 << state.byte_pos; - } else { - stats.min = false; + const auto &mask = FlatVector::Validity(input_column); + + const auto *const ptr = FlatVector::GetData(input_column); + if (stats.max && !stats.min && mask.AllValid()) { + // Fast path: stats have already been set, and there's no NULLs + for (idx_t r = chunk_start; r < chunk_end; r++) { + const auto &val = ptr[r]; + state.byte |= val << state.byte_pos; + if (++state.byte_pos == 8) { + temp_writer.Write(state.byte); + state.byte = 0; + state.byte_pos = 0; } - state.byte_pos++; + } + } else { + for (idx_t r = chunk_start; r < chunk_end; r++) { + if (!mask.RowIsValid(r)) { + continue; + } + const auto &val = ptr[r]; + + stats.max |= val; + stats.min &= val; + state.byte |= val << state.byte_pos; - if (state.byte_pos == 8) { - temp_writer.Write(state.byte); + if (++state.byte_pos == 8) { + temp_writer.Write(state.byte); state.byte = 0; state.byte_pos = 0; } @@ -72,7 +83,8 @@ void BooleanColumnWriter::WriteVector(WriteStream &temp_writer, ColumnWriterStat } } -unique_ptr BooleanColumnWriter::InitializePageState(PrimitiveColumnWriterState &state) { +unique_ptr BooleanColumnWriter::InitializePageState(PrimitiveColumnWriterState &state, + idx_t page_idx) { return make_uniq(); } diff --git a/extension/parquet/writer/enum_column_writer.cpp b/extension/parquet/writer/enum_column_writer.cpp index 51a2959cf36c..c6ae42827ada 100644 --- a/extension/parquet/writer/enum_column_writer.cpp +++ b/extension/parquet/writer/enum_column_writer.cpp @@ -36,15 +36,12 @@ void EnumColumnWriter::WriteEnumInternal(WriteStream &temp_writer, Vector &input for (idx_t r = chunk_start; r < chunk_end; r++) { if (mask.RowIsValid(r)) { if (!page_state.written_value) { - // first value - // write the bit-width as a one-byte entry + // first value: write the bit-width as a one-byte entry and initialize writer temp_writer.Write(bit_width); - // now begin writing the actual value - page_state.encoder.BeginWrite(temp_writer, ptr[r]); + page_state.encoder.BeginWrite(); page_state.written_value = true; - } else { - page_state.encoder.WriteValue(temp_writer, ptr[r]); } + page_state.encoder.WriteValue(temp_writer, ptr[r]); } } } @@ -68,7 +65,8 @@ void EnumColumnWriter::WriteVector(WriteStream &temp_writer, ColumnWriterStatist } } -unique_ptr EnumColumnWriter::InitializePageState(PrimitiveColumnWriterState &state) { +unique_ptr EnumColumnWriter::InitializePageState(PrimitiveColumnWriterState &state, + idx_t page_idx) { return make_uniq(bit_width); } diff --git a/extension/parquet/writer/primitive_column_writer.cpp b/extension/parquet/writer/primitive_column_writer.cpp index 627605fa23fa..e1f54ca88002 100644 --- a/extension/parquet/writer/primitive_column_writer.cpp +++ b/extension/parquet/writer/primitive_column_writer.cpp @@ -13,7 +13,7 @@ PrimitiveColumnWriter::PrimitiveColumnWriter(ParquetWriter &writer, idx_t schema } unique_ptr PrimitiveColumnWriter::InitializeWriteState(duckdb_parquet::RowGroup &row_group) { - auto result = make_uniq(row_group, row_group.columns.size()); + auto result = make_uniq(writer, row_group, row_group.columns.size()); RegisterToRowGroup(row_group); return std::move(result); } @@ -28,7 +28,8 @@ void PrimitiveColumnWriter::RegisterToRowGroup(duckdb_parquet::RowGroup &row_gro row_group.columns.push_back(std::move(column_chunk)); } -unique_ptr PrimitiveColumnWriter::InitializePageState(PrimitiveColumnWriterState &state) { +unique_ptr PrimitiveColumnWriter::InitializePageState(PrimitiveColumnWriterState &state, + idx_t page_idx) { return nullptr; } @@ -40,7 +41,6 @@ void PrimitiveColumnWriter::Prepare(ColumnWriterState &state_p, ColumnWriterStat auto &state = state_p.Cast(); auto &col_chunk = state.row_group.columns[state.col_idx]; - idx_t start = 0; idx_t vcount = parent ? parent->definition_levels.size() - state.definition_levels.size() : count; idx_t parent_index = state.definition_levels.size(); auto &validity = FlatVector::Validity(vector); @@ -49,24 +49,35 @@ void PrimitiveColumnWriter::Prepare(ColumnWriterState &state_p, ColumnWriterStat idx_t vector_index = 0; reference page_info_ref = state.page_info.back(); - for (idx_t i = start; i < vcount; i++) { + col_chunk.meta_data.num_values += vcount; + + const bool check_parent_empty = parent && !parent->is_empty.empty(); + if (!check_parent_empty && validity.AllValid() && TypeIsConstantSize(vector.GetType().InternalType()) && + page_info_ref.get().estimated_page_size + GetRowSize(vector, vector_index, state) * vcount < + MAX_UNCOMPRESSED_PAGE_SIZE) { + // Fast path: fixed-size type, all valid, and it fits on the current page auto &page_info = page_info_ref.get(); - page_info.row_count++; - col_chunk.meta_data.num_values++; - if (parent && !parent->is_empty.empty() && parent->is_empty[parent_index + i]) { - page_info.empty_count++; - continue; - } - if (validity.RowIsValid(vector_index)) { - page_info.estimated_page_size += GetRowSize(vector, vector_index, state); - if (page_info.estimated_page_size >= MAX_UNCOMPRESSED_PAGE_SIZE) { - PageInformation new_info; - new_info.offset = page_info.offset + page_info.row_count; - state.page_info.push_back(new_info); - page_info_ref = state.page_info.back(); + page_info.row_count += vcount; + page_info.estimated_page_size += GetRowSize(vector, vector_index, state) * vcount; + } else { + for (idx_t i = 0; i < vcount; i++) { + auto &page_info = page_info_ref.get(); + page_info.row_count++; + if (check_parent_empty && parent->is_empty[parent_index + i]) { + page_info.empty_count++; + continue; + } + if (validity.RowIsValid(vector_index)) { + page_info.estimated_page_size += GetRowSize(vector, vector_index, state); + if (page_info.estimated_page_size >= MAX_UNCOMPRESSED_PAGE_SIZE) { + PageInformation new_info; + new_info.offset = page_info.offset + page_info.row_count; + state.page_info.push_back(new_info); + page_info_ref = state.page_info.back(); + } } + vector_index++; } - vector_index++; } } @@ -104,7 +115,7 @@ void PrimitiveColumnWriter::BeginWrite(ColumnWriterState &state_p) { MaxValue(NextPowerOfTwo(page_info.estimated_page_size), MemoryStream::DEFAULT_INITIAL_CAPACITY)); write_info.write_count = page_info.empty_count; write_info.max_write_count = page_info.row_count; - write_info.page_state = InitializePageState(state); + write_info.page_state = InitializePageState(state, page_idx); write_info.compressed_size = 0; write_info.compressed_data = nullptr; @@ -117,28 +128,33 @@ void PrimitiveColumnWriter::BeginWrite(ColumnWriterState &state_p) { } void PrimitiveColumnWriter::WriteLevels(WriteStream &temp_writer, const unsafe_vector &levels, - idx_t max_value, idx_t offset, idx_t count) { + idx_t max_value, idx_t offset, idx_t count, optional_idx null_count) { if (levels.empty() || count == 0) { return; } // write the levels using the RLE-BP encoding - auto bit_width = RleBpDecoder::ComputeBitWidth((max_value)); + const auto bit_width = RleBpDecoder::ComputeBitWidth((max_value)); RleBpEncoder rle_encoder(bit_width); - rle_encoder.BeginPrepare(levels[offset]); - for (idx_t i = offset + 1; i < offset + count; i++) { - rle_encoder.PrepareValue(levels[i]); + // have to write to an intermediate stream first because we need to know the size + MemoryStream intermediate_stream(Allocator::DefaultAllocator()); + + rle_encoder.BeginWrite(); + if (null_count.IsValid() && null_count.GetIndex() == 0) { + // Fast path: no nulls + rle_encoder.WriteMany(intermediate_stream, levels[0], count); + } else { + for (idx_t i = offset; i < offset + count; i++) { + rle_encoder.WriteValue(intermediate_stream, levels[i]); + } } - rle_encoder.FinishPrepare(); + rle_encoder.FinishWrite(intermediate_stream); // start off by writing the byte count as a uint32_t - temp_writer.Write(rle_encoder.GetByteCount()); - rle_encoder.BeginWrite(temp_writer, levels[offset]); - for (idx_t i = offset + 1; i < offset + count; i++) { - rle_encoder.WriteValue(temp_writer, levels[i]); - } - rle_encoder.FinishWrite(temp_writer); + temp_writer.Write(NumericCast(intermediate_stream.GetPosition())); + // copy over the written data + temp_writer.WriteData(intermediate_stream.GetData(), intermediate_stream.GetPosition()); } void PrimitiveColumnWriter::NextPage(PrimitiveColumnWriterState &state) { @@ -160,7 +176,8 @@ void PrimitiveColumnWriter::NextPage(PrimitiveColumnWriterState &state) { WriteLevels(temp_writer, state.repetition_levels, max_repeat, page_info.offset, page_info.row_count); // write the definition levels - WriteLevels(temp_writer, state.definition_levels, max_define, page_info.offset, page_info.row_count); + WriteLevels(temp_writer, state.definition_levels, max_define, page_info.offset, page_info.row_count, + state.null_count + state.parent_null_count); } void PrimitiveColumnWriter::FlushPage(PrimitiveColumnWriterState &state) { diff --git a/extension/tpcds/dsdgen/include/dsdgen-c/porting.h b/extension/tpcds/dsdgen/include/dsdgen-c/porting.h index 6923a0f35286..cf27a036f813 100644 --- a/extension/tpcds/dsdgen/include/dsdgen-c/porting.h +++ b/extension/tpcds/dsdgen/include/dsdgen-c/porting.h @@ -57,7 +57,7 @@ #include -#ifdef WIN32 +#ifdef _WIN32 #include #define timeb _timeb #define ftime _ftime @@ -76,7 +76,7 @@ typedef HUGE_TYPE ds_key_t; char *strdup(const char *); #endif -#ifdef WIN32 +#ifdef _WIN32 #include #include #include diff --git a/extension/tpch/dbgen/text.cpp b/extension/tpch/dbgen/text.cpp index df67048b54ea..cfe9f8ef370b 100644 --- a/extension/tpch/dbgen/text.cpp +++ b/extension/tpch/dbgen/text.cpp @@ -22,10 +22,10 @@ #include "dbgen/config.h" #include -#ifndef WIN32 +#ifndef _WIN32 /* Change for Windows NT */ #include -#endif /* WIN32 */ +#endif /* _WIN32 */ #include #include #include diff --git a/scripts/amalgamation.py b/scripts/amalgamation.py index 325cc19f1521..1ba307278910 100644 --- a/scripts/amalgamation.py +++ b/scripts/amalgamation.py @@ -39,6 +39,7 @@ os.path.join(include_dir, 'duckdb', 'common', 'serializer', 'memory_stream.hpp'), os.path.join(include_dir, 'duckdb', 'main', 'appender.hpp'), os.path.join(include_dir, 'duckdb', 'main', 'client_context.hpp'), + os.path.join(include_dir, 'duckdb', 'main', 'extension_util.hpp'), os.path.join(include_dir, 'duckdb', 'function', 'function.hpp'), os.path.join(include_dir, 'duckdb', 'function', 'table_function.hpp'), os.path.join(include_dir, 'duckdb', 'parser', 'parsed_data', 'create_table_function_info.hpp'), diff --git a/src/catalog/catalog_entry/duck_table_entry.cpp b/src/catalog/catalog_entry/duck_table_entry.cpp index 4983710d9a99..d12bc557e7aa 100644 --- a/src/catalog/catalog_entry/duck_table_entry.cpp +++ b/src/catalog/catalog_entry/duck_table_entry.cpp @@ -885,7 +885,10 @@ void DuckTableEntry::CommitAlter(string &column_name) { break; } } - storage->CommitDropColumn(columns.LogicalToPhysical(LogicalIndex(removed_index.GetIndex())).index); + + auto logical_column_index = LogicalIndex(removed_index.GetIndex()); + auto column_index = columns.LogicalToPhysical(logical_column_index).index; + storage->CommitDropColumn(column_index); } void DuckTableEntry::CommitDrop() { diff --git a/src/catalog/catalog_entry/table_catalog_entry.cpp b/src/catalog/catalog_entry/table_catalog_entry.cpp index 3070b2e30d48..a7ca2fab69a3 100644 --- a/src/catalog/catalog_entry/table_catalog_entry.cpp +++ b/src/catalog/catalog_entry/table_catalog_entry.cpp @@ -332,4 +332,10 @@ bool TableCatalogEntry::HasPrimaryKey() const { return GetPrimaryKey() != nullptr; } +virtual_column_map_t TableCatalogEntry::GetVirtualColumns() const { + virtual_column_map_t virtual_columns; + virtual_columns.insert(make_pair(COLUMN_IDENTIFIER_ROW_ID, TableColumn("rowid", LogicalType::ROW_TYPE))); + return virtual_columns; +} + } // namespace duckdb diff --git a/src/common/adbc/adbc.cpp b/src/common/adbc/adbc.cpp index 35ceb2f3406f..17618c66ec55 100644 --- a/src/common/adbc/adbc.cpp +++ b/src/common/adbc/adbc.cpp @@ -875,8 +875,46 @@ AdbcStatusCode StatementSetSqlQuery(struct AdbcStatement *statement, const char duckdb_destroy_prepare(&wrapper->statement); wrapper->statement = nullptr; } - auto res = duckdb_prepare(wrapper->connection, query, &wrapper->statement); + duckdb_extracted_statements extracted_statements = nullptr; + auto extract_statements_size = duckdb_extract_statements(wrapper->connection, query, &extracted_statements); + auto error_msg_extract_statements = duckdb_extract_statements_error(extracted_statements); + if (error_msg_extract_statements != nullptr) { + // Things went wrong when executing internal prepared statement + duckdb_destroy_extracted(&extracted_statements); + SetError(error, error_msg_extract_statements); + return ADBC_STATUS_INTERNAL; + } + // Now lets loop over the statements, and execute every one + for (idx_t i = 0; i < extract_statements_size - 1; i++) { + duckdb_prepared_statement statement_internal = nullptr; + auto res = + duckdb_prepare_extracted_statement(wrapper->connection, extracted_statements, i, &statement_internal); + auto error_msg = duckdb_prepare_error(statement_internal); + auto adbc_status = CheckResult(res, error, error_msg); + if (adbc_status != ADBC_STATUS_OK) { + duckdb_destroy_prepare(&statement_internal); + duckdb_destroy_extracted(&extracted_statements); + return adbc_status; + } + // Execute + duckdb_arrow out_result; + res = duckdb_execute_prepared_arrow(statement_internal, &out_result); + if (res != DuckDBSuccess) { + SetError(error, duckdb_query_arrow_error(out_result)); + duckdb_destroy_arrow(&out_result); + duckdb_destroy_prepare(&statement_internal); + duckdb_destroy_extracted(&extracted_statements); + return ADBC_STATUS_INVALID_ARGUMENT; + } + duckdb_destroy_arrow(&out_result); + duckdb_destroy_prepare(&statement_internal); + } + + // Final statement (returned to caller) + auto res = duckdb_prepare_extracted_statement(wrapper->connection, extracted_statements, + extract_statements_size - 1, &wrapper->statement); auto error_msg = duckdb_prepare_error(wrapper->statement); + duckdb_destroy_extracted(&extracted_statements); return CheckResult(res, error, error_msg); } diff --git a/src/common/constants.cpp b/src/common/constants.cpp index edafe6b67650..2bddd619532f 100644 --- a/src/common/constants.cpp +++ b/src/common/constants.cpp @@ -9,7 +9,9 @@ namespace duckdb { constexpr const idx_t DConstants::INVALID_INDEX; const row_t MAX_ROW_ID = 36028797018960000ULL; // 2^55 const row_t MAX_ROW_ID_LOCAL = 72057594037920000ULL; // 2^56 -const column_t COLUMN_IDENTIFIER_ROW_ID = (column_t)-1; +const column_t COLUMN_IDENTIFIER_ROW_ID = UINT64_C(18446744073709551615); +const column_t COLUMN_IDENTIFIER_EMPTY = UINT64_C(18446744073709551614); +const column_t VIRTUAL_COLUMN_START = UINT64_C(9223372036854775808); // 2^63 const double PI = 3.141592653589793; const transaction_t TRANSACTION_ID_START = 4611686018427388000ULL; // 2^62 @@ -56,4 +58,8 @@ bool IsRowIdColumnId(column_t column_id) { return column_id == COLUMN_IDENTIFIER_ROW_ID; } +bool IsVirtualColumn(column_t column_id) { + return column_id >= VIRTUAL_COLUMN_START; +} + } // namespace duckdb diff --git a/src/common/enum_util.cpp b/src/common/enum_util.cpp index abddfade0a76..1a6b9082b684 100644 --- a/src/common/enum_util.cpp +++ b/src/common/enum_util.cpp @@ -2403,19 +2403,20 @@ const StringUtil::EnumStringLiteral *GetMetricsTypeValues() { { static_cast(MetricsType::OPTIMIZER_EXTENSION), "OPTIMIZER_EXTENSION" }, { static_cast(MetricsType::OPTIMIZER_MATERIALIZED_CTE), "OPTIMIZER_MATERIALIZED_CTE" }, { static_cast(MetricsType::OPTIMIZER_SUM_REWRITER), "OPTIMIZER_SUM_REWRITER" }, - { static_cast(MetricsType::OPTIMIZER_LATE_MATERIALIZATION), "OPTIMIZER_LATE_MATERIALIZATION" } + { static_cast(MetricsType::OPTIMIZER_LATE_MATERIALIZATION), "OPTIMIZER_LATE_MATERIALIZATION" }, + { static_cast(MetricsType::OPTIMIZER_REMOVE_USELESS_PROJECTIONS), "OPTIMIZER_REMOVE_USELESS_PROJECTIONS" } }; return values; } template<> const char* EnumUtil::ToChars(MetricsType value) { - return StringUtil::EnumToString(GetMetricsTypeValues(), 49, "MetricsType", static_cast(value)); + return StringUtil::EnumToString(GetMetricsTypeValues(), 50, "MetricsType", static_cast(value)); } template<> MetricsType EnumUtil::FromString(const char *value) { - return static_cast(StringUtil::StringToEnum(GetMetricsTypeValues(), 49, "MetricsType", value)); + return static_cast(StringUtil::StringToEnum(GetMetricsTypeValues(), 50, "MetricsType", value)); } const StringUtil::EnumStringLiteral *GetMultiFileReaderColumnMappingModeValues() { @@ -2607,19 +2608,20 @@ const StringUtil::EnumStringLiteral *GetOptimizerTypeValues() { { static_cast(OptimizerType::EXTENSION), "EXTENSION" }, { static_cast(OptimizerType::MATERIALIZED_CTE), "MATERIALIZED_CTE" }, { static_cast(OptimizerType::SUM_REWRITER), "SUM_REWRITER" }, - { static_cast(OptimizerType::LATE_MATERIALIZATION), "LATE_MATERIALIZATION" } + { static_cast(OptimizerType::LATE_MATERIALIZATION), "LATE_MATERIALIZATION" }, + { static_cast(OptimizerType::REMOVE_USELESS_PROJECTIONS), "REMOVE_USELESS_PROJECTIONS" } }; return values; } template<> const char* EnumUtil::ToChars(OptimizerType value) { - return StringUtil::EnumToString(GetOptimizerTypeValues(), 28, "OptimizerType", static_cast(value)); + return StringUtil::EnumToString(GetOptimizerTypeValues(), 29, "OptimizerType", static_cast(value)); } template<> OptimizerType EnumUtil::FromString(const char *value) { - return static_cast(StringUtil::StringToEnum(GetOptimizerTypeValues(), 28, "OptimizerType", value)); + return static_cast(StringUtil::StringToEnum(GetOptimizerTypeValues(), 29, "OptimizerType", value)); } const StringUtil::EnumStringLiteral *GetOrderByNullTypeValues() { diff --git a/src/common/enums/metric_type.cpp b/src/common/enums/metric_type.cpp index d97579c23f2a..24793fdcdecc 100644 --- a/src/common/enums/metric_type.cpp +++ b/src/common/enums/metric_type.cpp @@ -40,6 +40,7 @@ profiler_settings_t MetricsUtils::GetOptimizerMetrics() { MetricsType::OPTIMIZER_MATERIALIZED_CTE, MetricsType::OPTIMIZER_SUM_REWRITER, MetricsType::OPTIMIZER_LATE_MATERIALIZATION, + MetricsType::OPTIMIZER_REMOVE_USELESS_PROJECTIONS, }; } @@ -112,6 +113,8 @@ MetricsType MetricsUtils::GetOptimizerMetricByType(OptimizerType type) { return MetricsType::OPTIMIZER_SUM_REWRITER; case OptimizerType::LATE_MATERIALIZATION: return MetricsType::OPTIMIZER_LATE_MATERIALIZATION; + case OptimizerType::REMOVE_USELESS_PROJECTIONS: + return MetricsType::OPTIMIZER_REMOVE_USELESS_PROJECTIONS; default: throw InternalException("OptimizerType %s cannot be converted to a MetricsType", EnumUtil::ToString(type)); }; @@ -173,6 +176,8 @@ OptimizerType MetricsUtils::GetOptimizerTypeByMetric(MetricsType type) { return OptimizerType::SUM_REWRITER; case MetricsType::OPTIMIZER_LATE_MATERIALIZATION: return OptimizerType::LATE_MATERIALIZATION; + case MetricsType::OPTIMIZER_REMOVE_USELESS_PROJECTIONS: + return OptimizerType::REMOVE_USELESS_PROJECTIONS; default: return OptimizerType::INVALID; }; @@ -207,6 +212,7 @@ bool MetricsUtils::IsOptimizerMetric(MetricsType type) { case MetricsType::OPTIMIZER_MATERIALIZED_CTE: case MetricsType::OPTIMIZER_SUM_REWRITER: case MetricsType::OPTIMIZER_LATE_MATERIALIZATION: + case MetricsType::OPTIMIZER_REMOVE_USELESS_PROJECTIONS: return true; default: return false; diff --git a/src/common/enums/optimizer_type.cpp b/src/common/enums/optimizer_type.cpp index f4d02d68a3b5..fd307260d06d 100644 --- a/src/common/enums/optimizer_type.cpp +++ b/src/common/enums/optimizer_type.cpp @@ -29,6 +29,7 @@ static const DefaultOptimizerType internal_optimizer_types[] = { {"column_lifetime", OptimizerType::COLUMN_LIFETIME}, {"limit_pushdown", OptimizerType::LIMIT_PUSHDOWN}, {"top_n", OptimizerType::TOP_N}, + {"remove_useless_projections", OptimizerType::REMOVE_USELESS_PROJECTIONS}, {"build_side_probe_side", OptimizerType::BUILD_SIDE_PROBE_SIDE}, {"compressed_materialization", OptimizerType::COMPRESSED_MATERIALIZATION}, {"duplicate_groups", OptimizerType::DUPLICATE_GROUPS}, diff --git a/src/common/multi_file_list.cpp b/src/common/multi_file_list.cpp index 668a5b36399e..fd5d84a856ec 100644 --- a/src/common/multi_file_list.cpp +++ b/src/common/multi_file_list.cpp @@ -31,9 +31,10 @@ bool PushdownInternal(ClientContext &context, const MultiFileReaderOptions &opti vector> &filters, vector &expanded_files) { HivePartitioningFilterInfo filter_info; for (idx_t i = 0; i < info.column_ids.size(); i++) { - if (!IsRowIdColumnId(info.column_ids[i])) { - filter_info.column_map.insert({info.column_names[info.column_ids[i]], i}); + if (IsVirtualColumn(info.column_ids[i])) { + continue; } + filter_info.column_map.insert({info.column_names[info.column_ids[i]], i}); } filter_info.hive_enabled = options.hive_partitioning; filter_info.filename_enabled = options.filename; @@ -61,6 +62,9 @@ bool PushdownInternal(ClientContext &context, const MultiFileReaderOptions &opti vector> filter_expressions; for (auto &entry : filters.filters) { auto column_idx = column_ids[entry.first]; + if (IsVirtualColumn(column_idx)) { + continue; + } auto column_ref = make_uniq(types[column_idx], ColumnBinding(table_index, entry.first)); auto filter_expr = entry.second->ToExpression(*column_ref); diff --git a/src/common/multi_file_reader.cpp b/src/common/multi_file_reader.cpp index be17aaf9516b..3aacf065dca3 100644 --- a/src/common/multi_file_reader.cpp +++ b/src/common/multi_file_reader.cpp @@ -15,6 +15,8 @@ namespace duckdb { +constexpr column_t MultiFileReader::COLUMN_IDENTIFIER_FILENAME; + MultiFileReaderGlobalState::~MultiFileReaderGlobalState() { } @@ -235,6 +237,14 @@ void MultiFileReader::BindOptions(MultiFileReaderOptions &options, MultiFileList } } +void MultiFileReader::GetVirtualColumns(ClientContext &context, MultiFileReaderBindData &bind_data, + virtual_column_map_t &result) { + if (bind_data.filename_idx == DConstants::INVALID_INDEX || bind_data.filename_idx == COLUMN_IDENTIFIER_FILENAME) { + bind_data.filename_idx = COLUMN_IDENTIFIER_FILENAME; + result.insert(make_pair(COLUMN_IDENTIFIER_FILENAME, TableColumn("filename", LogicalType::VARCHAR))); + } +} + void MultiFileReader::FinalizeBind(const MultiFileReaderOptions &file_options, const MultiFileReaderBindData &options, const string &filename, const vector &local_columns, const vector &global_columns, @@ -251,17 +261,15 @@ void MultiFileReader::FinalizeBind(const MultiFileReaderOptions &file_options, c } for (idx_t i = 0; i < global_column_ids.size(); i++) { auto &col_idx = global_column_ids[i]; - if (col_idx.IsRowIdColumn()) { - // row-id - reader_data.constant_map.emplace_back(i, Value::BIGINT(42)); - continue; - } auto column_id = col_idx.GetPrimaryIndex(); if (column_id == options.filename_idx) { // filename reader_data.constant_map.emplace_back(i, Value(filename)); continue; } + if (IsVirtualColumn(column_id)) { + continue; + } if (!options.hive_partitioning_indexes.empty()) { // hive partition constants auto partitions = HivePartitioning::Parse(filename); @@ -335,6 +343,10 @@ void MultiFileReader::CreateColumnMappingByName(const string &file_name, // not constant - look up the column in the name map auto &global_idx = global_column_ids[i]; auto global_id = global_idx.GetPrimaryIndex(); + if (IsVirtualColumn(global_id)) { + // virtual column - these are emitted for every file + continue; + } if (global_id >= global_columns.size()) { throw InternalException( "MultiFileReader::CreateColumnMappingByName - global_id is out of range in global_types for this file"); diff --git a/src/common/serializer/memory_stream.cpp b/src/common/serializer/memory_stream.cpp index 92419b8d04c8..d608392e8cf7 100644 --- a/src/common/serializer/memory_stream.cpp +++ b/src/common/serializer/memory_stream.cpp @@ -102,4 +102,8 @@ idx_t MemoryStream::GetCapacity() const { return capacity; } +void MemoryStream::SetPosition(idx_t position_p) { + position = position_p; +} + } // namespace duckdb diff --git a/src/common/settings.json b/src/common/settings.json index 6b6718ea5474..ffa09591a398 100644 --- a/src/common/settings.json +++ b/src/common/settings.json @@ -114,6 +114,12 @@ "internal_setting": "arrow_use_list_view", "scope": "global" }, + { + "name": "asof_loop_join_threshold", + "description": "The maximum number of rows we need on the left side of an ASOF join to use a nested loop join", + "type": "UBIGINT", + "scope": "local" + }, { "name": "autoinstall_extension_repository", "description": "Overrides the custom endpoint for extension installation on autoloading", diff --git a/src/common/types/column/column_data_collection.cpp b/src/common/types/column/column_data_collection.cpp index 17be6722389b..e45228b8c219 100644 --- a/src/common/types/column/column_data_collection.cpp +++ b/src/common/types/column/column_data_collection.cpp @@ -417,12 +417,21 @@ static void TemplatedColumnDataCopy(ColumnDataMetaData &meta_data, const Unified // initialize the validity mask to set all to valid result_validity.SetAllValid(STANDARD_VECTOR_SIZE); } - for (idx_t i = 0; i < append_count; i++) { - auto source_idx = source_data.sel->get_index(offset + i); - if (source_data.validity.RowIsValid(source_idx)) { + if (source_data.validity.AllValid()) { + // Fast path: all valid + for (idx_t i = 0; i < append_count; i++) { + auto source_idx = source_data.sel->get_index(offset + i); OP::template Assign(meta_data, base_ptr, source_data.data, current_segment.count + i, source_idx); - } else { - result_validity.SetInvalid(current_segment.count + i); + } + } else { + for (idx_t i = 0; i < append_count; i++) { + auto source_idx = source_data.sel->get_index(offset + i); + if (source_data.validity.RowIsValid(source_idx)) { + OP::template Assign(meta_data, base_ptr, source_data.data, current_segment.count + i, + source_idx); + } else { + result_validity.SetInvalid(current_segment.count + i); + } } } current_segment.count += append_count; @@ -562,7 +571,7 @@ void ColumnDataCopy(ColumnDataMetaData &meta_data, const UnifiedVector offset += append_count; remaining -= append_count; - if (vector_remaining - append_count == 0) { + if (remaining != 0 && vector_remaining - append_count == 0) { // need to append more, check if we need to allocate a new vector or not if (!current_segment.next_data.IsValid()) { segment.AllocateVector(source.GetType(), meta_data.chunk_data, append_state, current_index); diff --git a/src/common/types/hash.cpp b/src/common/types/hash.cpp index 83a1ef22310e..604a6249f8b3 100644 --- a/src/common/types/hash.cpp +++ b/src/common/types/hash.cpp @@ -4,6 +4,7 @@ #include "duckdb/common/types/string_type.hpp" #include "duckdb/common/types/interval.hpp" #include "duckdb/common/types/uhugeint.hpp" +#include "duckdb/common/fast_mem.hpp" #include #include @@ -65,6 +66,11 @@ hash_t Hash(interval_t val) { return Hash(days) ^ Hash(months) ^ Hash(micros); } +template <> +hash_t Hash(dtime_tz_t val) { + return Hash(val.bits); +} + template <> hash_t Hash(const char *str) { return Hash(str, strlen(str)); @@ -72,6 +78,38 @@ hash_t Hash(const char *str) { template <> hash_t Hash(string_t val) { + // If the string is inlined, we can do a branchless hash + if (val.IsInlined()) { + // This seed slightly improves bit distribution, taken from here: + // https://github.com/martinus/robin-hood-hashing/blob/3.11.5/LICENSE + // MIT License Copyright (c) 2018-2021 Martin Ankerl + hash_t h = 0xe17a1465U ^ (val.GetSize() * 0xc6a4a7935bd1e995U); + + // Hash/combine the first 8-byte block + const bool not_an_empty_string = !val.Empty(); + h ^= Load(const_data_ptr_cast(val.GetPrefix())); + h *= 0xd6e8feb86659fd93U * not_an_empty_string + (1 - not_an_empty_string); + + // Load remaining 4 bytes + hash_t hr = 0; + memcpy(&hr, const_data_ptr_cast(val.GetPrefix()) + sizeof(hash_t), 4U); + + // Process the remainder the same an 8-byte block + // This operation is a NOP if the string is <= 8 bytes + const bool not_a_nop = val.GetSize() > sizeof(hash_t); + h ^= hr; + h *= 0xd6e8feb86659fd93U * not_a_nop + (1 - not_a_nop); + + // Finalize + h = Hash(h); + + // This is just an optimization. It should not change the result + // This property is important for verification (e.g., DUCKDB_DEBUG_NO_INLINE) + // We achieved this with the NOP trick above (and in HashBytes) + D_ASSERT(h == Hash(val.GetData(), val.GetSize())); + + return h; + } return Hash(val.GetData(), val.GetSize()); } @@ -80,68 +118,38 @@ hash_t Hash(char *val) { return Hash(val); } -// MIT License -// Copyright (c) 2018-2021 Martin Ankerl -// https://github.com/martinus/robin-hood-hashing/blob/3.11.5/LICENSE -hash_t HashBytes(void *ptr, size_t len) noexcept { - static constexpr uint64_t M = UINT64_C(0xc6a4a7935bd1e995); - static constexpr uint64_t SEED = UINT64_C(0xe17a1465); - static constexpr unsigned int R = 47; +hash_t HashBytes(const_data_ptr_t ptr, const idx_t len) noexcept { + // This seed slightly improves bit distribution, taken from here: + // https://github.com/martinus/robin-hood-hashing/blob/3.11.5/LICENSE + // MIT License Copyright (c) 2018-2021 Martin Ankerl + hash_t h = 0xe17a1465U ^ (len * 0xc6a4a7935bd1e995U); - auto const *const data64 = static_cast(ptr); - uint64_t h = SEED ^ (len * M); - - size_t const n_blocks = len / 8; - for (size_t i = 0; i < n_blocks; ++i) { - auto k = Load(reinterpret_cast(data64 + i)); + // Hash/combine in blocks of 8 bytes + for (const auto end = ptr + len - (len & 7U); ptr != end; ptr += 8U) { + h ^= Load(ptr); + h *= 0xd6e8feb86659fd93U; + } - k *= M; - k ^= k >> R; - k *= M; + // Load remaining (<8) bytes + hash_t hr = 0; + memcpy(&hr, ptr, len & 7U); - h ^= k; - h *= M; - } + // Process the remainder same as an 8-byte block + // This operation is a NOP if the number of remaining bytes is 0 + const bool not_a_nop = len & 7U; + h ^= hr; + h *= 0xd6e8feb86659fd93U * not_a_nop + (1 - not_a_nop); - auto const *const data8 = reinterpret_cast(data64 + n_blocks); - switch (len & 7U) { - case 7: - h ^= static_cast(data8[6]) << 48U; - DUCKDB_EXPLICIT_FALLTHROUGH; - case 6: - h ^= static_cast(data8[5]) << 40U; - DUCKDB_EXPLICIT_FALLTHROUGH; - case 5: - h ^= static_cast(data8[4]) << 32U; - DUCKDB_EXPLICIT_FALLTHROUGH; - case 4: - h ^= static_cast(data8[3]) << 24U; - DUCKDB_EXPLICIT_FALLTHROUGH; - case 3: - h ^= static_cast(data8[2]) << 16U; - DUCKDB_EXPLICIT_FALLTHROUGH; - case 2: - h ^= static_cast(data8[1]) << 8U; - DUCKDB_EXPLICIT_FALLTHROUGH; - case 1: - h ^= static_cast(data8[0]); - h *= M; - DUCKDB_EXPLICIT_FALLTHROUGH; - default: - break; - } - h ^= h >> R; - h *= M; - h ^= h >> R; - return static_cast(h); + // Finalize + return Hash(h); } hash_t Hash(const char *val, size_t size) { - return HashBytes((void *)val, size); + return HashBytes(const_data_ptr_cast(val), size); } hash_t Hash(uint8_t *val, size_t size) { - return HashBytes((void *)val, size); + return HashBytes(const_data_ptr_cast(val), size); } } // namespace duckdb diff --git a/src/execution/expression_executor/execute_operator.cpp b/src/execution/expression_executor/execute_operator.cpp index 6f37e5d29138..04883c5deeac 100644 --- a/src/execution/expression_executor/execute_operator.cpp +++ b/src/execution/expression_executor/execute_operator.cpp @@ -124,13 +124,16 @@ void ExpressionExecutor::Execute(const BoundOperatorExpression &expr, Expression } } SelectionVector selvec(1); - Vector intermediate(result.GetType(), 1); + DataChunk intermediate; + intermediate.Initialize(GetAllocator(), {result.GetType()}, 1); for (idx_t i = 0; i < count; i++) { + intermediate.Reset(); + intermediate.SetCardinality(1); selvec.set_index(0, sel ? sel->get_index(i) : i); Value val(result.GetType()); try { - Execute(*expr.children[0], &child_state, &selvec, 1, intermediate); - val = intermediate.GetValue(0); + Execute(*expr.children[0], &child_state, &selvec, 1, intermediate.data[0]); + val = intermediate.GetValue(0, 0); } catch (std::exception &ex) { ErrorData error(ex); auto error_type = error.Type(); diff --git a/src/execution/index/art/iterator.cpp b/src/execution/index/art/iterator.cpp index 689029a02e40..1c138e1d3e34 100644 --- a/src/execution/index/art/iterator.cpp +++ b/src/execution/index/art/iterator.cpp @@ -46,9 +46,11 @@ bool Iterator::Scan(const ARTKey &upper_bound, const idx_t max_count, unsafe_vec bool has_next; do { // An empty upper bound indicates that no upper bound exists. - if (!upper_bound.Empty() && status == GateStatus::GATE_NOT_SET) { - if (current_key.GreaterThan(upper_bound, equal, nested_depth)) { - return true; + if (!upper_bound.Empty()) { + if (status == GateStatus::GATE_NOT_SET || entered_nested_leaf) { + if (current_key.GreaterThan(upper_bound, equal, nested_depth)) { + return true; + } } } @@ -86,6 +88,7 @@ bool Iterator::Scan(const ARTKey &upper_bound, const idx_t max_count, unsafe_vec throw InternalException("Invalid leaf type for index scan."); } + entered_nested_leaf = false; has_next = Next(); } while (has_next); return true; @@ -104,6 +107,7 @@ void Iterator::FindMinimum(const Node &node) { if (node.GetGateStatus() == GateStatus::GATE_SET) { D_ASSERT(status == GateStatus::GATE_NOT_SET); status = GateStatus::GATE_SET; + entered_nested_leaf = true; nested_depth = 0; } diff --git a/src/execution/operator/csv_scanner/encode/csv_encoder.cpp b/src/execution/operator/csv_scanner/encode/csv_encoder.cpp index 89fc5df040bd..8a6c08032597 100644 --- a/src/execution/operator/csv_scanner/encode/csv_encoder.cpp +++ b/src/execution/operator/csv_scanner/encode/csv_encoder.cpp @@ -51,6 +51,10 @@ CSVEncoder::CSVEncoder(DBConfig &config, const string &encoding_name_to_find, id } // We ensure that the encoded buffer size is an even number to make the two byte lookup on utf-16 work idx_t encoded_buffer_size = buffer_size % 2 != 0 ? buffer_size - 1 : buffer_size; + if (encoded_buffer_size == 0) { + // This might happen if buffer size = 1 + encoded_buffer_size = 2; + } D_ASSERT(encoded_buffer_size > 0); encoded_buffer.Initialize(encoded_buffer_size); remaining_bytes_buffer.Initialize(function->GetBytesPerIteration()); diff --git a/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp b/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp index 94ef37399510..266f35196af5 100644 --- a/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp +++ b/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp @@ -688,23 +688,29 @@ bool LineError::HandleErrors(StringValueResult &result) { line_pos.GetGlobalPosition(result.requested_size), result.path); } break; - case CAST_ERROR: + case CAST_ERROR: { + string column_name; + LogicalTypeId type_id; + if (cur_error.col_idx < result.names.size()) { + column_name = result.names[cur_error.col_idx]; + } + if (cur_error.col_idx < result.number_of_columns) { + type_id = result.parse_types[cur_error.chunk_idx].type_id; + } if (result.current_line_position.begin == line_pos) { csv_error = CSVError::CastError( - result.state_machine.options, result.names[cur_error.col_idx], cur_error.error_message, - cur_error.col_idx, borked_line, lines_per_batch, + result.state_machine.options, column_name, cur_error.error_message, cur_error.col_idx, borked_line, + lines_per_batch, result.current_line_position.begin.GetGlobalPosition(result.requested_size, first_nl), - line_pos.GetGlobalPosition(result.requested_size, first_nl), - result.parse_types[cur_error.chunk_idx].type_id, result.path); + line_pos.GetGlobalPosition(result.requested_size, first_nl), type_id, result.path); } else { csv_error = CSVError::CastError( - result.state_machine.options, result.names[cur_error.col_idx], cur_error.error_message, - cur_error.col_idx, borked_line, lines_per_batch, + result.state_machine.options, column_name, cur_error.error_message, cur_error.col_idx, borked_line, + lines_per_batch, result.current_line_position.begin.GetGlobalPosition(result.requested_size, first_nl), - line_pos.GetGlobalPosition(result.requested_size), result.parse_types[cur_error.chunk_idx].type_id, - result.path); + line_pos.GetGlobalPosition(result.requested_size), type_id, result.path); } - break; + } break; case MAXIMUM_LINE_SIZE: csv_error = CSVError::LineSizeError( result.state_machine.options, lines_per_batch, borked_line, diff --git a/src/execution/operator/csv_scanner/util/csv_reader_options.cpp b/src/execution/operator/csv_scanner/util/csv_reader_options.cpp index 9f7391eaa7a8..a4e0c82440cc 100644 --- a/src/execution/operator/csv_scanner/util/csv_reader_options.cpp +++ b/src/execution/operator/csv_scanner/util/csv_reader_options.cpp @@ -254,6 +254,10 @@ void CSVReaderOptions::SetReadOption(const string &loption, const Value &value, throw BinderException("Invalid value for MAX_LINE_SIZE parameter: it cannot be smaller than 0"); } maximum_line_size.Set(NumericCast(line_size)); + if (buffer_size_option.IsSetByUser() && maximum_line_size.GetValue() > buffer_size_option.GetValue()) { + throw InvalidInputException("Buffer Size of %d must be a higher value than the maximum line size %d", + buffer_size_option.GetValue(), maximum_line_size.GetValue()); + } } else if (loption == "date_format" || loption == "dateformat") { string format = ParseString(value, loption); SetDateFormat(LogicalTypeId::DATE, format, true); @@ -267,6 +271,12 @@ void CSVReaderOptions::SetReadOption(const string &loption, const Value &value, if (buffer_size_option == 0) { throw InvalidInputException("Buffer Size option must be higher than 0"); } + if (maximum_line_size.IsSetByUser() && maximum_line_size.GetValue() > buffer_size_option.GetValue()) { + throw InvalidInputException("Buffer Size of %d must be a higher value than the maximum line size %d", + buffer_size_option.GetValue(), maximum_line_size.GetValue()); + } else { + maximum_line_size.Set(buffer_size_option.GetValue(), false); + } } else if (loption == "decimal_separator") { decimal_separator = ParseString(value, loption); if (decimal_separator != "." && decimal_separator != ",") { @@ -301,6 +311,9 @@ void CSVReaderOptions::SetReadOption(const string &loption, const Value &value, if (table_name.empty()) { throw BinderException("REJECTS_TABLE option cannot be empty"); } + if (KeywordHelper::RequiresQuotes(table_name)) { + throw BinderException("rejects_scan option: %s requires quotes to be used as an identifier", table_name); + } rejects_table_name.Set(table_name); } else if (loption == "rejects_scan") { // skip, handled in SetRejectsOptions @@ -308,6 +321,9 @@ void CSVReaderOptions::SetReadOption(const string &loption, const Value &value, if (table_name.empty()) { throw BinderException("rejects_scan option cannot be empty"); } + if (KeywordHelper::RequiresQuotes(table_name)) { + throw BinderException("rejects_scan option: %s requires quotes to be used as an identifier", table_name); + } rejects_scan_name.Set(table_name); } else if (loption == "rejects_limit") { auto limit = ParseInteger(value, loption); diff --git a/src/execution/operator/helper/physical_streaming_sample.cpp b/src/execution/operator/helper/physical_streaming_sample.cpp index 309256244927..ed9e21f35195 100644 --- a/src/execution/operator/helper/physical_streaming_sample.cpp +++ b/src/execution/operator/helper/physical_streaming_sample.cpp @@ -5,10 +5,11 @@ namespace duckdb { -PhysicalStreamingSample::PhysicalStreamingSample(vector types, SampleMethod method, double percentage, - int64_t seed, idx_t estimated_cardinality) - : PhysicalOperator(PhysicalOperatorType::STREAMING_SAMPLE, std::move(types), estimated_cardinality), method(method), - percentage(percentage / 100), seed(seed) { +PhysicalStreamingSample::PhysicalStreamingSample(vector types, unique_ptr options, + idx_t estimated_cardinality) + : PhysicalOperator(PhysicalOperatorType::STREAMING_SAMPLE, std::move(types), estimated_cardinality), + sample_options(std::move(options)) { + percentage = sample_options->sample_size.GetValue() / 100; } //===--------------------------------------------------------------------===// @@ -49,13 +50,21 @@ void PhysicalStreamingSample::BernoulliSample(DataChunk &input, DataChunk &resul } } +bool PhysicalStreamingSample::ParallelOperator() const { + return !(sample_options->repeatable || sample_options->seed.IsValid()); +} + unique_ptr PhysicalStreamingSample::GetOperatorState(ExecutionContext &context) const { - return make_uniq(seed); + if (!ParallelOperator()) { + return make_uniq(static_cast(sample_options->seed.GetIndex())); + } + RandomEngine random; + return make_uniq(static_cast(random.NextRandomInteger64())); } OperatorResultType PhysicalStreamingSample::Execute(ExecutionContext &context, DataChunk &input, DataChunk &chunk, GlobalOperatorState &gstate, OperatorState &state) const { - switch (method) { + switch (sample_options->method) { case SampleMethod::BERNOULLI_SAMPLE: BernoulliSample(input, chunk, state); break; @@ -70,7 +79,7 @@ OperatorResultType PhysicalStreamingSample::Execute(ExecutionContext &context, D InsertionOrderPreservingMap PhysicalStreamingSample::ParamsToString() const { InsertionOrderPreservingMap result; - result["Sample Method"] = EnumUtil::ToString(method) + ": " + to_string(100 * percentage) + "%"; + result["Sample Method"] = EnumUtil::ToString(sample_options->method) + ": " + to_string(100 * percentage) + "%"; return result; } diff --git a/src/execution/operator/persistent/physical_batch_copy_to_file.cpp b/src/execution/operator/persistent/physical_batch_copy_to_file.cpp index 4effccaff4b7..80a761d34f20 100644 --- a/src/execution/operator/persistent/physical_batch_copy_to_file.cpp +++ b/src/execution/operator/persistent/physical_batch_copy_to_file.cpp @@ -143,7 +143,8 @@ class FixedBatchCopyLocalState : public LocalSinkState { FixedBatchCopyState current_task = FixedBatchCopyState::SINKING_DATA; void InitializeCollection(ClientContext &context, const PhysicalOperator &op) { - collection = make_uniq(BufferAllocator::Get(context), op.children[0]->types); + collection = make_uniq(context, op.children[0]->types); + collection->SetPartitionIndex(0); // Makes the buffer manager less likely to spill this data collection->InitializeAppend(append_state); local_memory_usage = 0; } @@ -433,8 +434,8 @@ void PhysicalBatchCopyToFile::RepartitionBatches(ClientContext &context, GlobalS } else { // the collection is too large for a batch - we need to repartition // create an empty collection - auto new_collection = - make_uniq(BufferAllocator::Get(context), children[0]->types); + auto new_collection = make_uniq(context, children[0]->types); + new_collection->SetPartitionIndex(0); // Makes the buffer manager less likely to spill this data append_batch = make_uniq(0U, std::move(new_collection)); } if (append_batch) { @@ -458,7 +459,8 @@ void PhysicalBatchCopyToFile::RepartitionBatches(ClientContext &context, GlobalS // the collection is full - move it to the result and create a new one task_manager.AddTask(make_uniq(gstate.scheduled_batch_index++, std::move(append_batch))); - auto new_collection = make_uniq(BufferAllocator::Get(context), children[0]->types); + auto new_collection = make_uniq(context, children[0]->types); + new_collection->SetPartitionIndex(0); // Makes the buffer manager less likely to spill this data append_batch = make_uniq(0U, std::move(new_collection)); append_batch->collection->InitializeAppend(append_state); } diff --git a/src/execution/operator/persistent/physical_batch_insert.cpp b/src/execution/operator/persistent/physical_batch_insert.cpp index 2e546c477282..3caaff78914d 100644 --- a/src/execution/operator/persistent/physical_batch_insert.cpp +++ b/src/execution/operator/persistent/physical_batch_insert.cpp @@ -38,38 +38,47 @@ enum class RowGroupBatchType : uint8_t { FLUSHED, NOT_FLUSHED }; class CollectionMerger { public: - explicit CollectionMerger(ClientContext &context) : context(context) { + explicit CollectionMerger(ClientContext &context, DataTable &data_table) + : context(context), data_table(data_table), batch_type(RowGroupBatchType::NOT_FLUSHED) { } + //! The transaction context. ClientContext &context; - vector> current_collections; - RowGroupBatchType batch_type = RowGroupBatchType::NOT_FLUSHED; + //! The data table. + DataTable &data_table; + //! Indexes to the optimistic row group collection vector of the local table storage for this transaction. + vector collection_indexes; + //! The batch type for merging collections. + RowGroupBatchType batch_type; public: - void AddCollection(unique_ptr collection, RowGroupBatchType type) { - current_collections.push_back(std::move(collection)); + void AddCollection(const PhysicalIndex collection_index, RowGroupBatchType type) { + collection_indexes.push_back(collection_index); if (type == RowGroupBatchType::FLUSHED) { batch_type = RowGroupBatchType::FLUSHED; - if (current_collections.size() > 1) { + if (collection_indexes.size() > 1) { throw InternalException("Cannot merge flushed collections"); } } } bool Empty() { - return current_collections.empty(); + return collection_indexes.empty(); } - unique_ptr Flush(OptimisticDataWriter &writer) { + PhysicalIndex Flush(OptimisticDataWriter &writer) { if (Empty()) { - return nullptr; + return PhysicalIndex(DConstants::INVALID_INDEX); } - unique_ptr new_collection = std::move(current_collections[0]); - if (current_collections.size() > 1) { - // we have gathered multiple collections: create one big collection and merge that - auto &types = new_collection->GetTypes(); + + auto result_collection_index = collection_indexes[0]; + auto &result_collection = data_table.GetOptimisticCollection(context, result_collection_index); + + if (collection_indexes.size() > 1) { + // Merge all collections into one result collection. + auto &types = result_collection.GetTypes(); TableAppendState append_state; - new_collection->InitializeAppend(append_state); + result_collection.InitializeAppend(append_state); DataChunk scan_chunk; scan_chunk.Initialize(context, types); @@ -78,13 +87,11 @@ class CollectionMerger { for (idx_t i = 0; i < types.size(); i++) { column_ids.emplace_back(i); } - for (auto &collection : current_collections) { - if (!collection) { - continue; - } + for (idx_t i = 1; i < collection_indexes.size(); i++) { + auto &collection = data_table.GetOptimisticCollection(context, collection_indexes[i]); TableScanState scan_state; scan_state.Initialize(column_ids); - collection->InitializeScan(scan_state.local_state, column_ids, nullptr); + collection.InitializeScan(scan_state.local_state, column_ids, nullptr); while (true) { scan_chunk.Reset(); @@ -92,35 +99,38 @@ class CollectionMerger { if (scan_chunk.size() == 0) { break; } - auto new_row_group = new_collection->Append(scan_chunk, append_state); + auto new_row_group = result_collection.Append(scan_chunk, append_state); if (new_row_group) { - writer.WriteNewRowGroup(*new_collection); + writer.WriteNewRowGroup(result_collection); } } + data_table.ResetOptimisticCollection(context, collection_indexes[i]); } - new_collection->FinalizeAppend(TransactionData(0, 0), append_state); - writer.WriteLastRowGroup(*new_collection); + result_collection.FinalizeAppend(TransactionData(0, 0), append_state); + writer.WriteLastRowGroup(result_collection); } else if (batch_type == RowGroupBatchType::NOT_FLUSHED) { - writer.WriteLastRowGroup(*new_collection); + writer.WriteLastRowGroup(result_collection); } - current_collections.clear(); - return new_collection; + + collection_indexes.clear(); + return result_collection_index; } }; struct RowGroupBatchEntry { - RowGroupBatchEntry(idx_t batch_idx, unique_ptr collection_p, RowGroupBatchType type) - : batch_idx(batch_idx), total_rows(collection_p->GetTotalRows()), unflushed_memory(0), - collection(std::move(collection_p)), type(type) { + RowGroupBatchEntry(RowGroupCollection &collection, const idx_t batch_idx, const PhysicalIndex collection_index, + const RowGroupBatchType type) + : batch_idx(batch_idx), total_rows(collection.GetTotalRows()), unflushed_memory(0), + collection_index(collection_index), type(type) { if (type == RowGroupBatchType::NOT_FLUSHED) { - unflushed_memory = collection->GetAllocationSize(); + unflushed_memory = collection.GetAllocationSize(); } } idx_t batch_idx; idx_t total_rows; idx_t unflushed_memory; - unique_ptr collection; + PhysicalIndex collection_index; RowGroupBatchType type; }; @@ -138,7 +148,7 @@ class BatchInsertTask { class BatchInsertGlobalState : public GlobalSinkState { public: - explicit BatchInsertGlobalState(ClientContext &context, DuckTableEntry &table, idx_t minimum_memory_per_thread) + BatchInsertGlobalState(ClientContext &context, DuckTableEntry &table, const idx_t minimum_memory_per_thread) : memory_manager(context, minimum_memory_per_thread), table(table), insert_count(0), optimistically_written(false), minimum_memory_per_thread(minimum_memory_per_thread) { row_group_size = table.GetStorage().GetRowGroupSize(); @@ -155,16 +165,14 @@ class BatchInsertGlobalState : public GlobalSinkState { atomic optimistically_written; idx_t minimum_memory_per_thread; - bool ReadyToMerge(idx_t count) const; - void ScheduleMergeTasks(idx_t min_batch_index); - unique_ptr MergeCollections(ClientContext &context, - vector merge_collections, - OptimisticDataWriter &writer); - void AddCollection(ClientContext &context, idx_t batch_index, idx_t min_batch_index, - unique_ptr current_collection, - optional_ptr writer = nullptr); + bool ReadyToMerge(const idx_t count) const; + void ScheduleMergeTasks(ClientContext &context, const idx_t min_batch_index); + PhysicalIndex MergeCollections(ClientContext &context, const vector &merge_collections, + OptimisticDataWriter &writer); + void AddCollection(ClientContext &context, const idx_t batch_index, const idx_t min_batch_index, + const PhysicalIndex collection_index, optional_ptr writer = nullptr); - idx_t MaxThreads(idx_t source_max_threads) override { + idx_t MaxThreads(const idx_t source_max_threads) override { // try to request 4MB per column per thread memory_manager.SetMemorySize(source_max_threads * minimum_memory_per_thread); // cap the concurrent threads working on this task based on the amount of available memory @@ -176,7 +184,7 @@ class BatchInsertLocalState : public LocalSinkState { public: BatchInsertLocalState(ClientContext &context, const vector &types, const vector> &bound_defaults) - : default_executor(context, bound_defaults) { + : default_executor(context, bound_defaults), collection_index(DConstants::INVALID_INDEX) { insert_chunk.Initialize(Allocator::Get(context), types); } @@ -184,17 +192,23 @@ class BatchInsertLocalState : public LocalSinkState { ExpressionExecutor default_executor; idx_t current_index; TableAppendState current_append_state; - unique_ptr current_collection; - optional_ptr writer; + PhysicalIndex collection_index; + unique_ptr optimistic_writer; unique_ptr constraint_state; - void CreateNewCollection(DuckTableEntry &table, const vector &insert_types) { - auto table_info = table.GetStorage().GetDataTableInfo(); - auto &io_manager = TableIOManager::Get(table.GetStorage()); - current_collection = make_uniq(std::move(table_info), io_manager, insert_types, - NumericCast(MAX_ROW_ID)); - current_collection->InitializeEmpty(); - current_collection->InitializeAppend(current_append_state); + void CreateNewCollection(ClientContext &context, DuckTableEntry &table_entry, + const vector &insert_types) { + auto table_info = table_entry.GetStorage().GetDataTableInfo(); + auto &io_manager = TableIOManager::Get(table_entry.GetStorage()); + + // Create the local row group collection. + auto max_row_id = NumericCast(MAX_ROW_ID); + auto collection = make_uniq(std::move(table_info), io_manager, insert_types, max_row_id); + collection->InitializeEmpty(); + collection->InitializeAppend(current_append_state); + + auto &data_table = table_entry.GetStorage(); + collection_index = data_table.CreateOptimisticCollection(context, std::move(collection)); } }; @@ -210,23 +224,31 @@ class MergeCollectionTask : public BatchInsertTask { vector merge_collections; idx_t merged_batch_index; - void Execute(const PhysicalBatchInsert &op, ClientContext &context, GlobalSinkState &gstate_p, - LocalSinkState &lstate_p) override { - auto &gstate = gstate_p.Cast(); - auto &lstate = lstate_p.Cast(); - // merge together the collections - D_ASSERT(lstate.writer); - auto final_collection = gstate.MergeCollections(context, std::move(merge_collections), *lstate.writer); - // add the merged-together collection to the set of batch indexes - lock_guard l(gstate.lock); - RowGroupBatchEntry new_entry(merged_batch_index, std::move(final_collection), RowGroupBatchType::FLUSHED); + void Execute(const PhysicalBatchInsert &op, ClientContext &context, GlobalSinkState &g_state_p, + LocalSinkState &l_state_p) override { + auto &g_state = g_state_p.Cast(); + auto &l_state = l_state_p.Cast(); + + // Merge the collections. + if (!l_state.optimistic_writer) { + l_state.optimistic_writer = make_uniq(g_state.table.GetStorage()); + } + auto result_collection_index = g_state.MergeCollections(context, merge_collections, *l_state.optimistic_writer); + merge_collections.clear(); + + lock_guard l(g_state.lock); + auto &result_collection = g_state.table.GetStorage().GetOptimisticCollection(context, result_collection_index); + RowGroupBatchEntry new_entry(result_collection, merged_batch_index, result_collection_index, + RowGroupBatchType::FLUSHED); + + // Add the result collection to the set of batch indexes. auto it = std::lower_bound( - gstate.collections.begin(), gstate.collections.end(), new_entry, + g_state.collections.begin(), g_state.collections.end(), new_entry, [&](const RowGroupBatchEntry &a, const RowGroupBatchEntry &b) { return a.batch_idx < b.batch_idx; }); if (it->batch_idx != merged_batch_index) { throw InternalException("Merged batch index was no longer present in collection"); } - it->collection = std::move(new_entry.collection); + it->collection_index = new_entry.collection_index; } }; @@ -239,7 +261,7 @@ struct BatchMergeTask { idx_t total_count; }; -bool BatchInsertGlobalState::ReadyToMerge(idx_t count) const { +bool BatchInsertGlobalState::ReadyToMerge(const idx_t count) const { // we try to merge so the count fits nicely into row groups if (count >= row_group_size / 10 * 9 && count <= row_group_size) { // 90%-100% of row group size @@ -260,9 +282,8 @@ bool BatchInsertGlobalState::ReadyToMerge(idx_t count) const { return false; } -void BatchInsertGlobalState::ScheduleMergeTasks(idx_t min_batch_index) { +void BatchInsertGlobalState::ScheduleMergeTasks(ClientContext &context, const idx_t min_batch_index) { idx_t current_idx; - vector to_be_scheduled_tasks; BatchMergeTask current_task(next_start); @@ -308,19 +329,21 @@ void BatchInsertGlobalState::ScheduleMergeTasks(idx_t min_batch_index) { for (auto &scheduled_task : to_be_scheduled_tasks) { D_ASSERT(scheduled_task.total_count > 0); D_ASSERT(current_idx > scheduled_task.start_index); - idx_t merged_batch_index = collections[scheduled_task.start_index].batch_idx; + auto merged_batch_index = collections[scheduled_task.start_index].batch_idx; vector merge_collections; for (idx_t idx = scheduled_task.start_index; idx < scheduled_task.end_index; idx++) { auto &entry = collections[idx]; - if (!entry.collection || entry.type == RowGroupBatchType::FLUSHED) { + if (!entry.collection_index.IsValid() || entry.type == RowGroupBatchType::FLUSHED) { throw InternalException("Adding a row group collection that should not be flushed"); } - RowGroupBatchEntry added_entry(collections[scheduled_task.start_index].batch_idx, - std::move(entry.collection), RowGroupBatchType::FLUSHED); + auto &collection = table.GetStorage().GetOptimisticCollection(context, entry.collection_index); + RowGroupBatchEntry added_entry(collection, collections[scheduled_task.start_index].batch_idx, + entry.collection_index, RowGroupBatchType::FLUSHED); added_entry.unflushed_memory = entry.unflushed_memory; - merge_collections.push_back(std::move(added_entry)); + merge_collections.push_back(added_entry); entry.total_rows = scheduled_task.total_count; entry.type = RowGroupBatchType::FLUSHED; + entry.collection_index = PhysicalIndex(DConstants::INVALID_INDEX); } task_manager.AddTask(make_uniq(std::move(merge_collections), merged_batch_index)); } @@ -335,14 +358,14 @@ void BatchInsertGlobalState::ScheduleMergeTasks(idx_t min_batch_index) { } } -unique_ptr BatchInsertGlobalState::MergeCollections(ClientContext &context, - vector merge_collections, - OptimisticDataWriter &writer) { +PhysicalIndex BatchInsertGlobalState::MergeCollections(ClientContext &context, + const vector &merge_collections, + OptimisticDataWriter &writer) { D_ASSERT(!merge_collections.empty()); - CollectionMerger merger(context); + CollectionMerger merger(context, table.GetStorage()); idx_t written_data = 0; for (auto &entry : merge_collections) { - merger.AddCollection(std::move(entry.collection), RowGroupBatchType::NOT_FLUSHED); + merger.AddCollection(entry.collection_index, RowGroupBatchType::NOT_FLUSHED); written_data += entry.unflushed_memory; } optimistically_written = true; @@ -350,22 +373,23 @@ unique_ptr BatchInsertGlobalState::MergeCollections(ClientCo return merger.Flush(writer); } -void BatchInsertGlobalState::AddCollection(ClientContext &context, idx_t batch_index, idx_t min_batch_index, - unique_ptr current_collection, +void BatchInsertGlobalState::AddCollection(ClientContext &context, const idx_t batch_index, const idx_t min_batch_index, + const PhysicalIndex collection_index, optional_ptr writer) { if (batch_index < min_batch_index) { throw InternalException("Batch index of the added collection (%llu) is smaller than the min batch index (%llu)", batch_index, min_batch_index); } - auto new_count = current_collection->GetTotalRows(); + auto &collection = table.GetStorage().GetOptimisticCollection(context, collection_index); + auto new_count = collection.GetTotalRows(); auto batch_type = new_count < row_group_size ? RowGroupBatchType::NOT_FLUSHED : RowGroupBatchType::FLUSHED; if (batch_type == RowGroupBatchType::FLUSHED && writer) { - writer->WriteLastRowGroup(*current_collection); + writer->WriteLastRowGroup(collection); } lock_guard l(lock); insert_count += new_count; // add the collection to the batch index - RowGroupBatchEntry new_entry(batch_index, std::move(current_collection), batch_type); + RowGroupBatchEntry new_entry(collection, batch_index, collection_index, batch_type); if (batch_type == RowGroupBatchType::NOT_FLUSHED) { memory_manager.IncreaseUnflushedMemory(new_entry.unflushed_memory); } @@ -379,9 +403,9 @@ void BatchInsertGlobalState::AddCollection(ClientContext &context, idx_t batch_i "batch indexes are not uniquely distributed over threads", batch_index); } - collections.insert(it, std::move(new_entry)); + collections.insert(it, new_entry); if (writer) { - ScheduleMergeTasks(min_batch_index); + ScheduleMergeTasks(context, min_batch_index); } } @@ -441,15 +465,16 @@ SinkNextBatchType PhysicalBatchInsert::NextBatch(ExecutionContext &context, Oper auto &memory_manager = gstate.memory_manager; auto batch_index = lstate.partition_info.batch_index.GetIndex(); - if (lstate.current_collection) { + if (lstate.collection_index.IsValid()) { if (lstate.current_index == batch_index) { throw InternalException("NextBatch called with the same batch index?"); } // batch index has changed: move the old collection to the global state and create a new collection TransactionData tdata(0, 0); - lstate.current_collection->FinalizeAppend(tdata, lstate.current_append_state); + auto &collection = gstate.table.GetStorage().GetOptimisticCollection(context.client, lstate.collection_index); + collection.FinalizeAppend(tdata, lstate.current_append_state); gstate.AddCollection(context.client, lstate.current_index, lstate.partition_info.min_batch_index.GetIndex(), - std::move(lstate.current_collection), lstate.writer); + lstate.collection_index, lstate.optimistic_writer); bool any_unblocked; { @@ -459,7 +484,7 @@ SinkNextBatchType PhysicalBatchInsert::NextBatch(ExecutionContext &context, Oper if (!any_unblocked) { ExecuteTasks(context.client, gstate, lstate); } - lstate.current_collection.reset(); + lstate.collection_index.index = DConstants::INVALID_INDEX; } lstate.current_index = batch_index; @@ -501,12 +526,12 @@ SinkResultType PhysicalBatchInsert::Sink(ExecutionContext &context, DataChunk &c } } } - if (!lstate.current_collection) { + if (!lstate.collection_index.IsValid()) { lock_guard l(gstate.lock); // no collection yet: create a new one - lstate.CreateNewCollection(table, insert_types); - if (!lstate.writer) { - lstate.writer = &table.GetStorage().CreateOptimisticWriter(context.client); + lstate.CreateNewCollection(context.client, table, insert_types); + if (!lstate.optimistic_writer) { + lstate.optimistic_writer = make_uniq(table.GetStorage()); } } @@ -520,10 +545,11 @@ SinkResultType PhysicalBatchInsert::Sink(ExecutionContext &context, DataChunk &c auto &storage = table.GetStorage(); storage.VerifyAppendConstraints(*lstate.constraint_state, context.client, lstate.insert_chunk, nullptr, nullptr); - auto new_row_group = lstate.current_collection->Append(lstate.insert_chunk, lstate.current_append_state); + auto &collection = table.GetStorage().GetOptimisticCollection(context.client, lstate.collection_index); + auto new_row_group = collection.Append(lstate.insert_chunk, lstate.current_append_state); if (new_row_group) { // we have already written to disk - flush the next row group as well - lstate.writer->WriteNewRowGroup(*lstate.current_collection); + lstate.optimistic_writer->WriteNewRowGroup(collection); } return SinkResultType::NEED_MORE_INPUT; } @@ -541,17 +567,20 @@ SinkCombineResultType PhysicalBatchInsert::Combine(ExecutionContext &context, Op memory_manager.UpdateMinBatchIndex(lstate.partition_info.min_batch_index.GetIndex()); - if (lstate.current_collection) { + if (lstate.collection_index.IsValid()) { TransactionData tdata(0, 0); - lstate.current_collection->FinalizeAppend(tdata, lstate.current_append_state); - if (lstate.current_collection->GetTotalRows() > 0) { - gstate.AddCollection(context.client, lstate.current_index, lstate.partition_info.min_batch_index.GetIndex(), - std::move(lstate.current_collection)); + auto &collection = gstate.table.GetStorage().GetOptimisticCollection(context.client, lstate.collection_index); + collection.FinalizeAppend(tdata, lstate.current_append_state); + if (collection.GetTotalRows() > 0) { + auto batch_index = lstate.partition_info.min_batch_index.GetIndex(); + gstate.AddCollection(context.client, lstate.current_index, batch_index, lstate.collection_index); + lstate.collection_index = PhysicalIndex(DConstants::INVALID_INDEX); } } - if (lstate.writer) { + if (lstate.optimistic_writer) { lock_guard l(gstate.lock); - gstate.table.GetStorage().FinalizeOptimisticWriter(context.client, *lstate.writer); + auto &optimistic_writer = gstate.table.GetStorage().GetOptimisticWriter(context.client); + optimistic_writer.Merge(*lstate.optimistic_writer); } // unblock any blocked tasks @@ -566,75 +595,87 @@ SinkCombineResultType PhysicalBatchInsert::Combine(ExecutionContext &context, Op //===--------------------------------------------------------------------===// SinkFinalizeType PhysicalBatchInsert::Finalize(Pipeline &pipeline, Event &event, ClientContext &context, OperatorSinkFinalizeInput &input) const { - auto &gstate = input.global_state.Cast(); - auto &memory_manager = gstate.memory_manager; + auto &g_state = input.global_state.Cast(); + auto &table = g_state.table; + auto &data_table = g_state.table.GetStorage(); + auto &memory_manager = g_state.memory_manager; - if (gstate.optimistically_written || gstate.insert_count >= gstate.row_group_size) { + if (g_state.optimistically_written || g_state.insert_count >= g_state.row_group_size) { // we have written data to disk optimistically or are inserting a large amount of data // perform a final pass over all of the row groups and merge them together vector> mergers; unique_ptr current_merger; - auto &storage = gstate.table.GetStorage(); - for (auto &entry : gstate.collections) { + for (auto &entry : g_state.collections) { if (entry.type == RowGroupBatchType::NOT_FLUSHED) { // this collection has not been flushed: add it to the merge set if (!current_merger) { - current_merger = make_uniq(context); + current_merger = make_uniq(context, data_table); } - current_merger->AddCollection(std::move(entry.collection), entry.type); + current_merger->AddCollection(entry.collection_index, entry.type); memory_manager.ReduceUnflushedMemory(entry.unflushed_memory); - } else { - // this collection has been flushed: it does not need to be merged - // create a separate collection merger only for this entry - if (current_merger) { - // we have small collections remaining: flush them - mergers.push_back(std::move(current_merger)); - current_merger.reset(); - } - auto larger_merger = make_uniq(context); - larger_merger->AddCollection(std::move(entry.collection), entry.type); - mergers.push_back(std::move(larger_merger)); + continue; } + + // This collection has been flushed, so it does not need to be merged. + // Create a separate collection merger for it. + if (current_merger) { + // Flush any remaining small allocations. + mergers.push_back(std::move(current_merger)); + current_merger.reset(); + } + auto larger_merger = make_uniq(context, data_table); + larger_merger->AddCollection(entry.collection_index, entry.type); + mergers.push_back(std::move(larger_merger)); } + + g_state.collections.clear(); if (current_merger) { mergers.push_back(std::move(current_merger)); } // now that we have created all of the mergers, perform the actual merging - vector> final_collections; + vector final_collections; final_collections.reserve(mergers.size()); - auto &writer = storage.CreateOptimisticWriter(context); + auto writer = make_uniq(data_table); for (auto &merger : mergers) { - final_collections.push_back(merger->Flush(writer)); + final_collections.push_back(merger->Flush(*writer)); } // finally, merge the row groups into the local storage - for (auto &collection : final_collections) { - storage.LocalMerge(context, *collection); + for (const auto collection_index : final_collections) { + auto &collection = data_table.GetOptimisticCollection(context, collection_index); + data_table.LocalMerge(context, collection); + data_table.ResetOptimisticCollection(context, collection_index); } - storage.FinalizeOptimisticWriter(context, writer); - } else { - // we are writing a small amount of data to disk - // append directly to transaction local storage - auto &table = gstate.table; - auto &storage = table.GetStorage(); - LocalAppendState append_state; - storage.InitializeLocalAppend(append_state, table, context, bound_constraints); - auto &transaction = DuckTransaction::Get(context, table.catalog); - for (auto &entry : gstate.collections) { - if (entry.type != RowGroupBatchType::NOT_FLUSHED) { - throw InternalException("Encountered a flushed batch"); - } - memory_manager.ReduceUnflushedMemory(entry.unflushed_memory); - entry.collection->Scan(transaction, [&](DataChunk &insert_chunk) { - storage.LocalAppend(append_state, context, insert_chunk, false); - return true; - }); + auto &optimistic_writer = data_table.GetOptimisticWriter(context); + optimistic_writer.Merge(*writer); + memory_manager.FinalCheck(); + return SinkFinalizeType::READY; + } + + // We are writing a small amount of data to disk. + // Thus, we append directly to the transaction local storage. + LocalAppendState append_state; + data_table.InitializeLocalAppend(append_state, table, context, bound_constraints); + auto &transaction = DuckTransaction::Get(context, table.catalog); + for (auto &entry : g_state.collections) { + if (entry.type != RowGroupBatchType::NOT_FLUSHED) { + throw InternalException("Encountered a flushed batch"); } - storage.FinalizeLocalAppend(append_state); + + memory_manager.ReduceUnflushedMemory(entry.unflushed_memory); + auto &collection = data_table.GetOptimisticCollection(context, entry.collection_index); + collection.Scan(transaction, [&](DataChunk &insert_chunk) { + data_table.LocalAppend(append_state, context, insert_chunk, false); + return true; + }); + data_table.ResetOptimisticCollection(context, entry.collection_index); } + + g_state.collections.clear(); + data_table.FinalizeLocalAppend(append_state); memory_manager.FinalCheck(); return SinkFinalizeType::READY; } diff --git a/src/execution/operator/persistent/physical_insert.cpp b/src/execution/operator/persistent/physical_insert.cpp index 161fa8f58a20..8d5572be200c 100644 --- a/src/execution/operator/persistent/physical_insert.cpp +++ b/src/execution/operator/persistent/physical_insert.cpp @@ -83,7 +83,8 @@ InsertGlobalState::InsertGlobalState(ClientContext &context, const vector &types, const vector> &bound_defaults, const vector> &bound_constraints) - : default_executor(context, bound_defaults), bound_constraints(bound_constraints) { + : default_executor(context, bound_defaults), collection_index(DConstants::INVALID_INDEX), + bound_constraints(bound_constraints) { auto &allocator = Allocator::Get(context); insert_chunk.Initialize(allocator, types); @@ -664,7 +665,7 @@ SinkResultType PhysicalInsert::Sink(ExecutionContext &context, DataChunk &chunk, gstate.insert_count += lstate.insert_chunk.size(); gstate.insert_count += updated_tuples; - if (!parallel && return_chunk) { + if (return_chunk) { gstate.return_collection.Append(lstate.insert_chunk); } storage.LocalAppend(gstate.append_state, context.client, lstate.insert_chunk, true); @@ -674,29 +675,35 @@ SinkResultType PhysicalInsert::Sink(ExecutionContext &context, DataChunk &chunk, // All of the tuples should have been turned into an update, leaving the chunk empty afterwards D_ASSERT(lstate.update_chunk.size() == 0); } - } else { - //! FIXME: can't we enable this by using a BatchedDataCollection ? - D_ASSERT(!return_chunk); - // parallel append - if (!lstate.local_collection) { - lock_guard l(gstate.lock); - auto table_info = storage.GetDataTableInfo(); - auto &io_manager = TableIOManager::Get(table.GetStorage()); - lstate.local_collection = make_uniq(std::move(table_info), io_manager, insert_types, - NumericCast(MAX_ROW_ID)); - lstate.local_collection->InitializeEmpty(); - lstate.local_collection->InitializeAppend(lstate.local_append_state); - lstate.writer = &gstate.table.GetStorage().CreateOptimisticWriter(context.client); - } - OnConflictHandling(table, context, gstate, lstate); - D_ASSERT(action_type != OnConflictAction::UPDATE); + return SinkResultType::NEED_MORE_INPUT; + } - auto new_row_group = lstate.local_collection->Append(lstate.insert_chunk, lstate.local_append_state); - if (new_row_group) { - lstate.writer->WriteNewRowGroup(*lstate.local_collection); - } + // Parallel append. + D_ASSERT(!return_chunk); + auto &data_table = gstate.table.GetStorage(); + if (!lstate.collection_index.IsValid()) { + auto table_info = storage.GetDataTableInfo(); + auto &io_manager = TableIOManager::Get(table.GetStorage()); + + // Create the local row group collection. + auto max_row_id = NumericCast(MAX_ROW_ID); + auto collection = make_uniq(std::move(table_info), io_manager, insert_types, max_row_id); + collection->InitializeEmpty(); + collection->InitializeAppend(lstate.local_append_state); + + lock_guard l(gstate.lock); + lstate.optimistic_writer = make_uniq(data_table); + lstate.collection_index = data_table.CreateOptimisticCollection(context.client, std::move(collection)); } + OnConflictHandling(table, context, gstate, lstate); + D_ASSERT(action_type != OnConflictAction::UPDATE); + + auto &collection = data_table.GetOptimisticCollection(context.client, lstate.collection_index); + auto new_row_group = collection.Append(lstate.insert_chunk, lstate.local_append_state); + if (new_row_group) { + lstate.optimistic_writer->WriteNewRowGroup(collection); + } return SinkResultType::NEED_MORE_INPUT; } @@ -707,7 +714,7 @@ SinkCombineResultType PhysicalInsert::Combine(ExecutionContext &context, Operato context.thread.profiler.Flush(*this); client_profiler.Flush(context.thread.profiler); - if (!parallel || !lstate.local_collection) { + if (!parallel || !lstate.collection_index.IsValid()) { return SinkCombineResultType::FINISHED; } @@ -717,9 +724,11 @@ SinkCombineResultType PhysicalInsert::Combine(ExecutionContext &context, Operato // parallel append: finalize the append TransactionData tdata(0, 0); - lstate.local_collection->FinalizeAppend(tdata, lstate.local_append_state); + auto &data_table = gstate.table.GetStorage(); + auto &collection = data_table.GetOptimisticCollection(context.client, lstate.collection_index); + collection.FinalizeAppend(tdata, lstate.local_append_state); - auto append_count = lstate.local_collection->GetTotalRows(); + auto append_count = collection.GetTotalRows(); lock_guard lock(gstate.lock); gstate.insert_count += append_count; @@ -727,17 +736,18 @@ SinkCombineResultType PhysicalInsert::Combine(ExecutionContext &context, Operato // we have few rows - append to the local storage directly storage.InitializeLocalAppend(gstate.append_state, table, context.client, bound_constraints); auto &transaction = DuckTransaction::Get(context.client, table.catalog); - lstate.local_collection->Scan(transaction, [&](DataChunk &insert_chunk) { + collection.Scan(transaction, [&](DataChunk &insert_chunk) { storage.LocalAppend(gstate.append_state, context.client, insert_chunk, false); return true; }); storage.FinalizeLocalAppend(gstate.append_state); } else { // we have written rows to disk optimistically - merge directly into the transaction-local storage - lstate.writer->WriteLastRowGroup(*lstate.local_collection); - lstate.writer->FinalFlush(); - gstate.table.GetStorage().LocalMerge(context.client, *lstate.local_collection); - gstate.table.GetStorage().FinalizeOptimisticWriter(context.client, *lstate.writer); + lstate.optimistic_writer->WriteLastRowGroup(collection); + lstate.optimistic_writer->FinalFlush(); + gstate.table.GetStorage().LocalMerge(context.client, collection); + auto &optimistic_writer = gstate.table.GetStorage().GetOptimisticWriter(context.client); + optimistic_writer.Merge(*lstate.optimistic_writer); } return SinkCombineResultType::FINISHED; diff --git a/src/execution/operator/scan/physical_table_scan.cpp b/src/execution/operator/scan/physical_table_scan.cpp index 0ea996d341c8..2c821526cee6 100644 --- a/src/execution/operator/scan/physical_table_scan.cpp +++ b/src/execution/operator/scan/physical_table_scan.cpp @@ -14,11 +14,12 @@ PhysicalTableScan::PhysicalTableScan(vector types, TableFunction fu vector column_ids_p, vector projection_ids_p, vector names_p, unique_ptr table_filters_p, idx_t estimated_cardinality, ExtraOperatorInfo extra_info, - vector parameters_p) + vector parameters_p, virtual_column_map_t virtual_columns_p) : PhysicalOperator(PhysicalOperatorType::TABLE_SCAN, std::move(types), estimated_cardinality), function(std::move(function_p)), bind_data(std::move(bind_data_p)), returned_types(std::move(returned_types_p)), column_ids(std::move(column_ids_p)), projection_ids(std::move(projection_ids_p)), names(std::move(names_p)), - table_filters(std::move(table_filters_p)), extra_info(extra_info), parameters(std::move(parameters_p)) { + table_filters(std::move(table_filters_p)), extra_info(extra_info), parameters(std::move(parameters_p)), + virtual_columns(std::move(virtual_columns_p)) { } class TableScanGlobalSourceState : public GlobalSourceState { @@ -214,8 +215,12 @@ InsertionOrderPreservingMap PhysicalTableScan::ParamsToString() const { first_item = false; const auto col_id = column_ids[column_index].GetPrimaryIndex(); - if (col_id == COLUMN_IDENTIFIER_ROW_ID) { - filters_info += filter->ToString("rowid"); + if (IsVirtualColumn(col_id)) { + auto entry = virtual_columns.find(col_id); + if (entry == virtual_columns.end()) { + throw InternalException("Virtual column not found"); + } + filters_info += filter->ToString(entry->second.name); } else { filters_info += filter->ToString(names[col_id]); } diff --git a/src/execution/operator/schema/physical_create_type.cpp b/src/execution/operator/schema/physical_create_type.cpp index 68bc258b36d6..e73ca2662dc8 100644 --- a/src/execution/operator/schema/physical_create_type.cpp +++ b/src/execution/operator/schema/physical_create_type.cpp @@ -51,7 +51,7 @@ SinkResultType PhysicalCreateType::Sink(ExecutionContext &context, DataChunk &ch for (idx_t i = 0; i < chunk.size(); i++) { idx_t idx = sdata.sel->get_index(i); if (!sdata.validity.RowIsValid(idx)) { - throw InvalidInputException("Attempted to create ENUM type with NULL value!"); + continue; } auto str = src_ptr[idx]; auto entry = gstate.found_strings.find(src_ptr[idx]); diff --git a/src/execution/physical_plan/plan_asof_join.cpp b/src/execution/physical_plan/plan_asof_join.cpp index 927defa4ff27..32fa52280e62 100644 --- a/src/execution/physical_plan/plan_asof_join.cpp +++ b/src/execution/physical_plan/plan_asof_join.cpp @@ -1,8 +1,14 @@ +#include "duckdb/catalog/catalog_entry/aggregate_function_catalog_entry.hpp" +#include "duckdb/execution/operator/aggregate/physical_hash_aggregate.hpp" +#include "duckdb/execution/operator/aggregate/physical_streaming_window.hpp" #include "duckdb/execution/operator/aggregate/physical_window.hpp" #include "duckdb/execution/operator/join/physical_asof_join.hpp" #include "duckdb/execution/operator/join/physical_iejoin.hpp" +#include "duckdb/execution/operator/join/physical_nested_loop_join.hpp" #include "duckdb/execution/operator/projection/physical_projection.hpp" +#include "duckdb/function/aggregate/distributive_function_utils.hpp" #include "duckdb/execution/physical_plan_generator.hpp" +#include "duckdb/function/function_binder.hpp" #include "duckdb/main/client_context.hpp" #include "duckdb/planner/expression/bound_constant_expression.hpp" #include "duckdb/planner/expression/bound_reference_expression.hpp" @@ -10,6 +16,223 @@ namespace duckdb { +static unique_ptr PlanAsOfLoopJoin(LogicalComparisonJoin &op, unique_ptr &probe, + unique_ptr &build, ClientContext &context) { + + // Plan a inverse nested loop join, then aggregate the values to choose the optimal match for each probe row. + // Use a row number primary key to handle duplicate probe values. + // aggregate the fields to produce at most one match per probe row, + // then project the columns back into the correct order and drop the primary key. + // + // ∏ * \ pk + // | + // Γ pk;first(P),arg_xxx(B,inequality) + // | + // ∏ *,inequality + // | + // ⨝ swapped + // / \  + // B W pk:row_number + // | + // P + + LogicalComparisonJoin join_op(InverseJoinType(op.join_type)); + + join_op.types = op.children[1]->types; + const auto &probe_types = op.children[0]->types; + join_op.types.insert(join_op.types.end(), probe_types.begin(), probe_types.end()); + + // Fill in the projection maps to simplify the code below + // Since NLJ doesn't support projection, but ASOF does, + // we have to track this carefully... + join_op.left_projection_map = op.right_projection_map; + if (join_op.left_projection_map.empty()) { + for (idx_t i = 0; i < op.children[1]->types.size(); ++i) { + join_op.left_projection_map.emplace_back(i); + } + } + + join_op.right_projection_map = op.left_projection_map; + if (join_op.right_projection_map.empty()) { + for (idx_t i = 0; i < op.children[0]->types.size(); ++i) { + join_op.right_projection_map.emplace_back(i); + } + } + + // Project pk + LogicalType pk_type = LogicalType::BIGINT; + join_op.types.emplace_back(pk_type); + + auto binder = Binder::CreateBinder(context); + FunctionBinder function_binder(*binder); + auto asof_idx = op.conditions.size(); + string arg_min_max; + for (idx_t i = 0; i < op.conditions.size(); ++i) { + const auto &cond = op.conditions[i]; + JoinCondition nested_cond; + nested_cond.left = cond.right->Copy(); + nested_cond.right = cond.left->Copy(); + if (!nested_cond.left || !nested_cond.right) { + return nullptr; + } + nested_cond.comparison = FlipComparisonExpression(cond.comparison); + join_op.conditions.emplace_back(std::move(nested_cond)); + switch (cond.comparison) { + case ExpressionType::COMPARE_GREATERTHANOREQUALTO: + case ExpressionType::COMPARE_GREATERTHAN: + D_ASSERT(asof_idx == op.conditions.size()); + asof_idx = i; + arg_min_max = "arg_max"; + break; + case ExpressionType::COMPARE_LESSTHANOREQUALTO: + case ExpressionType::COMPARE_LESSTHAN: + D_ASSERT(asof_idx == op.conditions.size()); + asof_idx = i; + arg_min_max = "arg_min"; + break; + default: + break; + } + } + + // NLJ does not support some join types + switch (join_op.join_type) { + case JoinType::SEMI: + case JoinType::ANTI: + case JoinType::MARK: + case JoinType::INNER: + case JoinType::RIGHT: + // Unfortunately, this does not check all the join types... + if (!PhysicalNestedLoopJoin::IsSupported(op.conditions, op.join_type)) { + return nullptr; + } + break; + case JoinType::OUTER: + case JoinType::LEFT: + // RIGHT ASOF JOINs produce the entire build table and would require grouping on all build rows, + // which defeats the purpose of this optimisation. + default: + return nullptr; + } + + QueryErrorContext error_context; + auto arg_min_max_func = binder->GetCatalogEntry(CatalogType::SCALAR_FUNCTION_ENTRY, SYSTEM_CATALOG, DEFAULT_SCHEMA, + arg_min_max, OnEntryNotFound::RETURN_NULL, error_context); + // Can't find the arg_min/max aggregate we need, so give up before we break anything. + if (!arg_min_max_func || arg_min_max_func->type != CatalogType::AGGREGATE_FUNCTION_ENTRY) { + return nullptr; + } + auto &arg_min_max_entry = arg_min_max_func->Cast(); + + // PhysicalHashAggregate requires that the arguments to aggregate functions be bound references, + // so we Project the (shared) ordering argument on the end of the join results. + vector> comp_list; + for (const auto &col_type : join_op.types) { + const auto col_idx = comp_list.size(); + comp_list.emplace_back(make_uniq(col_type, col_idx)); + } + vector comp_types = join_op.types; + auto comp_expr = op.conditions[asof_idx].right->Copy(); + comp_types.emplace_back(comp_expr->return_type); + comp_list.emplace_back(std::move(comp_expr)); + + // Bind the aggregates first so we can abort safely if we can't find one. + vector aggr_types(1, pk_type); + + // Wrap all the projected non-pk probe fields in `first` aggregates; + vector> aggregates; + for (const auto &i : join_op.right_projection_map) { + const auto col_idx = op.children[1]->types.size() + i; + const auto col_type = join_op.types[col_idx]; + aggr_types.emplace_back(col_type); + + vector> aggr_children; + auto col_ref = make_uniq(col_type, col_idx); + aggr_children.push_back(std::move(col_ref)); + + auto first_aggregate = FirstFunctionGetter::GetFunction(col_type); + auto aggr_expr = make_uniq(std::move(first_aggregate), std::move(aggr_children), + nullptr, nullptr, AggregateType::NON_DISTINCT); + D_ASSERT(col_type == aggr_expr->return_type); + aggregates.emplace_back(std::move(aggr_expr)); + } + + // Wrap all the projected build fields in `arg_max/min` aggregates using the inequality ordering; + // We are doing all this first in case we can't find a matching function. + for (const auto &col_idx : join_op.left_projection_map) { + const auto col_type = join_op.types[col_idx]; + aggr_types.emplace_back(col_type); + + vector> aggr_children; + auto col_ref = make_uniq(col_type, col_idx); + aggr_children.push_back(std::move(col_ref)); + auto comp_expr = make_uniq(comp_types.back(), comp_types.size() - 1); + aggr_children.push_back(std::move(comp_expr)); + vector child_types; + for (const auto &child : aggr_children) { + child_types.emplace_back(child->return_type); + } + + auto &func = arg_min_max_entry; + ErrorData error; + auto best_function = function_binder.BindFunction(func.name, func.functions, child_types, error); + if (!best_function.IsValid()) { + return nullptr; + } + auto bound_function = func.functions.GetFunctionByOffset(best_function.GetIndex()); + auto aggr_expr = function_binder.BindAggregateFunction(bound_function, std::move(aggr_children), nullptr, + AggregateType::NON_DISTINCT); + D_ASSERT(col_type == aggr_expr->return_type); + aggregates.emplace_back(std::move(aggr_expr)); + } + + // Add a synthetic primary integer key to the probe relation using streaming windowing. + vector> window_select; + auto pk = make_uniq(ExpressionType::WINDOW_ROW_NUMBER, pk_type, nullptr, nullptr); + pk->start = WindowBoundary::UNBOUNDED_PRECEDING; + pk->end = WindowBoundary::CURRENT_ROW_ROWS; + pk->alias = "row_number"; + window_select.emplace_back(std::move(pk)); + + auto window_types = probe->types; + window_types.emplace_back(pk_type); + + idx_t probe_cardinality = op.children[0]->EstimateCardinality(context); + auto window = make_uniq(window_types, std::move(window_select), probe_cardinality); + window->children.emplace_back(std::move(probe)); + + auto join = make_uniq(join_op, std::move(build), std::move(window), + std::move(join_op.conditions), join_op.join_type, probe_cardinality); + + // Plan a projection of the compare column + auto comp = make_uniq(std::move(comp_types), std::move(comp_list), probe_cardinality); + comp->children.emplace_back(std::move(join)); + + // Plan an aggregation on the output of the join, grouping by key; + // TODO: Can we make it perfect? + // Note that the NLJ produced all fields, but only the projected ones were aggregated + vector> groups; + auto pk_ref = make_uniq(pk_type, join_op.types.size() - 1); + groups.emplace_back(std::move(pk_ref)); + auto aggr = make_uniq(context, aggr_types, std::move(aggregates), std::move(groups), + probe_cardinality); + aggr->children.emplace_back(std::move(comp)); + + // Project away primary/grouping key + // The aggregates were generated in the output order of the original ASOF, + // so we just have to shift away the pk + vector> project_list; + for (column_t i = 1; i < aggr->types.size(); ++i) { + auto col_ref = make_uniq(aggr->types[i], i); + project_list.emplace_back(std::move(col_ref)); + } + + auto proj = make_uniq(op.types, std::move(project_list), probe_cardinality); + proj->children.emplace_back(std::move(aggr)); + + return std::move(proj); +} + unique_ptr PhysicalPlanGenerator::PlanAsOfJoin(LogicalComparisonJoin &op) { // now visit the children D_ASSERT(op.children.size() == 2); @@ -42,7 +265,14 @@ unique_ptr PhysicalPlanGenerator::PlanAsOfJoin(LogicalComparis } D_ASSERT(asof_idx < op.conditions.size()); - if (!ClientConfig::GetConfig(context).force_asof_iejoin) { + auto &config = ClientConfig::GetConfig(context); + if (!config.force_asof_iejoin) { + if (op.children[0]->has_estimated_cardinality && lhs_cardinality <= config.asof_loop_join_threshold) { + auto result = PlanAsOfLoopJoin(op, left, right, context); + if (result) { + return result; + } + } return make_uniq(op, std::move(left), std::move(right)); } diff --git a/src/execution/physical_plan/plan_get.cpp b/src/execution/physical_plan/plan_get.cpp index 3b5d940eb924..978bf4b70220 100644 --- a/src/execution/physical_plan/plan_get.cpp +++ b/src/execution/physical_plan/plan_get.cpp @@ -133,9 +133,10 @@ unique_ptr PhysicalPlanGenerator::CreatePlan(LogicalGet &op) { // create the table scan node if (!op.function.projection_pushdown) { // function does not support projection pushdown - auto node = make_uniq( - op.returned_types, op.function, std::move(op.bind_data), op.returned_types, column_ids, vector(), - op.names, std::move(table_filters), op.estimated_cardinality, op.extra_info, std::move(op.parameters)); + auto node = make_uniq(op.returned_types, op.function, std::move(op.bind_data), + op.returned_types, column_ids, vector(), op.names, + std::move(table_filters), op.estimated_cardinality, op.extra_info, + std::move(op.parameters), std::move(op.virtual_columns)); // first check if an additional projection is necessary if (column_ids.size() == op.returned_types.size()) { bool projection_necessary = false; @@ -159,10 +160,8 @@ unique_ptr PhysicalPlanGenerator::CreatePlan(LogicalGet &op) { vector types; vector> expressions; for (auto &column_id : column_ids) { - if (column_id.IsRowIdColumn()) { - types.emplace_back(op.GetRowIdType()); - // Now how to make that a constant expression. - expressions.push_back(make_uniq(Value(op.GetRowIdType()))); + if (column_id.IsVirtualColumn()) { + throw NotImplementedException("Virtual columns require projection pushdown"); } else { auto col_id = column_id.GetPrimaryIndex(); auto type = op.returned_types[col_id]; @@ -182,7 +181,8 @@ unique_ptr PhysicalPlanGenerator::CreatePlan(LogicalGet &op) { } else { auto node = make_uniq(op.types, op.function, std::move(op.bind_data), op.returned_types, column_ids, op.projection_ids, op.names, std::move(table_filters), - op.estimated_cardinality, op.extra_info, std::move(op.parameters)); + op.estimated_cardinality, op.extra_info, std::move(op.parameters), + std::move(op.virtual_columns)); node->dynamic_filters = op.dynamic_filters; if (filter) { filter->children.push_back(std::move(node)); diff --git a/src/execution/physical_plan/plan_sample.cpp b/src/execution/physical_plan/plan_sample.cpp index be55784779fb..883c7055d46f 100644 --- a/src/execution/physical_plan/plan_sample.cpp +++ b/src/execution/physical_plan/plan_sample.cpp @@ -28,9 +28,7 @@ unique_ptr PhysicalPlanGenerator::CreatePlan(LogicalSample &op "reservoir sampling or use a sample_size", EnumUtil::ToString(op.sample_options->method)); } - sample = make_uniq( - op.types, op.sample_options->method, op.sample_options->sample_size.GetValue(), - static_cast(op.sample_options->seed.GetIndex()), op.estimated_cardinality); + sample = make_uniq(op.types, std::move(op.sample_options), op.estimated_cardinality); break; default: throw InternalException("Unimplemented sample method"); diff --git a/src/function/aggregate/distributive/minmax.cpp b/src/function/aggregate/distributive/minmax.cpp index b862bf6d9623..32a9518e1889 100644 --- a/src/function/aggregate/distributive/minmax.cpp +++ b/src/function/aggregate/distributive/minmax.cpp @@ -412,8 +412,8 @@ class MinMaxNState { UnaryAggregateHeap heap; bool is_initialized = false; - void Initialize(idx_t nval) { - heap.Initialize(nval); + void Initialize(ArenaAllocator &allocator, idx_t nval) { + heap.Initialize(allocator, nval); is_initialized = true; } @@ -432,7 +432,7 @@ static void MinMaxNUpdate(Vector inputs[], AggregateInputData &aggr_input, idx_t UnifiedVectorFormat val_format; UnifiedVectorFormat n_format; UnifiedVectorFormat state_format; - ; + auto val_extra_state = STATE::VAL_TYPE::CreateExtraState(val_vector, count); STATE::VAL_TYPE::PrepareData(val_vector, count, val_extra_state, val_format); @@ -464,7 +464,7 @@ static void MinMaxNUpdate(Vector inputs[], AggregateInputData &aggr_input, idx_t if (nval >= MAX_N) { throw InvalidInputException("Invalid input for MIN/MAX: n value must be < %d", MAX_N); } - state.Initialize(UnsafeNumericCast(nval)); + state.Initialize(aggr_input.allocator, UnsafeNumericCast(nval)); } // Now add the input to the heap diff --git a/src/function/cast/string_cast.cpp b/src/function/cast/string_cast.cpp index f3a19c4273bf..07375bf7bd37 100644 --- a/src/function/cast/string_cast.cpp +++ b/src/function/cast/string_cast.cpp @@ -160,9 +160,9 @@ bool VectorStringToList::StringToNestedTypeCastLoop(const string_t *source_data, list_data[i].offset = total; if (!VectorStringToList::SplitStringList(source_data[idx], child_data, total, varchar_vector)) { - string text = "Type VARCHAR with value '" + source_data[idx].GetString() + - "' can't be cast to the destination type LIST"; - HandleVectorCastError::Operation(text, result_mask, i, vector_cast_data); + auto error = StringUtil::Format("Type VARCHAR with value '%s' can't be cast to the destination type %s", + source_data[idx].GetString(), result.GetType().ToString()); + HandleVectorCastError::Operation(error, result_mask, i, vector_cast_data); } list_data[i].length = total - list_data[i].offset; // length is the amount of parts coming from this string } @@ -233,16 +233,13 @@ bool VectorStringToStruct::StringToNestedTypeCastLoop(const string_t *source_dat result_mask.SetInvalid(i); continue; } - if (is_unnamed) { - throw ConversionException("Casting strings to unnamed structs is unsupported"); - } if (!VectorStringToStruct::SplitStruct(source_data[idx], child_vectors, i, child_names, child_masks)) { - string text = "Type VARCHAR with value '" + source_data[idx].GetString() + - "' can't be cast to the destination type STRUCT"; + auto error = StringUtil::Format("Type VARCHAR with value '%s' can't be cast to the destination type %s", + source_data[idx].GetString(), result.GetType().ToString()); for (auto &child_mask : child_masks) { child_mask.get().SetInvalid(i); // some values may have already been found and set valid } - HandleVectorCastError::Operation(text, result_mask, i, vector_cast_data); + HandleVectorCastError::Operation(error, result_mask, i, vector_cast_data); } } @@ -319,10 +316,10 @@ bool VectorStringToMap::StringToNestedTypeCastLoop(const string_t *source_data, list_data[i].offset = total; if (!VectorStringToMap::SplitStringMap(source_data[idx], child_key_data, child_val_data, total, varchar_key_vector, varchar_val_vector)) { - string text = "Type VARCHAR with value '" + source_data[idx].GetString() + - "' can't be cast to the destination type MAP"; + auto error = StringUtil::Format("Type VARCHAR with value '%s' can't be cast to the destination type %s", + source_data[idx].GetString(), result.GetType().ToString()); FlatVector::SetNull(result, i, true); - HandleVectorCastError::Operation(text, result_mask, i, vector_cast_data); + HandleVectorCastError::Operation(error, result_mask, i, vector_cast_data); } list_data[i].length = total - list_data[i].offset; } @@ -382,10 +379,9 @@ bool VectorStringToArray::StringToNestedTypeCastLoop(const string_t *source_data if (array_size != str_array_size) { if (all_lengths_match) { all_lengths_match = false; - auto msg = - StringUtil::Format("Type VARCHAR with value '%s' can't be cast to the destination type ARRAY[%u]" - ", the size of the array must match the destination type", - source_data[idx].GetString(), array_size); + auto msg = StringUtil::Format("Type VARCHAR with value '%s' can't be cast to the destination type %s" + ", the size of the array must match the destination type", + source_data[idx].GetString(), result.GetType().ToString()); if (parameters.strict) { throw ConversionException(msg); } @@ -421,9 +417,9 @@ bool VectorStringToArray::StringToNestedTypeCastLoop(const string_t *source_data } if (!VectorStringToList::SplitStringList(source_data[idx], child_data, total, varchar_vector)) { - auto text = StringUtil::Format("Type VARCHAR with value '%s' can't be cast to the destination type ARRAY", - source_data[idx].GetString()); - HandleVectorCastError::Operation(text, result_mask, i, vector_cast_data); + auto error = StringUtil::Format("Type VARCHAR with value '%s' can't be cast to the destination type %s", + source_data[idx].GetString(), result.GetType().ToString()); + HandleVectorCastError::Operation(error, result_mask, i, vector_cast_data); } } D_ASSERT(total == child_count); diff --git a/src/function/cast/vector_cast_helpers.cpp b/src/function/cast/vector_cast_helpers.cpp index c7aa523eaa0d..c0790d56b4ce 100644 --- a/src/function/cast/vector_cast_helpers.cpp +++ b/src/function/cast/vector_cast_helpers.cpp @@ -1,85 +1,107 @@ #include "duckdb/function/cast/vector_cast_helpers.hpp" +#include "duckdb/common/stack.hpp" +#include "duckdb/common/typedefs.hpp" + +namespace { + +struct StringCastInputState { +public: + StringCastInputState(const char *buf, duckdb::idx_t &pos, duckdb::idx_t &len) : buf(buf), pos(pos), len(len) { + } + +public: + const char *buf; + duckdb::idx_t &pos; + duckdb::idx_t &len; + bool escaped = false; +}; + +} // namespace namespace duckdb { // ------- Helper functions for splitting string nested types ------- -static bool IsNull(const char *buf, idx_t start_pos, Vector &child, idx_t row_idx) { - if ((buf[start_pos] == 'N' || buf[start_pos] == 'n') && (buf[start_pos + 1] == 'U' || buf[start_pos + 1] == 'u') && - (buf[start_pos + 2] == 'L' || buf[start_pos + 2] == 'l') && - (buf[start_pos + 3] == 'L' || buf[start_pos + 3] == 'l')) { - FlatVector::SetNull(child, row_idx, true); - return true; +static bool IsNull(const char *buf, idx_t pos, idx_t end_pos) { + if (pos + 4 != end_pos) { + return false; } - return false; + return StringUtil::CIEquals(string(buf + pos, buf + pos + 4), "null"); } -inline static void SkipWhitespace(const char *buf, idx_t &pos, idx_t len) { +inline static void SkipWhitespace(StringCastInputState &input_state) { + auto &buf = input_state.buf; + auto &pos = input_state.pos; + auto &len = input_state.len; while (pos < len && StringUtil::CharacterIsSpace(buf[pos])) { pos++; + input_state.escaped = false; } } -static bool SkipToCloseQuotes(idx_t &pos, const char *buf, idx_t &len) { +static bool SkipToCloseQuotes(StringCastInputState &input_state) { + auto &buf = input_state.buf; + auto &pos = input_state.pos; + auto &len = input_state.len; + auto &escaped = input_state.escaped; + char quote = buf[pos]; pos++; - bool escaped = false; while (pos < len) { + bool set_escaped = false; if (buf[pos] == '\\') { - escaped = !escaped; + if (!escaped) { + set_escaped = true; + } } else { if (buf[pos] == quote && !escaped) { return true; } - escaped = false; } + escaped = set_escaped; pos++; } return false; } -static bool SkipToClose(idx_t &idx, const char *buf, idx_t &len, idx_t &lvl, char close_bracket) { - idx++; +static bool SkipToClose(StringCastInputState &input_state) { + auto &idx = input_state.pos; + auto &buf = input_state.buf; + auto &len = input_state.len; + + D_ASSERT(buf[idx] == '{' || buf[idx] == '[' || buf[idx] == '('); vector brackets; - brackets.push_back(close_bracket); while (idx < len) { + bool set_escaped = false; if (buf[idx] == '"' || buf[idx] == '\'') { - if (!SkipToCloseQuotes(idx, buf, len)) { - return false; + if (!input_state.escaped) { + if (!SkipToCloseQuotes(input_state)) { + return false; + } } } else if (buf[idx] == '{') { brackets.push_back('}'); + } else if (buf[idx] == '(') { + brackets.push_back(')'); } else if (buf[idx] == '[') { brackets.push_back(']'); - lvl++; } else if (buf[idx] == brackets.back()) { - if (buf[idx] == ']') { - lvl--; - } brackets.pop_back(); if (brackets.empty()) { return true; } + } else if (buf[idx] == '\\') { + //! Note that we don't treat `\\` special here, backslashes can't be escaped outside of quotes + //! backslashes within quotes will not be encountered in this function + set_escaped = true; } + input_state.escaped = set_escaped; idx++; } return false; } -static idx_t StringTrim(const char *buf, idx_t &start_pos, idx_t pos) { - idx_t trailing_whitespace = 0; - while (pos > start_pos && StringUtil::CharacterIsSpace(buf[pos - trailing_whitespace - 1])) { - trailing_whitespace++; - } - if ((buf[start_pos] == '"' && buf[pos - trailing_whitespace - 1] == '"') || - (buf[start_pos] == '\'' && buf[pos - trailing_whitespace - 1] == '\'')) { - start_pos++; - trailing_whitespace++; - } - return (pos - trailing_whitespace); -} - struct CountPartOperation { idx_t count = 0; @@ -92,75 +114,200 @@ struct CountPartOperation { } }; +template +static string_t HandleString(Vector &vec, const char *buf, idx_t start, idx_t end) { + D_ASSERT(start <= end); + auto length = end - start; + auto allocated_string = StringVector::EmptyString(vec, length); + auto string_data = allocated_string.GetDataWriteable(); + uint32_t copied_count = 0; + bool escaped = false; + + bool quoted = false; + char quote_char; + stack scopes; + for (idx_t i = 0; i < length; i++) { + auto current_char = buf[start + i]; + if (!escaped) { + if (scopes.empty() && current_char == '\\') { + if (quoted || (start + i + 1 < end && (buf[start + i + 1] == '\'' || buf[start + i + 1] == '"'))) { + //! Start of escape + escaped = true; + continue; + } + } + if (scopes.empty() && (current_char == '\'' || current_char == '"')) { + if (quoted && current_char == quote_char) { + quoted = false; + //! Skip the ending quote + continue; + } else if (!quoted) { + quoted = true; + quote_char = current_char; + //! Skip the starting quote + continue; + } + } + if (!quoted && !scopes.empty() && current_char == scopes.top()) { + //! Close scope + scopes.pop(); + } + if (!quoted && (current_char == '[' || current_char == '{' || current_char == '(')) { + if (RESPECT_SCOPES) { + //! 'RESPECT_SCOPES' is false in things like STRUCT keys, these are regular strings + //! New scope + char end_char; + if (current_char == '[') { + end_char = ']'; + } else if (current_char == '{') { + end_char = '}'; + } else { + D_ASSERT(current_char == '('); + end_char = ')'; + } + scopes.push(end_char); + } + } + //! Regular character + string_data[copied_count++] = current_char; + } else { + string_data[copied_count++] = current_char; + escaped = false; + } + } + return string_t((const char *)string_data, copied_count); // NOLINT +} + // ------- LIST SPLIT ------- struct SplitStringListOperation { - SplitStringListOperation(string_t *child_data, idx_t &child_start, Vector &child) - : child_data(child_data), child_start(child_start), child(child) { +public: + SplitStringListOperation(string_t *child_data, idx_t &entry_count, Vector &child) + : child_data(child_data), entry_count(entry_count), child(child) { + } + +public: + void HandleValue(const char *buf, idx_t start, idx_t end) { + if (IsNull(buf, start, end)) { + FlatVector::SetNull(child, entry_count, true); + entry_count++; + return; + } + child_data[entry_count] = HandleString(child, buf, start, end); + entry_count++; } +private: string_t *child_data; - idx_t &child_start; + idx_t &entry_count; Vector &child; +}; - void HandleValue(const char *buf, idx_t start_pos, idx_t pos) { - if ((pos - start_pos) == 4 && IsNull(buf, start_pos, child, child_start)) { - child_start++; - return; +static inline bool ValueStateTransition(StringCastInputState &input_state, optional_idx &start_pos, idx_t &end_pos) { + auto &buf = input_state.buf; + auto &pos = input_state.pos; + + bool set_escaped = false; + if (buf[pos] == '"' || buf[pos] == '\'') { + if (!start_pos.IsValid()) { + start_pos = pos; } - if (start_pos > pos) { - pos = start_pos; + if (!input_state.escaped) { + if (!SkipToCloseQuotes(input_state)) { + return false; + } } - child_data[child_start] = StringVector::AddString(child, buf + start_pos, pos - start_pos); - child_start++; + end_pos = pos; + } else if (buf[pos] == '{') { + if (!start_pos.IsValid()) { + start_pos = pos; + } + if (!SkipToClose(input_state)) { + return false; + } + end_pos = pos; + } else if (buf[pos] == '(') { + if (!start_pos.IsValid()) { + start_pos = pos; + } + if (!SkipToClose(input_state)) { + return false; + } + end_pos = pos; + } else if (buf[pos] == '[') { + if (!start_pos.IsValid()) { + start_pos = pos; + } + if (!SkipToClose(input_state)) { + return false; + } + end_pos = pos; + } else if (buf[pos] == '\\') { + if (!start_pos.IsValid()) { + start_pos = pos; + } + set_escaped = true; + end_pos = pos; + } else if (!StringUtil::CharacterIsSpace(buf[pos])) { + if (!start_pos.IsValid()) { + start_pos = pos; + } + end_pos = pos; } -}; + input_state.escaped = set_escaped; + pos++; + + return true; +} template static bool SplitStringListInternal(const string_t &input, OP &state) { const char *buf = input.GetData(); idx_t len = input.GetSize(); - idx_t lvl = 1; idx_t pos = 0; - bool seen_value = false; - SkipWhitespace(buf, pos, len); + StringCastInputState input_state(buf, pos, len); + + SkipWhitespace(input_state); if (pos == len || buf[pos] != '[') { + //! Does not have a valid list start return false; } - SkipWhitespace(buf, ++pos, len); - idx_t start_pos = pos; + //! Skip the '[' + pos++; + SkipWhitespace(input_state); + bool seen_value = false; while (pos < len) { - if (buf[pos] == '[') { - if (!SkipToClose(pos, buf, len, ++lvl, ']')) { + optional_idx start_pos; + idx_t end_pos; + + while (pos < len && (buf[pos] != ',' && buf[pos] != ']')) { + if (!ValueStateTransition(input_state, start_pos, end_pos)) { return false; } - } else if ((buf[pos] == '"' || buf[pos] == '\'') && pos == start_pos) { - SkipToCloseQuotes(pos, buf, len); - } else if (buf[pos] == '{') { - idx_t struct_lvl = 0; - SkipToClose(pos, buf, len, struct_lvl, '}'); - } else if (buf[pos] == ',' || buf[pos] == ']') { - idx_t trailing_whitespace = 0; - while (StringUtil::CharacterIsSpace(buf[pos - trailing_whitespace - 1])) { - trailing_whitespace++; - } - if (buf[pos] != ']' || start_pos != pos || seen_value) { - state.HandleValue(buf, start_pos, pos - trailing_whitespace); - seen_value = true; - } - if (buf[pos] == ']') { - lvl--; - break; + } + if (pos == len) { + return false; + } + if (buf[pos] != ']' || start_pos.IsValid() || seen_value) { + if (!start_pos.IsValid()) { + state.HandleValue(buf, 0, 0); + } else { + auto start = start_pos.GetIndex(); + state.HandleValue(buf, start, end_pos + 1); } - SkipWhitespace(buf, ++pos, len); - start_pos = pos; - continue; + seen_value = true; + } + if (buf[pos] == ']') { + break; } + pos++; + SkipWhitespace(input_state); } - SkipWhitespace(buf, ++pos, len); - return (pos == len && lvl == 0); + pos++; + SkipWhitespace(input_state); + return (pos == len); } bool VectorStringToList::SplitStringList(const string_t &input, string_t *child_data, idx_t &child_start, @@ -190,78 +337,95 @@ struct SplitStringMapOperation { Vector &varchar_val; bool HandleKey(const char *buf, idx_t start_pos, idx_t pos) { - if ((pos - start_pos) == 4 && IsNull(buf, start_pos, varchar_key, child_start)) { + if (IsNull(buf, start_pos, pos)) { FlatVector::SetNull(varchar_val, child_start, true); + FlatVector::SetNull(varchar_key, child_start, true); child_start++; return false; } - child_key_data[child_start] = StringVector::AddString(varchar_key, buf + start_pos, pos - start_pos); + child_key_data[child_start] = HandleString(varchar_key, buf, start_pos, pos); return true; } void HandleValue(const char *buf, idx_t start_pos, idx_t pos) { - if ((pos - start_pos) == 4 && IsNull(buf, start_pos, varchar_val, child_start)) { + if (IsNull(buf, start_pos, pos)) { + FlatVector::SetNull(varchar_val, child_start, true); child_start++; return; } - child_val_data[child_start] = StringVector::AddString(varchar_val, buf + start_pos, pos - start_pos); + child_val_data[child_start] = HandleString(varchar_val, buf, start_pos, pos); child_start++; } }; -template -static bool FindKeyOrValueMap(const char *buf, idx_t len, idx_t &pos, OP &state, bool key) { - auto start_pos = pos; - idx_t lvl = 0; - while (pos < len) { - if (buf[pos] == '"' || buf[pos] == '\'') { - SkipToCloseQuotes(pos, buf, len); - } else if (buf[pos] == '{') { - SkipToClose(pos, buf, len, lvl, '}'); - } else if (buf[pos] == '[') { - SkipToClose(pos, buf, len, lvl, ']'); - } else if (key && buf[pos] == '=') { - idx_t end_pos = StringTrim(buf, start_pos, pos); - return state.HandleKey(buf, start_pos, end_pos); // put string in KEY_child_vector - } else if (!key && (buf[pos] == ',' || buf[pos] == '}')) { - idx_t end_pos = StringTrim(buf, start_pos, pos); - state.HandleValue(buf, start_pos, end_pos); // put string in VALUE_child_vector - return true; - } - pos++; - } - return false; -} - template static bool SplitStringMapInternal(const string_t &input, OP &state) { const char *buf = input.GetData(); idx_t len = input.GetSize(); idx_t pos = 0; + StringCastInputState input_state(buf, pos, len); - SkipWhitespace(buf, pos, len); + SkipWhitespace(input_state); if (pos == len || buf[pos] != '{') { return false; } - SkipWhitespace(buf, ++pos, len); + pos++; + SkipWhitespace(input_state); if (pos == len) { return false; } if (buf[pos] == '}') { - SkipWhitespace(buf, ++pos, len); - return (pos == len); + pos++; + SkipWhitespace(input_state); + return pos == len; } + while (pos < len) { - if (!FindKeyOrValueMap(buf, len, pos, state, true)) { + optional_idx start_pos; + idx_t end_pos; + while (pos < len && buf[pos] != '=') { + if (!ValueStateTransition(input_state, start_pos, end_pos)) { + return false; + } + } + if (pos == len) { return false; } - SkipWhitespace(buf, ++pos, len); - if (!FindKeyOrValueMap(buf, len, pos, state, false)) { + if (!start_pos.IsValid()) { + start_pos = 0; + end_pos = 0; + } else { + end_pos++; + } + if (!state.HandleKey(buf, start_pos.GetIndex(), end_pos)) { return false; } - SkipWhitespace(buf, ++pos, len); + start_pos = optional_idx(); + pos++; + SkipWhitespace(input_state); + while (pos < len && (buf[pos] != ',' && buf[pos] != '}')) { + if (!ValueStateTransition(input_state, start_pos, end_pos)) { + return false; + } + } + if (pos == len) { + return false; + } + if (!start_pos.IsValid()) { + //! Value is empty + state.HandleValue(buf, 0, 0); + } else { + state.HandleValue(buf, start_pos.GetIndex(), end_pos + 1); + } + if (buf[pos] == '}') { + break; + } + pos++; + SkipWhitespace(input_state); } - return true; + pos++; + SkipWhitespace(input_state); + return (pos == len); } bool VectorStringToMap::SplitStringMap(const string_t &input, string_t *child_key_data, string_t *child_val_data, @@ -277,42 +441,6 @@ idx_t VectorStringToMap::CountPartsMap(const string_t &input) { } // ------- STRUCT SPLIT ------- -static bool FindKeyStruct(const char *buf, idx_t len, idx_t &pos) { - while (pos < len) { - if (buf[pos] == ':') { - return true; - } - pos++; - } - return false; -} - -static bool FindValueStruct(const char *buf, idx_t len, idx_t &pos, Vector &varchar_child, idx_t &row_idx, - ValidityMask &child_mask) { - auto start_pos = pos; - idx_t lvl = 0; - while (pos < len) { - if (buf[pos] == '"' || buf[pos] == '\'') { - SkipToCloseQuotes(pos, buf, len); - } else if (buf[pos] == '{') { - SkipToClose(pos, buf, len, lvl, '}'); - } else if (buf[pos] == '[') { - SkipToClose(pos, buf, len, lvl, ']'); - } else if (buf[pos] == ',' || buf[pos] == '}') { - idx_t end_pos = StringTrim(buf, start_pos, pos); - if ((end_pos - start_pos) == 4 && IsNull(buf, start_pos, varchar_child, row_idx)) { - return true; - } - FlatVector::GetData(varchar_child)[row_idx] = - StringVector::AddString(varchar_child, buf + start_pos, end_pos - start_pos); - child_mask.SetValid(row_idx); // any child not set to valid will remain invalid - return true; - } - pos++; - } - return false; -} - bool VectorStringToStruct::SplitStruct(const string_t &input, vector> &varchar_vectors, idx_t &row_idx, string_map_t &child_names, vector> &child_masks) { @@ -321,39 +449,161 @@ bool VectorStringToStruct::SplitStruct(const string_t &input, vector= key_end) { - // empty key name unsupported + if (!start_pos.IsValid()) { + //! Key can not be empty return false; } - string_t found_key(buf + key_start, UnsafeNumericCast(key_end - key_start)); - - auto it = child_names.find(found_key); + idx_t key_start = start_pos.GetIndex(); + end_pos++; + if (IsNull(buf, key_start, end_pos)) { + //! Key can not be NULL + return false; + } + auto child_name = HandleString(temp_vec, buf, key_start, end_pos); + auto it = child_names.find(child_name); if (it == child_names.end()) { return false; // false key } child_idx = it->second; - SkipWhitespace(buf, ++pos, len); - if (!FindValueStruct(buf, len, pos, *varchar_vectors[child_idx], row_idx, child_masks[child_idx].get())) { + + start_pos = optional_idx(); + pos++; + SkipWhitespace(input_state); + while (pos < len && (buf[pos] != ',' && buf[pos] != '}')) { + if (!ValueStateTransition(input_state, start_pos, end_pos)) { + return false; + } + } + if (pos == len) { return false; } - SkipWhitespace(buf, ++pos, len); + auto &child_vec = *varchar_vectors[child_idx]; + auto string_data = FlatVector::GetData(child_vec); + auto &child_mask = child_masks[child_idx].get(); + + if (!start_pos.IsValid()) { + start_pos = 0; + end_pos = 0; + } else { + end_pos++; + } + auto value_start = start_pos.GetIndex(); + if (IsNull(buf, value_start, end_pos)) { + child_mask.SetInvalid(row_idx); + } else { + string_data[row_idx] = HandleString(child_vec, buf, value_start, end_pos); + child_mask.SetValid(row_idx); + } + + if (buf[pos] == '}') { + break; + } + pos++; + SkipWhitespace(input_state); + } + } else { + //! This is an unnamed struct in the form of `(value, value_2, ...)` + D_ASSERT(end_char == ')'); + idx_t child_idx = 0; + while (pos < len) { + if (child_idx == child_masks.size()) { + return false; + } + + optional_idx start_pos; + idx_t end_pos; + while (pos < len && (buf[pos] != ',' && buf[pos] != ')')) { + if (!ValueStateTransition(input_state, start_pos, end_pos)) { + return false; + } + } + if (pos == len) { + return false; + } + auto &child_vec = *varchar_vectors[child_idx]; + auto string_data = FlatVector::GetData(child_vec); + auto &child_mask = child_masks[child_idx].get(); + + if (!start_pos.IsValid()) { + start_pos = 0; + end_pos = 0; + } else { + end_pos++; + } + auto value_start = start_pos.GetIndex(); + if (IsNull(buf, value_start, end_pos)) { + child_mask.SetInvalid(row_idx); + } else { + string_data[row_idx] = HandleString(child_vec, buf, value_start, end_pos); + child_mask.SetValid(row_idx); + } + + if (buf[pos] == ')') { + break; + } + child_idx++; + pos++; + SkipWhitespace(input_state); } + (void)child_idx; } - SkipWhitespace(buf, pos, len); + pos++; + SkipWhitespace(input_state); return (pos == len); } diff --git a/src/function/pragma/pragma_functions.cpp b/src/function/pragma/pragma_functions.cpp index 635828066029..5612f519c1e0 100644 --- a/src/function/pragma/pragma_functions.cpp +++ b/src/function/pragma/pragma_functions.cpp @@ -94,6 +94,10 @@ static void PragmaForceCheckpoint(ClientContext &context, const FunctionParamete DBConfig::GetConfig(context).options.force_checkpoint = true; } +static void PragmaTruncateDuckDBLogs(ClientContext &context, const FunctionParameters ¶meters) { + context.db->GetLogManager().TruncateLogStorage(); +} + static void PragmaDisableForceParallelism(ClientContext &context, const FunctionParameters ¶meters) { ClientConfig::GetConfig(context).verify_parallelism = false; } @@ -149,6 +153,8 @@ void PragmaFunctions::RegisterFunction(BuiltinFunctions &set) { set.AddFunction(PragmaFunction::PragmaStatement("force_checkpoint", PragmaForceCheckpoint)); + set.AddFunction(PragmaFunction::PragmaStatement("truncate_duckdb_logs", PragmaTruncateDuckDBLogs)); + set.AddFunction(PragmaFunction::PragmaStatement("enable_progress_bar", PragmaEnableProgressBar)); set.AddFunction(PragmaFunction::PragmaStatement("disable_progress_bar", PragmaDisableProgressBar)); diff --git a/src/function/scalar/generic/getvariable.cpp b/src/function/scalar/generic/getvariable.cpp index 14d32954d1cf..0181c07523bc 100644 --- a/src/function/scalar/generic/getvariable.cpp +++ b/src/function/scalar/generic/getvariable.cpp @@ -24,12 +24,12 @@ struct GetVariableBindData : FunctionData { static unique_ptr GetVariableBind(ClientContext &context, ScalarFunction &function, vector> &arguments) { + if (arguments[0]->HasParameter() || arguments[0]->return_type.id() == LogicalTypeId::UNKNOWN) { + throw ParameterNotResolvedException(); + } if (!arguments[0]->IsFoldable()) { throw NotImplementedException("getvariable requires a constant input"); } - if (arguments[0]->HasParameter()) { - throw ParameterNotResolvedException(); - } Value value; auto variable_name = ExpressionExecutor::EvaluateScalar(context, *arguments[0]); if (!variable_name.IsNull()) { diff --git a/src/function/table/read_csv.cpp b/src/function/table/read_csv.cpp index e5bce2264db1..1bbe01ac8476 100644 --- a/src/function/table/read_csv.cpp +++ b/src/function/table/read_csv.cpp @@ -140,7 +140,7 @@ static unique_ptr ReadCSVBind(ClientContext &context, TableFunctio auto result = make_uniq(); auto &options = result->options; - const auto multi_file_reader = MultiFileReader::Create(input.table_function); + auto multi_file_reader = MultiFileReader::Create(input.table_function); const auto multi_file_list = multi_file_reader->CreateFileList(context, input.inputs[0]); if (multi_file_list->GetTotalFileCount() > 1) { options.multi_file_reader = true; @@ -415,6 +415,14 @@ void PushdownTypeToCSVScanner(ClientContext &context, optional_ptr } } +virtual_column_map_t ReadCSVGetVirtualColumns(ClientContext &context, optional_ptr bind_data) { + auto &csv_bind = bind_data->Cast(); + virtual_column_map_t result; + MultiFileReader::GetVirtualColumns(context, csv_bind.reader_bind, result); + result.insert(make_pair(COLUMN_IDENTIFIER_EMPTY, TableColumn("", LogicalType::BOOLEAN))); + return result; +} + TableFunction ReadCSVTableFunction::GetFunction() { TableFunction read_csv("read_csv", {LogicalType::VARCHAR}, ReadCSVFunction, ReadCSVBind, ReadCSVInitGlobal, ReadCSVInitLocal); @@ -426,6 +434,7 @@ TableFunction ReadCSVTableFunction::GetFunction() { read_csv.cardinality = CSVReaderCardinality; read_csv.projection_pushdown = true; read_csv.type_pushdown = PushdownTypeToCSVScanner; + read_csv.get_virtual_columns = ReadCSVGetVirtualColumns; ReadCSVAddNamedParameters(read_csv); return read_csv; } diff --git a/src/function/table/system/duckdb_extensions.cpp b/src/function/table/system/duckdb_extensions.cpp index 0edc2c2ff929..8adeb356afde 100644 --- a/src/function/table/system/duckdb_extensions.cpp +++ b/src/function/table/system/duckdb_extensions.cpp @@ -84,7 +84,7 @@ unique_ptr DuckDBExtensionsInit(ClientContext &context info.loaded = false; info.file_path = extension.statically_loaded ? "(BUILT-IN)" : string(); info.install_mode = - extension.statically_loaded ? ExtensionInstallMode::STATICALLY_LINKED : ExtensionInstallMode::UNKNOWN; + extension.statically_loaded ? ExtensionInstallMode::STATICALLY_LINKED : ExtensionInstallMode::NOT_INSTALLED; info.description = extension.description; for (idx_t k = 0; k < alias_count; k++) { auto alias = ExtensionHelper::GetExtensionAlias(k); @@ -206,7 +206,7 @@ void DuckDBExtensionsFunction(ClientContext &context, TableFunctionInput &data_p // extension version LogicalType::LIST(LogicalType::VARCHAR) output.SetValue(6, count, Value(entry.extension_version)); // installed_mode LogicalType::VARCHAR - output.SetValue(7, count, entry.installed ? Value(EnumUtil::ToString(entry.install_mode)) : Value()); + output.SetValue(7, count, EnumUtil::ToString(entry.install_mode)); // installed_source LogicalType::VARCHAR output.SetValue(8, count, Value(entry.installed_from)); diff --git a/src/function/table/table_scan.cpp b/src/function/table/table_scan.cpp index 0cf85d64a2b9..178a2a6a8bb0 100644 --- a/src/function/table/table_scan.cpp +++ b/src/function/table/table_scan.cpp @@ -703,6 +703,11 @@ static unique_ptr TableScanDeserialize(Deserializer &deserializer, return std::move(result); } +virtual_column_map_t TableScanGetVirtualColumns(ClientContext &context, optional_ptr bind_data_p) { + auto &bind_data = bind_data_p->Cast(); + return bind_data.table.GetVirtualColumns(); +} + TableFunction TableScanFunction::GetFunction() { TableFunction scan_function("seq_scan", {}, TableScanFunc); scan_function.init_local = TableScanInitLocal; @@ -720,8 +725,10 @@ TableFunction TableScanFunction::GetFunction() { scan_function.filter_pushdown = true; scan_function.filter_prune = true; scan_function.sampling_pushdown = true; + scan_function.late_materialization = true; scan_function.serialize = TableScanSerialize; scan_function.deserialize = TableScanDeserialize; + scan_function.get_virtual_columns = TableScanGetVirtualColumns; return scan_function; } diff --git a/src/function/table_function.cpp b/src/function/table_function.cpp index 6e9df81943ae..e2172cef2b79 100644 --- a/src/function/table_function.cpp +++ b/src/function/table_function.cpp @@ -22,8 +22,9 @@ TableFunction::TableFunction(string name, vector arguments, table_f in_out_function_final(nullptr), statistics(nullptr), dependency(nullptr), cardinality(nullptr), pushdown_complex_filter(nullptr), to_string(nullptr), table_scan_progress(nullptr), get_partition_data(nullptr), get_bind_info(nullptr), type_pushdown(nullptr), get_multi_file_reader(nullptr), supports_pushdown_type(nullptr), - get_partition_info(nullptr), get_partition_stats(nullptr), serialize(nullptr), deserialize(nullptr), - projection_pushdown(false), filter_pushdown(false), filter_prune(false), sampling_pushdown(false) { + get_partition_info(nullptr), get_partition_stats(nullptr), get_virtual_columns(nullptr), serialize(nullptr), + deserialize(nullptr), projection_pushdown(false), filter_pushdown(false), filter_prune(false), + sampling_pushdown(false), late_materialization(false) { } TableFunction::TableFunction(const vector &arguments, table_function_t function, @@ -36,9 +37,9 @@ TableFunction::TableFunction() init_local(nullptr), function(nullptr), in_out_function(nullptr), statistics(nullptr), dependency(nullptr), cardinality(nullptr), pushdown_complex_filter(nullptr), to_string(nullptr), table_scan_progress(nullptr), get_partition_data(nullptr), get_bind_info(nullptr), type_pushdown(nullptr), get_multi_file_reader(nullptr), - supports_pushdown_type(nullptr), get_partition_info(nullptr), get_partition_stats(nullptr), serialize(nullptr), - deserialize(nullptr), projection_pushdown(false), filter_pushdown(false), filter_prune(false), - sampling_pushdown(false) { + supports_pushdown_type(nullptr), get_partition_info(nullptr), get_partition_stats(nullptr), + get_virtual_columns(nullptr), serialize(nullptr), deserialize(nullptr), projection_pushdown(false), + filter_pushdown(false), filter_prune(false), sampling_pushdown(false), late_materialization(false) { } bool TableFunction::Equal(const TableFunction &rhs) const { diff --git a/src/function/window/window_boundaries_state.cpp b/src/function/window/window_boundaries_state.cpp index ce3ba3bbeb85..a4b034441d38 100644 --- a/src/function/window/window_boundaries_state.cpp +++ b/src/function/window/window_boundaries_state.cpp @@ -180,9 +180,9 @@ struct OperationCompare : public std::function { }; template -static idx_t FindTypedRangeBound(WindowCursor &over, const idx_t order_begin, const idx_t order_end, - const WindowBoundary range, WindowInputExpression &boundary, const idx_t chunk_idx, - const FrameBounds &prev) { +static idx_t FindTypedRangeBound(WindowCursor &range_lo, WindowCursor &range_hi, const idx_t order_begin, + const idx_t order_end, const WindowBoundary range, WindowInputExpression &boundary, + const idx_t chunk_idx, const FrameBounds &prev) { D_ASSERT(!boundary.CellIsNull(chunk_idx)); const auto val = boundary.GetCell(chunk_idx); @@ -191,14 +191,14 @@ static idx_t FindTypedRangeBound(WindowCursor &over, const idx_t order_begin, co // Check that the value we are searching for is in range. if (range == WindowBoundary::EXPR_PRECEDING_RANGE) { // Preceding but value past the current value - const auto cur_val = over.GetCell(0, order_end - 1); + const auto cur_val = range_hi.GetCell(0, order_end - 1); if (comp(cur_val, val)) { throw OutOfRangeException("Invalid RANGE PRECEDING value"); } } else { // Following but value before the current value D_ASSERT(range == WindowBoundary::EXPR_FOLLOWING_RANGE); - const auto cur_val = over.GetCell(0, order_begin); + const auto cur_val = range_lo.GetCell(0, order_begin); if (comp(val, cur_val)) { throw OutOfRangeException("Invalid RANGE FOLLOWING value"); } @@ -206,20 +206,28 @@ static idx_t FindTypedRangeBound(WindowCursor &over, const idx_t order_begin, co // Try to reuse the previous bounds to restrict the search. // This is only valid if the previous bounds were non-empty // Only inject the comparisons if the previous bounds are a strict subset. - WindowColumnIterator begin(over, order_begin); - WindowColumnIterator end(over, order_end); + WindowColumnIterator begin(range_lo, order_begin); + WindowColumnIterator end(range_hi, order_end); if (prev.start < prev.end) { if (order_begin < prev.start && prev.start < order_end) { - const auto first = over.GetCell(0, prev.start); - if (!comp(val, first)) { - // prev.first <= val, so we can start further forward + const auto first = range_lo.GetCell(0, prev.start); + if (FROM && !comp(val, first)) { + // If prev.start == val and we are looking for a lower bound, then we are done + if (!comp(first, val)) { + return prev.start; + } + // prev.start <= val, so we can start further forward begin += UnsafeNumericCast(prev.start - order_begin); } } if (order_begin < prev.end && prev.end < order_end) { - const auto second = over.GetCell(0, prev.end - 1); + const auto second = range_hi.GetCell(0, prev.end - 1); if (!comp(second, val)) { - // val <= prev.second, so we can end further back + // If val == prev.end and we are looking for an upper bound, then we are done + if (!FROM && !comp(val, second)) { + return prev.end; + } + // val <= prev.end, so we can end further back // (prev.second is the largest peer) end -= UnsafeNumericCast(order_end - prev.end - 1); } @@ -234,52 +242,65 @@ static idx_t FindTypedRangeBound(WindowCursor &over, const idx_t order_begin, co } template -static idx_t FindRangeBound(WindowCursor &over, const idx_t order_begin, const idx_t order_end, - const WindowBoundary range, WindowInputExpression &boundary, const idx_t chunk_idx, - const FrameBounds &prev) { +static idx_t FindRangeBound(WindowCursor &range_lo, WindowCursor &range_hi, const idx_t order_begin, + const idx_t order_end, const WindowBoundary range, WindowInputExpression &boundary, + const idx_t chunk_idx, const FrameBounds &prev) { switch (boundary.InternalType()) { case PhysicalType::INT8: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::INT16: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::INT32: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::INT64: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::UINT8: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::UINT16: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::UINT32: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::UINT64: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::INT128: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::UINT128: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, - prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::FLOAT: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::DOUBLE: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); case PhysicalType::INTERVAL: - return FindTypedRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, - prev); + return FindTypedRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, + chunk_idx, prev); default: throw InternalException("Unsupported column type for RANGE"); } } template -static idx_t FindOrderedRangeBound(WindowCursor &over, const OrderType range_sense, const idx_t order_begin, - const idx_t order_end, const WindowBoundary range, WindowInputExpression &boundary, - const idx_t chunk_idx, const FrameBounds &prev) { +static idx_t FindOrderedRangeBound(WindowCursor &range_lo, WindowCursor &range_hi, const OrderType range_sense, + const idx_t order_begin, const idx_t order_end, const WindowBoundary range, + WindowInputExpression &boundary, const idx_t chunk_idx, const FrameBounds &prev) { switch (range_sense) { case OrderType::ASCENDING: - return FindRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, chunk_idx, + prev); case OrderType::DESCENDING: - return FindRangeBound(over, order_begin, order_end, range, boundary, chunk_idx, prev); + return FindRangeBound(range_lo, range_hi, order_begin, order_end, range, boundary, chunk_idx, + prev); default: throw InternalException("Unsupported ORDER BY sense for RANGE"); } @@ -718,6 +739,13 @@ void WindowBoundariesState::FrameBegin(DataChunk &bounds, idx_t row_idx, const i prev.start = valid_begin_data[0]; prev.end = valid_end_data[0]; + if (has_preceding_range || has_following_range) { + if (range_lo.get() != range.get()) { + range_lo = range.get(); + range_hi = range_lo->Copy(); + } + } + switch (start_boundary) { case WindowBoundary::UNBOUNDED_PRECEDING: bounds.data[FRAME_BEGIN].Reference(bounds.data[PARTITION_BEGIN]); @@ -766,7 +794,12 @@ void WindowBoundariesState::FrameBegin(DataChunk &bounds, idx_t row_idx, const i } else { const auto valid_start = valid_begin_data[chunk_idx]; prev.end = valid_end_data[chunk_idx]; - window_start = FindOrderedRangeBound(*range, range_sense, valid_start, row_idx + 1, + const auto cur_partition = partition_begin_data[chunk_idx]; + if (cur_partition != prev_partition) { + prev.start = valid_start; + prev_partition = cur_partition; + } + window_start = FindOrderedRangeBound(*range_lo, *range_hi, range_sense, valid_start, row_idx + 1, start_boundary, boundary_begin, chunk_idx, prev); prev.start = window_start; } @@ -785,8 +818,8 @@ void WindowBoundariesState::FrameBegin(DataChunk &bounds, idx_t row_idx, const i prev.start = valid_begin_data[chunk_idx]; prev_partition = cur_partition; } - window_start = FindOrderedRangeBound(*range, range_sense, row_idx, valid_end, start_boundary, - boundary_begin, chunk_idx, prev); + window_start = FindOrderedRangeBound(*range_lo, *range_hi, range_sense, row_idx, valid_end, + start_boundary, boundary_begin, chunk_idx, prev); prev.start = window_start; } frame_begin_data[chunk_idx] = window_start; @@ -862,6 +895,13 @@ void WindowBoundariesState::FrameEnd(DataChunk &bounds, idx_t row_idx, const idx prev.start = valid_begin_data[0]; prev.end = valid_end_data[0]; + if (has_preceding_range || has_following_range) { + if (range_lo.get() != range.get()) { + range_lo = range.get(); + range_hi = range_lo->Copy(); + } + } + switch (end_boundary) { case WindowBoundary::CURRENT_ROW_ROWS: for (idx_t chunk_idx = 0; chunk_idx < count; ++chunk_idx, ++row_idx) { @@ -911,8 +951,13 @@ void WindowBoundariesState::FrameEnd(DataChunk &bounds, idx_t row_idx, const idx } else { const auto valid_start = valid_begin_data[chunk_idx]; prev.start = valid_start; - window_end = FindOrderedRangeBound(*range, range_sense, valid_start, row_idx + 1, end_boundary, - boundary_end, chunk_idx, prev); + const auto cur_partition = partition_begin_data[chunk_idx]; + if (cur_partition != prev_partition) { + prev.end = valid_end; + prev_partition = cur_partition; + } + window_end = FindOrderedRangeBound(*range_lo, *range_hi, range_sense, valid_start, row_idx + 1, + end_boundary, boundary_end, chunk_idx, prev); prev.end = window_end; } frame_end_data[chunk_idx] = window_end; @@ -930,8 +975,8 @@ void WindowBoundariesState::FrameEnd(DataChunk &bounds, idx_t row_idx, const idx prev.end = valid_end; prev_partition = cur_partition; } - window_end = FindOrderedRangeBound(*range, range_sense, row_idx, valid_end, end_boundary, - boundary_end, chunk_idx, prev); + window_end = FindOrderedRangeBound(*range_lo, *range_hi, range_sense, row_idx, valid_end, + end_boundary, boundary_end, chunk_idx, prev); prev.end = window_end; } frame_end_data[chunk_idx] = window_end; diff --git a/src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp b/src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp index 398e49974b88..0adb7857b24d 100644 --- a/src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp +++ b/src/include/duckdb/catalog/catalog_entry/table_catalog_entry.hpp @@ -18,6 +18,7 @@ #include "duckdb/common/case_insensitive_map.hpp" #include "duckdb/catalog/catalog_entry/table_column_type.hpp" #include "duckdb/catalog/catalog_entry/column_dependency_manager.hpp" +#include "duckdb/common/table_column.hpp" namespace duckdb { @@ -117,10 +118,8 @@ class TableCatalogEntry : public StandardEntry { //! Returns true, if the table has a primary key, else false. bool HasPrimaryKey() const; - //! Returns the rowid type of this table - virtual LogicalType GetRowIdType() const { - return LogicalType::ROW_TYPE; - } + //! Returns the virtual columns for this table + virtual virtual_column_map_t GetVirtualColumns() const; protected: //! A list of columns that are part of this table diff --git a/src/include/duckdb/common/column_index.hpp b/src/include/duckdb/common/column_index.hpp index 32d2e1828462..a563005f43f3 100644 --- a/src/include/duckdb/common/column_index.hpp +++ b/src/include/duckdb/common/column_index.hpp @@ -61,6 +61,9 @@ struct ColumnIndex { bool IsRowIdColumn() const { return index == DConstants::INVALID_INDEX; } + bool IsVirtualColumn() const { + return index >= VIRTUAL_COLUMN_START; + } void Serialize(Serializer &serializer) const; static ColumnIndex Deserialize(Deserializer &deserializer); diff --git a/src/include/duckdb/common/constants.hpp b/src/include/duckdb/common/constants.hpp index d4a0d7cda1c3..e9f816a17619 100644 --- a/src/include/duckdb/common/constants.hpp +++ b/src/include/duckdb/common/constants.hpp @@ -40,7 +40,11 @@ DUCKDB_API bool IsInvalidCatalog(const string &str); //! Special value used to signify the ROW ID of a table DUCKDB_API extern const column_t COLUMN_IDENTIFIER_ROW_ID; +//! Special value used to signify an empty column (used for e.g. COUNT(*)) +DUCKDB_API extern const column_t COLUMN_IDENTIFIER_EMPTY; +DUCKDB_API extern const column_t VIRTUAL_COLUMN_START; DUCKDB_API bool IsRowIdColumnId(column_t column_id); +DUCKDB_API bool IsVirtualColumn(column_t column_id); //! The maximum row identifier used in tables extern const row_t MAX_ROW_ID; diff --git a/src/include/duckdb/common/enums/metric_type.hpp b/src/include/duckdb/common/enums/metric_type.hpp index 14389bf4a5f9..825e5c5369e8 100644 --- a/src/include/duckdb/common/enums/metric_type.hpp +++ b/src/include/duckdb/common/enums/metric_type.hpp @@ -69,6 +69,7 @@ enum class MetricsType : uint8_t { OPTIMIZER_MATERIALIZED_CTE, OPTIMIZER_SUM_REWRITER, OPTIMIZER_LATE_MATERIALIZATION, + OPTIMIZER_REMOVE_USELESS_PROJECTIONS, }; struct MetricsTypeHashFunction { diff --git a/src/include/duckdb/common/enums/optimizer_type.hpp b/src/include/duckdb/common/enums/optimizer_type.hpp index adabacec225d..e9209d56ba1b 100644 --- a/src/include/duckdb/common/enums/optimizer_type.hpp +++ b/src/include/duckdb/common/enums/optimizer_type.hpp @@ -41,7 +41,8 @@ enum class OptimizerType : uint32_t { EXTENSION, MATERIALIZED_CTE, SUM_REWRITER, - LATE_MATERIALIZATION + LATE_MATERIALIZATION, + REMOVE_USELESS_PROJECTIONS }; string OptimizerTypeToString(OptimizerType type); diff --git a/src/include/duckdb/common/multi_file_reader.hpp b/src/include/duckdb/common/multi_file_reader.hpp index 942f72c1e277..0d5d36484e24 100644 --- a/src/include/duckdb/common/multi_file_reader.hpp +++ b/src/include/duckdb/common/multi_file_reader.hpp @@ -200,6 +200,10 @@ struct MultiFileReaderData { //! The MultiFileReader class provides a set of helper methods to handle scanning from multiple files struct MultiFileReader { +public: + static constexpr column_t COLUMN_IDENTIFIER_FILENAME = UINT64_C(9223372036854775808); + +public: virtual ~MultiFileReader(); //! Create a MultiFileReader for a specific TableFunction, using its function name for errors @@ -289,6 +293,9 @@ struct MultiFileReader { const OperatorPartitionInfo &partition_info, OperatorPartitionData &partition_data); + DUCKDB_API static void GetVirtualColumns(ClientContext &context, MultiFileReaderBindData &bind_data, + virtual_column_map_t &result); + template MultiFileReaderBindData BindUnionReader(ClientContext &context, vector &return_types, vector &names, MultiFileList &files, RESULT_CLASS &result, diff --git a/src/include/duckdb/common/primitive_dictionary.hpp b/src/include/duckdb/common/primitive_dictionary.hpp new file mode 100644 index 000000000000..8d75389a1a7b --- /dev/null +++ b/src/include/duckdb/common/primitive_dictionary.hpp @@ -0,0 +1,186 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/common/primitive_dictionary.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/common/types/string_type.hpp" +#include "duckdb/common/allocator.hpp" +#include "duckdb/common/serializer/memory_stream.hpp" + +namespace duckdb { + +struct PrimitiveCastOperator { + template + static TGT Operation(SRC input) { + return TGT(input); + } +}; + +template +class PrimitiveDictionary { +private: + static constexpr idx_t LOAD_FACTOR = 2; + + static constexpr uint32_t INVALID_INDEX = static_cast(-1); + struct primitive_dictionary_entry_t { + SRC value; + uint32_t index; + bool IsEmpty() const { + return index == INVALID_INDEX; + } + }; + +public: + static constexpr uint32_t MAXIMUM_POSSIBLE_SIZE = INVALID_INDEX - 1; + + //! PrimitiveDictionary is a fixed-size linear probing hash table for primitive types + //! It is used to dictionary-encode data in, e.g., Parquet files + PrimitiveDictionary(Allocator &allocator, idx_t maximum_size_p, idx_t target_capacity_p) + : maximum_size(maximum_size_p), size(0), capacity(NextPowerOfTwo(maximum_size * LOAD_FACTOR)), + capacity_mask(capacity - 1), target_capacity(target_capacity_p), + allocated_dictionary(allocator.Allocate(capacity * sizeof(primitive_dictionary_entry_t))), + allocated_target( + allocator.Allocate(std::is_same::value ? target_capacity : capacity * sizeof(TGT))), + target_stream(allocated_target.get(), allocated_target.GetSize()), + dictionary(reinterpret_cast(allocated_dictionary.get())), full(false) { + // Initialize empty + for (idx_t i = 0; i < capacity; i++) { + dictionary[i].index = INVALID_INDEX; + } + } + +public: + //! Insert value into dictionary (if not full) + void Insert(SRC value) { + if (full) { + return; + } + auto &entry = Lookup(value); + if (entry.IsEmpty()) { + if (size + 1 > maximum_size || !AddToTarget(value)) { + full = true; + return; + } + entry.value = value; + entry.index = size++; + } + } + + //! Get dictionary index of an already inserted value + uint32_t GetIndex(const SRC &value) const { + const auto &entry = Lookup(value); + D_ASSERT(!entry.IsEmpty()); + return entry.index; + } + + //! Iterates over inserted values + template ::value, int>::type = 0> + void IterateValues(const std::function &fun) const { + const auto target_values = reinterpret_cast(allocated_target.get()); + for (idx_t i = 0; i < capacity; i++) { + auto &entry = dictionary[i]; + if (entry.IsEmpty()) { + continue; + } + fun(entry.value, target_values[entry.index]); + } + } + + //! Specialized template to iterate over string_t values + template ::value, int>::type = 0> + void IterateValues(const std::function &fun) const { + for (idx_t i = 0; i < capacity; i++) { + auto &entry = dictionary[i]; + if (entry.IsEmpty()) { + continue; + } + fun(entry.value, entry.value); + } + } + + //! Get the number of unique values in the dictionary + idx_t GetSize() const { + return size; + } + + //! If any of the inserts caused the dictionary to be full, this returns true + bool IsFull() const { + return full; + } + + //! Get the target written values as a memory stream (zero-copy) + unique_ptr GetTargetMemoryStream() const { + auto result = make_uniq(target_stream.GetData(), target_stream.GetCapacity()); + result->SetPosition(target_stream.GetPosition()); + return result; + } + +private: + //! Look up a value in the dictionary using linear probing + primitive_dictionary_entry_t &Lookup(const SRC &value) const { + auto offset = Hash(value) & capacity_mask; + while (!dictionary[offset].IsEmpty() && dictionary[offset].value != value) { + ++offset &= capacity_mask; + } + return dictionary[offset]; + } + + //! Write a value to the target data (if source is not string) + template ::value, int>::type = 0> + bool AddToTarget(const SRC &src_value) { + const auto tgt_value = OP::template Operation(src_value); + if (target_stream.GetPosition() + OP::template WriteSize(tgt_value) > target_stream.GetCapacity()) { + return false; // Out of capacity + } + OP::template WriteToStream(tgt_value, target_stream); + return true; + } + + //! Write a value to the target data (if source is string) + template ::value, int>::type = 0> + bool AddToTarget(SRC &src_value) { + // If source is string, target must also be string + if (target_stream.GetPosition() + OP::template WriteSize(src_value) > target_stream.GetCapacity()) { + return false; // Out of capacity + } + + const auto ptr = target_stream.GetData() + target_stream.GetPosition() + sizeof(uint32_t); + OP::template WriteToStream(src_value, target_stream); + + if (!src_value.IsInlined()) { + src_value.SetPointer(char_ptr_cast(ptr)); + } + + return true; + } + +private: + //! Maximum size and current size + const idx_t maximum_size; + idx_t size; + + //! Dictionary capacity (power of two) and corresponding mask + const idx_t capacity; + const idx_t capacity_mask; + + //! Capacity of target encoded data + const idx_t target_capacity; + + //! Allocated regions for dictionary/target + AllocatedData allocated_dictionary; + AllocatedData allocated_target; + MemoryStream target_stream; + + //! Pointers to allocated regions for convenience + primitive_dictionary_entry_t *const dictionary; + + //! More values inserted than possible + bool full; +}; + +} // namespace duckdb diff --git a/src/include/duckdb/common/serializer/memory_stream.hpp b/src/include/duckdb/common/serializer/memory_stream.hpp index a735ad1aa90e..f5cd1c153049 100644 --- a/src/include/duckdb/common/serializer/memory_stream.hpp +++ b/src/include/duckdb/common/serializer/memory_stream.hpp @@ -25,20 +25,20 @@ class MemoryStream : public WriteStream, public ReadStream { public: static constexpr idx_t DEFAULT_INITIAL_CAPACITY = 512; - // Create a new owning MemoryStream with an internal backing buffer with the specified capacity. The stream will - // own the backing buffer, resize it when needed and free its memory when the stream is destroyed + //! Create a new owning MemoryStream with an internal backing buffer with the specified capacity. The stream will + //! own the backing buffer, resize it when needed and free its memory when the stream is destroyed explicit MemoryStream(Allocator &allocator, idx_t capacity = DEFAULT_INITIAL_CAPACITY); - // Create a new owning MemoryStream with an internal backing buffer with the specified capacity. The stream will - // own the backing buffer, resize it when needed and free its memory when the stream is destroyed + //! Create a new owning MemoryStream with an internal backing buffer with the specified capacity. The stream will + //! own the backing buffer, resize it when needed and free its memory when the stream is destroyed explicit MemoryStream(idx_t capacity = DEFAULT_INITIAL_CAPACITY); - // Create a new non-owning MemoryStream over the specified external buffer and capacity. The stream will not take - // ownership of the backing buffer, will not attempt to resize it and will not free the memory when the stream - // is destroyed + //! Create a new non-owning MemoryStream over the specified external buffer and capacity. The stream will not take + //! ownership of the backing buffer, will not attempt to resize it and will not free the memory when the stream + //! is destroyed explicit MemoryStream(data_ptr_t buffer, idx_t capacity); - // Cant copy! + //! Cant copy! MemoryStream(const MemoryStream &) = delete; MemoryStream &operator=(const MemoryStream &) = delete; @@ -47,30 +47,33 @@ class MemoryStream : public WriteStream, public ReadStream { ~MemoryStream() override; - // Write data to the stream. - // Throws if the write would exceed the capacity of the stream and the backing buffer is not owned by the stream + //! Write data to the stream. + //! Throws if the write would exceed the capacity of the stream and the backing buffer is not owned by the stream void WriteData(const_data_ptr_t buffer, idx_t write_size) override; - // Read data from the stream. - // Throws if the read would exceed the capacity of the stream + //! Read data from the stream. + //! Throws if the read would exceed the capacity of the stream void ReadData(data_ptr_t buffer, idx_t read_size) override; - // Rewind the stream to the start, keeping the capacity and the backing buffer intact + //! Rewind the stream to the start, keeping the capacity and the backing buffer intact void Rewind(); - // Release ownership of the backing buffer and turn a owning stream into a non-owning one. - // The stream will no longer be responsible for freeing the data. - // The stream will also no longer attempt to automatically resize the buffer when the capacity is reached. + //! Release ownership of the backing buffer and turn a owning stream into a non-owning one. + //! The stream will no longer be responsible for freeing the data. + //! The stream will also no longer attempt to automatically resize the buffer when the capacity is reached. void Release(); - // Get a pointer to the underlying backing buffer + //! Get a pointer to the underlying backing buffer data_ptr_t GetData() const; - // Get the current position in the stream + //! Get the current position in the stream idx_t GetPosition() const; - // Get the capacity of the stream + //! Get the capacity of the stream idx_t GetCapacity() const; + + //! Set the position in the stream + void SetPosition(idx_t position); }; } // namespace duckdb diff --git a/src/include/duckdb/common/table_column.hpp b/src/include/duckdb/common/table_column.hpp new file mode 100644 index 000000000000..6cc4b8fe7e60 --- /dev/null +++ b/src/include/duckdb/common/table_column.hpp @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/common/table_column.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/common/types.hpp" +#include "duckdb/common/unordered_map.hpp" + +namespace duckdb { + +struct TableColumn { + TableColumn(string name_p, LogicalType type_p) : name(std::move(name_p)), type(std::move(type_p)) { + } + + string name; + LogicalType type; +}; + +using virtual_column_map_t = unordered_map; + +} // namespace duckdb diff --git a/src/include/duckdb/common/types/datetime.hpp b/src/include/duckdb/common/types/datetime.hpp index ccbb95524244..d52edd57379c 100644 --- a/src/include/duckdb/common/types/datetime.hpp +++ b/src/include/duckdb/common/types/datetime.hpp @@ -1,3 +1,11 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/common/types/datetime.hpp +// +// +//===----------------------------------------------------------------------===// + #pragma once #include "duckdb/common/common.hpp" diff --git a/src/include/duckdb/common/types/hash.hpp b/src/include/duckdb/common/types/hash.hpp index b213f249e402..128ee634dcd7 100644 --- a/src/include/duckdb/common/types/hash.hpp +++ b/src/include/duckdb/common/types/hash.hpp @@ -10,6 +10,7 @@ #include "duckdb/common/common.hpp" #include "duckdb/common/types.hpp" +#include "duckdb/common/types/datetime.hpp" namespace duckdb { @@ -63,6 +64,8 @@ template <> DUCKDB_API hash_t Hash(string_t val); template <> DUCKDB_API hash_t Hash(interval_t val); +template <> +DUCKDB_API hash_t Hash(dtime_tz_t val); DUCKDB_API hash_t Hash(const char *val, size_t size); DUCKDB_API hash_t Hash(uint8_t *val, size_t size); diff --git a/src/include/duckdb/common/types/validity_mask.hpp b/src/include/duckdb/common/types/validity_mask.hpp index 05583cddd82f..89c39750d326 100644 --- a/src/include/duckdb/common/types/validity_mask.hpp +++ b/src/include/duckdb/common/types/validity_mask.hpp @@ -188,7 +188,7 @@ struct TemplatedValidityMask { D_ASSERT(validity_mask); idx_t entry_idx, idx_in_entry; GetEntryIndex(row_idx, entry_idx, idx_in_entry); - auto entry = GetValidityEntry(entry_idx); + auto entry = GetValidityEntryUnsafe(entry_idx); return RowIsValid(entry, idx_in_entry); } diff --git a/src/include/duckdb/execution/index/art/iterator.hpp b/src/include/duckdb/execution/index/art/iterator.hpp index 58a0f106d54d..977cc7791081 100644 --- a/src/include/duckdb/execution/index/art/iterator.hpp +++ b/src/include/duckdb/execution/index/art/iterator.hpp @@ -90,6 +90,8 @@ class Iterator { GateStatus status; //! Depth in a nested leaf. uint8_t nested_depth = 0; + //! True, if we entered a nested leaf to retrieve the next node. + bool entered_nested_leaf = false; private: //! Goes to the next leaf in the ART and sets it as last_leaf, diff --git a/src/include/duckdb/execution/operator/helper/physical_streaming_sample.hpp b/src/include/duckdb/execution/operator/helper/physical_streaming_sample.hpp index dafaf849f556..6f75b2cf1964 100644 --- a/src/include/duckdb/execution/operator/helper/physical_streaming_sample.hpp +++ b/src/include/duckdb/execution/operator/helper/physical_streaming_sample.hpp @@ -19,12 +19,10 @@ class PhysicalStreamingSample : public PhysicalOperator { static constexpr const PhysicalOperatorType TYPE = PhysicalOperatorType::STREAMING_SAMPLE; public: - PhysicalStreamingSample(vector types, SampleMethod method, double percentage, int64_t seed, - idx_t estimated_cardinality); + PhysicalStreamingSample(vector types, unique_ptr options, idx_t estimated_cardinality); - SampleMethod method; + unique_ptr sample_options; double percentage; - int64_t seed; public: // Operator interface @@ -32,9 +30,7 @@ class PhysicalStreamingSample : public PhysicalOperator { OperatorResultType Execute(ExecutionContext &context, DataChunk &input, DataChunk &chunk, GlobalOperatorState &gstate, OperatorState &state) const override; - bool ParallelOperator() const override { - return true; - } + bool ParallelOperator() const override; InsertionOrderPreservingMap ParamsToString() const override; diff --git a/src/include/duckdb/execution/operator/persistent/physical_export.hpp b/src/include/duckdb/execution/operator/persistent/physical_export.hpp index fd7f4981fc6f..214487dac0f8 100644 --- a/src/include/duckdb/execution/operator/persistent/physical_export.hpp +++ b/src/include/duckdb/execution/operator/persistent/physical_export.hpp @@ -14,6 +14,7 @@ #include "duckdb/function/copy_function.hpp" #include "duckdb/parser/parsed_data/copy_info.hpp" #include "duckdb/parser/parsed_data/exported_table_data.hpp" +#include "duckdb/catalog/catalog_entry_map.hpp" namespace duckdb { diff --git a/src/include/duckdb/execution/operator/persistent/physical_insert.hpp b/src/include/duckdb/execution/operator/persistent/physical_insert.hpp index ccb113c4f939..ffa4f6b224e3 100644 --- a/src/include/duckdb/execution/operator/persistent/physical_insert.hpp +++ b/src/include/duckdb/execution/operator/persistent/physical_insert.hpp @@ -16,6 +16,7 @@ #include "duckdb/storage/table/append_state.hpp" #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" #include "duckdb/storage/table/delete_state.hpp" +#include "duckdb/storage/optimistic_data_writer.hpp" namespace duckdb { @@ -53,8 +54,9 @@ class InsertLocalState : public LocalSinkState { DataChunk update_chunk; ExpressionExecutor default_executor; TableAppendState local_append_state; - unique_ptr local_collection; - optional_ptr writer; + //! An index to the optimistic row group collection vector of the local table storage for this transaction. + PhysicalIndex collection_index; + unique_ptr optimistic_writer; // Rows that have been updated by a DO UPDATE conflict unordered_set updated_rows; idx_t update_count = 0; diff --git a/src/include/duckdb/execution/operator/scan/physical_table_scan.hpp b/src/include/duckdb/execution/operator/scan/physical_table_scan.hpp index 45ac1e34c5b7..ca7cd4a0db34 100644 --- a/src/include/duckdb/execution/operator/scan/physical_table_scan.hpp +++ b/src/include/duckdb/execution/operator/scan/physical_table_scan.hpp @@ -27,7 +27,7 @@ class PhysicalTableScan : public PhysicalOperator { PhysicalTableScan(vector types, TableFunction function, unique_ptr bind_data, vector returned_types, vector column_ids, vector projection_ids, vector names, unique_ptr table_filters, idx_t estimated_cardinality, - ExtraOperatorInfo extra_info, vector parameters); + ExtraOperatorInfo extra_info, vector parameters, virtual_column_map_t virtual_columns); //! The table function TableFunction function; @@ -50,6 +50,8 @@ class PhysicalTableScan : public PhysicalOperator { vector parameters; //! Contains a reference to dynamically generated table filters (through e.g. a join up in the tree) shared_ptr dynamic_filters; + //! Virtual columns + virtual_column_map_t virtual_columns; public: string GetName() const override; diff --git a/src/include/duckdb/function/aggregate/minmax_n_helpers.hpp b/src/include/duckdb/function/aggregate/minmax_n_helpers.hpp index 9c59d11cbd1a..a26772819c43 100644 --- a/src/include/duckdb/function/aggregate/minmax_n_helpers.hpp +++ b/src/include/duckdb/function/aggregate/minmax_n_helpers.hpp @@ -102,20 +102,23 @@ class UnaryAggregateHeap { public: UnaryAggregateHeap() = default; - explicit UnaryAggregateHeap(idx_t capacity_p) : capacity(capacity_p) { - heap.reserve(capacity); + UnaryAggregateHeap(ArenaAllocator &allocator, idx_t capacity_p) { + Initialize(allocator, capacity_p); } - void Initialize(const idx_t capacity_p) { + void Initialize(ArenaAllocator &allocator, const idx_t capacity_p) { capacity = capacity_p; - heap.reserve(capacity); + auto ptr = allocator.AllocateAligned(capacity * sizeof(HeapEntry)); + memset(ptr, 0, capacity * sizeof(HeapEntry)); + heap = reinterpret_cast *>(ptr); + size = 0; } bool IsEmpty() const { - return heap.empty(); + return size == 0; } idx_t Size() const { - return heap.size(); + return size; } idx_t Capacity() const { return capacity; @@ -125,29 +128,28 @@ class UnaryAggregateHeap { D_ASSERT(capacity != 0); // must be initialized // If the heap is not full, insert the value into a new slot - if (heap.size() < capacity) { - heap.emplace_back(); - heap.back().Assign(allocator, value); - std::push_heap(heap.begin(), heap.end(), Compare); + if (size < capacity) { + heap[size++].Assign(allocator, value); + std::push_heap(heap, heap + size, Compare); } // If the heap is full, check if the value is greater than the smallest value in the heap // If it is, assign the new value to the slot and re-heapify - else if (T_COMPARATOR::Operation(value, heap.front().value)) { - std::pop_heap(heap.begin(), heap.end(), Compare); - heap.back().Assign(allocator, value); - std::push_heap(heap.begin(), heap.end(), Compare); + else if (T_COMPARATOR::Operation(value, heap[0].value)) { + std::pop_heap(heap, heap + size, Compare); + heap[size - 1].Assign(allocator, value); + std::push_heap(heap, heap + size, Compare); } - D_ASSERT(std::is_heap(heap.begin(), heap.end(), Compare)); + D_ASSERT(std::is_heap(heap, heap + size, Compare)); } void Insert(ArenaAllocator &allocator, const UnaryAggregateHeap &other) { - for (auto &slot : other.heap) { - Insert(allocator, slot.value); + for (idx_t slot = 0; slot < other.Size(); slot++) { + Insert(allocator, other.heap[slot].value); } } - vector> &SortAndGetHeap() { - std::sort_heap(heap.begin(), heap.end(), Compare); + HeapEntry *SortAndGetHeap() { + std::sort_heap(heap, heap + size, Compare); return heap; } @@ -160,8 +162,9 @@ class UnaryAggregateHeap { return T_COMPARATOR::Operation(left.value, right.value); } - vector> heap; idx_t capacity; + HeapEntry *heap; + idx_t size; }; template @@ -171,20 +174,23 @@ class BinaryAggregateHeap { public: BinaryAggregateHeap() = default; - explicit BinaryAggregateHeap(idx_t capacity_p) : capacity(capacity_p) { - heap.reserve(capacity); + BinaryAggregateHeap(ArenaAllocator &allocator, idx_t capacity_p) { + Initialize(allocator, capacity_p); } - void Initialize(const idx_t capacity_p) { + void Initialize(ArenaAllocator &allocator, const idx_t capacity_p) { capacity = capacity_p; - heap.reserve(capacity); + auto ptr = allocator.AllocateAligned(capacity * sizeof(STORAGE_TYPE)); + memset(ptr, 0, capacity * sizeof(STORAGE_TYPE)); + heap = reinterpret_cast(ptr); + size = 0; } bool IsEmpty() const { - return heap.empty(); + return size == 0; } idx_t Size() const { - return heap.size(); + return size; } idx_t Capacity() const { return capacity; @@ -194,31 +200,31 @@ class BinaryAggregateHeap { D_ASSERT(capacity != 0); // must be initialized // If the heap is not full, insert the value into a new slot - if (heap.size() < capacity) { - heap.emplace_back(); - heap.back().first.Assign(allocator, key); - heap.back().second.Assign(allocator, value); - std::push_heap(heap.begin(), heap.end(), Compare); + if (size < capacity) { + heap[size].first.Assign(allocator, key); + heap[size].second.Assign(allocator, value); + size++; + std::push_heap(heap, heap + size, Compare); } // If the heap is full, check if the value is greater than the smallest value in the heap // If it is, assign the new value to the slot and re-heapify - else if (K_COMPARATOR::Operation(key, heap.front().first.value)) { - std::pop_heap(heap.begin(), heap.end(), Compare); - heap.back().first.Assign(allocator, key); - heap.back().second.Assign(allocator, value); - std::push_heap(heap.begin(), heap.end(), Compare); + else if (K_COMPARATOR::Operation(key, heap[0].first.value)) { + std::pop_heap(heap, heap + size, Compare); + heap[size - 1].first.Assign(allocator, key); + heap[size - 1].second.Assign(allocator, value); + std::push_heap(heap, heap + size, Compare); } - D_ASSERT(std::is_heap(heap.begin(), heap.end(), Compare)); + D_ASSERT(std::is_heap(heap, heap + size, Compare)); } void Insert(ArenaAllocator &allocator, const BinaryAggregateHeap &other) { - for (auto &slot : other.heap) { - Insert(allocator, slot.first.value, slot.second.value); + for (idx_t slot = 0; slot < other.Size(); slot++) { + Insert(allocator, other.heap[slot].first.value, other.heap[slot].second.value); } } - vector &SortAndGetHeap() { - std::sort_heap(heap.begin(), heap.end(), Compare); + STORAGE_TYPE *SortAndGetHeap() { + std::sort_heap(heap, heap + size, Compare); return heap; } @@ -231,8 +237,9 @@ class BinaryAggregateHeap { return K_COMPARATOR::Operation(left.first.value, right.first.value); } - vector heap; idx_t capacity; + STORAGE_TYPE *heap; + idx_t size; }; //------------------------------------------------------------------------------ @@ -326,7 +333,7 @@ struct MinMaxNOperation { } if (!target.is_initialized) { - target.Initialize(source.heap.Capacity()); + target.Initialize(aggr_input.allocator, source.heap.Capacity()); } else if (source.heap.Capacity() != target.heap.Capacity()) { throw InvalidInputException("Mismatched n values in min/max/arg_min/arg_max"); } @@ -377,10 +384,10 @@ struct MinMaxNOperation { list_entry.length = state.heap.Size(); // Turn the heap into a sorted list, invalidating the heap property - auto &heap = state.heap.SortAndGetHeap(); + auto heap = state.heap.SortAndGetHeap(); - for (const auto &slot : heap) { - STATE::VAL_TYPE::Assign(child_data, current_offset++, state.heap.GetValue(slot)); + for (idx_t slot = 0; slot < state.heap.Size(); slot++) { + STATE::VAL_TYPE::Assign(child_data, current_offset++, state.heap.GetValue(heap[slot])); } } diff --git a/src/include/duckdb/function/table_function.hpp b/src/include/duckdb/function/table_function.hpp index 6293100ef1d1..e432e0f9568c 100644 --- a/src/include/duckdb/function/table_function.hpp +++ b/src/include/duckdb/function/table_function.hpp @@ -12,11 +12,12 @@ #include "duckdb/common/optional_ptr.hpp" #include "duckdb/execution/execution_context.hpp" #include "duckdb/function/function.hpp" -#include "duckdb/planner/bind_context.hpp" #include "duckdb/planner/logical_operator.hpp" #include "duckdb/storage/statistics/node_statistics.hpp" #include "duckdb/common/column_index.hpp" +#include "duckdb/common/table_column.hpp" #include "duckdb/function/partition_stats.hpp" +#include "duckdb/common/exception/binder_exception.hpp" #include @@ -27,7 +28,9 @@ class LogicalDependencyList; class LogicalGet; class TableFunction; class TableFilterSet; +class TableFunctionRef; class TableCatalogEntry; +class SampleOptions; struct MultiFileReader; struct OperatorPartitionData; struct OperatorPartitionInfo; @@ -292,6 +295,9 @@ typedef TablePartitionInfo (*table_function_get_partition_info_t)(ClientContext typedef vector (*table_function_get_partition_stats_t)(ClientContext &context, GetPartitionStatsInput &input); +typedef virtual_column_map_t (*table_function_get_virtual_columns_t)(ClientContext &context, + optional_ptr bind_data); + //! When to call init_global to initialize the table function enum class TableFunctionInitialization { INITIALIZE_ON_EXECUTE, INITIALIZE_ON_SCHEDULE }; @@ -360,6 +366,8 @@ class TableFunction : public SimpleNamedParameterFunction { // NOLINT: work-arou table_function_get_partition_info_t get_partition_info; //! (Optional) get a list of all the partition stats of the table table_function_get_partition_stats_t get_partition_stats; + //! (Optional) returns a list of virtual columns emitted by the table function + table_function_get_virtual_columns_t get_virtual_columns; table_function_serialize_t serialize; table_function_deserialize_t deserialize; @@ -377,6 +385,8 @@ class TableFunction : public SimpleNamedParameterFunction { // NOLINT: work-arou //! Whether or not the table function supports sampling pushdown. If not supported a sample will be taken after the //! table function. bool sampling_pushdown; + //! Whether or not the table function supports late materialization + bool late_materialization; //! Additional function info, passed to the bind shared_ptr function_info; diff --git a/src/include/duckdb/function/window/window_boundaries_state.hpp b/src/include/duckdb/function/window/window_boundaries_state.hpp index 2748bc7a0600..11c724d9b638 100644 --- a/src/include/duckdb/function/window/window_boundaries_state.hpp +++ b/src/include/duckdb/function/window/window_boundaries_state.hpp @@ -148,6 +148,10 @@ struct WindowBoundariesState { idx_t valid_end = 0; FrameBounds prev; + + // Extra range cursor + optional_ptr range_lo; + unique_ptr range_hi; }; } // namespace duckdb diff --git a/src/include/duckdb/logging/log_manager.hpp b/src/include/duckdb/logging/log_manager.hpp index 90c6384c3274..6f414d9efa7e 100644 --- a/src/include/duckdb/logging/log_manager.hpp +++ b/src/include/duckdb/logging/log_manager.hpp @@ -54,6 +54,8 @@ class LogManager : public enable_shared_from_this { DUCKDB_API void SetDisabledLogTypes(unordered_set &disabled_log_types); DUCKDB_API void SetLogStorage(DatabaseInstance &db, const string &storage_name); + DUCKDB_API void TruncateLogStorage(); + DUCKDB_API LogConfig GetConfig(); protected: diff --git a/src/include/duckdb/logging/log_storage.hpp b/src/include/duckdb/logging/log_storage.hpp index f99175b590ea..d30d370a7028 100644 --- a/src/include/duckdb/logging/log_storage.hpp +++ b/src/include/duckdb/logging/log_storage.hpp @@ -61,6 +61,8 @@ class LogStorage { DUCKDB_API virtual unique_ptr CreateScanContextsState() const; DUCKDB_API virtual bool ScanContexts(LogStorageScanState &state, DataChunk &result) const; DUCKDB_API virtual void InitializeScanContexts(LogStorageScanState &state) const; + + DUCKDB_API virtual void Truncate(); }; class StdOutLogStorage : public LogStorage { @@ -73,6 +75,8 @@ class StdOutLogStorage : public LogStorage { const RegisteredLoggingContext &context) override; void WriteLogEntries(DataChunk &chunk, const RegisteredLoggingContext &context) override; void Flush() override; + + void Truncate() override; }; class InMemoryLogStorageScanState : public LogStorageScanState { @@ -94,6 +98,8 @@ class InMemoryLogStorage : public LogStorage { void WriteLogEntries(DataChunk &chunk, const RegisteredLoggingContext &context) override; void Flush() override; + void Truncate() override; + //! LogStorage API: READING bool CanScan() override; @@ -106,6 +112,7 @@ class InMemoryLogStorage : public LogStorage { protected: void WriteLoggingContext(const RegisteredLoggingContext &context); + void ResetBuffers(); protected: mutable mutex lock; diff --git a/src/include/duckdb/main/client_config.hpp b/src/include/duckdb/main/client_config.hpp index 9cedb4074a49..4398a788585c 100644 --- a/src/include/duckdb/main/client_config.hpp +++ b/src/include/duckdb/main/client_config.hpp @@ -101,6 +101,8 @@ struct ClientConfig { idx_t nested_loop_join_threshold = 5; //! The number of rows we need on either table to choose a merge join over an IE join idx_t merge_join_threshold = 1000; + //! The maximum number of rows to use the nested loop join implementation + idx_t asof_loop_join_threshold = 64; //! The maximum amount of memory to keep buffered in a streaming query result. Default: 1mb. idx_t streaming_buffer_size = 1000000; diff --git a/src/include/duckdb/main/pending_query_result.hpp b/src/include/duckdb/main/pending_query_result.hpp index 72fe9405fef8..cf0268712cba 100644 --- a/src/include/duckdb/main/pending_query_result.hpp +++ b/src/include/duckdb/main/pending_query_result.hpp @@ -29,6 +29,8 @@ class PendingQueryResult : public BaseQueryResult { DUCKDB_API explicit PendingQueryResult(ErrorData error_message); DUCKDB_API ~PendingQueryResult() override; DUCKDB_API bool AllowStreamResult() const; + PendingQueryResult(const PendingQueryResult &) = delete; + PendingQueryResult &operator=(const PendingQueryResult &) = delete; public: //! Executes a single task within the query, returning whether or not the query is ready. diff --git a/src/include/duckdb/main/settings.hpp b/src/include/duckdb/main/settings.hpp index b9c979dcaa13..02ee9b2ee507 100644 --- a/src/include/duckdb/main/settings.hpp +++ b/src/include/duckdb/main/settings.hpp @@ -218,6 +218,17 @@ struct ArrowOutputListViewSetting { static Value GetSetting(const ClientContext &context); }; +struct AsofLoopJoinThresholdSetting { + using RETURN_TYPE = idx_t; + static constexpr const char *Name = "asof_loop_join_threshold"; + static constexpr const char *Description = + "The maximum number of rows we need on the left side of an ASOF join to use a nested loop join"; + static constexpr const char *InputType = "UBIGINT"; + static void SetLocal(ClientContext &context, const Value ¶meter); + static void ResetLocal(ClientContext &context); + static Value GetSetting(const ClientContext &context); +}; + struct AutoinstallExtensionRepositorySetting { using RETURN_TYPE = string; static constexpr const char *Name = "autoinstall_extension_repository"; diff --git a/src/include/duckdb/optimizer/late_materialization.hpp b/src/include/duckdb/optimizer/late_materialization.hpp index 76f4f05e86cc..16350601a807 100644 --- a/src/include/duckdb/optimizer/late_materialization.hpp +++ b/src/include/duckdb/optimizer/late_materialization.hpp @@ -40,6 +40,8 @@ class LateMaterialization : public BaseColumnPruner { Optimizer &optimizer; //! The max row count for which we will consider late materialization idx_t max_row_count; + //! The type of the row id column + LogicalType row_id_type; }; } // namespace duckdb diff --git a/src/include/duckdb/optimizer/remove_useless_projections.hpp b/src/include/duckdb/optimizer/remove_useless_projections.hpp new file mode 100644 index 000000000000..595d090630fb --- /dev/null +++ b/src/include/duckdb/optimizer/remove_useless_projections.hpp @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/optimizer/remove_useless_projections.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/optimizer/column_binding_replacer.hpp" + +namespace duckdb { + +//! The RemoveUselessProjections Optimizer traverses the logical operator tree and removes all projections that just +class RemoveUselessProjections : LogicalOperatorVisitor { +public: + RemoveUselessProjections() : first_projection(true) { + } + unique_ptr RemoveProjections(unique_ptr plan); + unique_ptr RemoveProjectionsChildren(unique_ptr plan); + void ReplaceBindings(LogicalOperator &plan); + +private: + bool first_projection; + ColumnBindingReplacer replacer; +}; + +} // namespace duckdb diff --git a/src/include/duckdb/planner/bind_context.hpp b/src/include/duckdb/planner/bind_context.hpp index 8234805e7ba9..ff070ceb49d6 100644 --- a/src/include/duckdb/planner/bind_context.hpp +++ b/src/include/duckdb/planner/bind_context.hpp @@ -100,7 +100,7 @@ class BindContext { //! Adds a call to a table function with the given alias to the BindContext. void AddTableFunction(idx_t index, const string &alias, const vector &names, const vector &types, vector &bound_column_ids, - optional_ptr entry); + optional_ptr entry, virtual_column_map_t virtual_columns); //! Adds a table view with a given alias to the BindContext. void AddView(idx_t index, const string &alias, SubqueryRef &ref, BoundQueryNode &subquery, ViewCatalogEntry &view); //! Adds a subquery with a given alias to the BindContext. diff --git a/src/include/duckdb/planner/operator/logical_get.hpp b/src/include/duckdb/planner/operator/logical_get.hpp index 81b93accb48b..395224bc89dd 100644 --- a/src/include/duckdb/planner/operator/logical_get.hpp +++ b/src/include/duckdb/planner/operator/logical_get.hpp @@ -24,7 +24,7 @@ class LogicalGet : public LogicalOperator { public: LogicalGet(idx_t table_index, TableFunction function, unique_ptr bind_data, vector returned_types, vector returned_names, - LogicalType rowid_type = LogicalType(LogicalType::ROW_TYPE)); + virtual_column_map_t virtual_columns = virtual_column_map_t()); //! The table index in the current bind context idx_t table_index; @@ -36,6 +36,8 @@ class LogicalGet : public LogicalOperator { vector returned_types; //! The names of ALL columns that can be returned by the table function vector names; + //! A mapping of column index -> type/name for all virtual columns + virtual_column_map_t virtual_columns; //! Columns that are used outside the scan vector projection_ids; //! Filters pushed down for table scan @@ -61,6 +63,12 @@ class LogicalGet : public LogicalOperator { InsertionOrderPreservingMap ParamsToString() const override; //! Returns the underlying table that is being scanned, or nullptr if there is none optional_ptr GetTable() const; + //! Returns any column to query - preferably the cheapest column + //! This is used when we are running e.g. a COUNT(*) and don't care about the contents of any columns in the table + column_t GetAnyColumn() const; + + const LogicalType &GetColumnType(const ColumnIndex &column_index) const; + const string &GetColumnName(const ColumnIndex &column_index) const; public: void SetColumnIds(vector &&column_ids); @@ -80,10 +88,6 @@ class LogicalGet : public LogicalOperator { void Serialize(Serializer &serializer) const override; static unique_ptr Deserialize(Deserializer &deserializer); - const LogicalType &GetRowIdType() const { - return rowid_type; - } - protected: void ResolveTypes() override; @@ -93,8 +97,5 @@ class LogicalGet : public LogicalOperator { private: //! Bound column IDs vector column_ids; - - //! The type of the rowid column - LogicalType rowid_type = LogicalType(LogicalType::ROW_TYPE); }; } // namespace duckdb diff --git a/src/include/duckdb/planner/table_binding.hpp b/src/include/duckdb/planner/table_binding.hpp index 50631f57ac45..9aedc7e70058 100644 --- a/src/include/duckdb/planner/table_binding.hpp +++ b/src/include/duckdb/planner/table_binding.hpp @@ -16,6 +16,7 @@ #include "duckdb/catalog/catalog_entry/table_column_type.hpp" #include "duckdb/planner/binding_alias.hpp" #include "duckdb/common/column_index.hpp" +#include "duckdb/common/table_column.hpp" namespace duckdb { class BindContext; @@ -33,8 +34,7 @@ enum class BindingType { BASE, TABLE, DUMMY, CATALOG_ENTRY }; //! A Binding represents a binding to a table, table-producing function or subquery with a specified table index. struct Binding { - Binding(BindingType binding_type, BindingAlias alias, vector types, vector names, idx_t index, - LogicalType rowid_type = LogicalType(LogicalType::ROW_TYPE)); + Binding(BindingType binding_type, BindingAlias alias, vector types, vector names, idx_t index); virtual ~Binding() = default; //! The type of Binding @@ -50,8 +50,6 @@ struct Binding { //! Name -> index for the names case_insensitive_map_t name_map; - LogicalType rowid_type; - public: bool TryGetBindingIndex(const string &column_name, column_t &column_index); column_t GetBindingIndex(const string &column_name); @@ -104,12 +102,14 @@ struct TableBinding : public Binding { public: TableBinding(const string &alias, vector types, vector names, vector &bound_column_ids, optional_ptr entry, idx_t index, - bool add_row_id = false); + virtual_column_map_t virtual_columns); //! A reference to the set of bound column ids vector &bound_column_ids; //! The underlying catalog entry (if any) optional_ptr entry; + //! Virtual columns + virtual_column_map_t virtual_columns; public: unique_ptr ExpandGeneratedColumn(const string &column_name); diff --git a/src/include/duckdb/storage/data_table.hpp b/src/include/duckdb/storage/data_table.hpp index 6a0b97727384..5d9f6c057aed 100644 --- a/src/include/duckdb/storage/data_table.hpp +++ b/src/include/duckdb/storage/data_table.hpp @@ -29,7 +29,6 @@ class ColumnDataCollection; class ColumnDefinition; class DataTable; class DuckTransaction; -class OptimisticDataWriter; class RowGroup; class StorageManager; class TableCatalogEntry; @@ -115,9 +114,15 @@ class DataTable { optional_ptr> column_ids); //! Merge a row group collection into the transaction-local storage void LocalMerge(ClientContext &context, RowGroupCollection &collection); - //! Creates an optimistic writer for this table - used for optimistically writing parallel appends - OptimisticDataWriter &CreateOptimisticWriter(ClientContext &context); - void FinalizeOptimisticWriter(ClientContext &context, OptimisticDataWriter &writer); + //! Create an optimistic row group collection for this table. Used for optimistically writing parallel appends. + //! Returns the index into the optimistic_collections vector for newly created collection. + PhysicalIndex CreateOptimisticCollection(ClientContext &context, unique_ptr collection); + //! Returns the optimistic row group collection corresponding to the index. + RowGroupCollection &GetOptimisticCollection(ClientContext &context, const PhysicalIndex collection_index); + //! Resets the optimistic row group collection corresponding to the index. + void ResetOptimisticCollection(ClientContext &context, const PhysicalIndex collection_index); + //! Returns the optimistic writer of the corresponding local table. + OptimisticDataWriter &GetOptimisticWriter(ClientContext &context); unique_ptr InitializeDelete(TableCatalogEntry &table, ClientContext &context, const vector> &bound_constraints); @@ -201,7 +206,7 @@ class DataTable { //! Checkpoint the table to the specified table data writer void Checkpoint(TableDataWriter &writer, Serializer &serializer); void CommitDropTable(); - void CommitDropColumn(idx_t index); + void CommitDropColumn(const idx_t column_index); idx_t ColumnCount() const; idx_t GetTotalRows() const; diff --git a/src/include/duckdb/storage/optimistic_data_writer.hpp b/src/include/duckdb/storage/optimistic_data_writer.hpp index 802d51bad707..cdf96a038264 100644 --- a/src/include/duckdb/storage/optimistic_data_writer.hpp +++ b/src/include/duckdb/storage/optimistic_data_writer.hpp @@ -39,7 +39,7 @@ class OptimisticDataWriter { private: //! The table DataTable &table; - //! The partial block manager (if we created one yet) + //! The partial block manager, if any was created. unique_ptr partial_manager; }; diff --git a/src/include/duckdb/storage/partial_block_manager.hpp b/src/include/duckdb/storage/partial_block_manager.hpp index b46ea65ad031..c59869976a91 100644 --- a/src/include/duckdb/storage/partial_block_manager.hpp +++ b/src/include/duckdb/storage/partial_block_manager.hpp @@ -129,9 +129,6 @@ class PartialBlockManager { //! Returns a reference to the underlying block manager. BlockManager &GetBlockManager() const; - //! Registers a block as "written" by this partial block manager - void AddWrittenBlock(block_id_t block); - protected: BlockManager &block_manager; PartialBlockType partial_block_type; @@ -140,8 +137,6 @@ class PartialBlockManager { //! This is a multimap because there might be outstanding partial blocks with //! the same amount of left-over space multimap> partially_filled_blocks; - //! The set of written blocks - unordered_set written_blocks; //! The maximum size (in bytes) at which a partial block will be considered a partial block uint32_t max_partial_block_size; diff --git a/src/include/duckdb/storage/table/row_group.hpp b/src/include/duckdb/storage/table/row_group.hpp index 16a535a4e4f9..8ceea68a3615 100644 --- a/src/include/duckdb/storage/table/row_group.hpp +++ b/src/include/duckdb/storage/table/row_group.hpp @@ -102,7 +102,7 @@ class RowGroup : public SegmentBase { unique_ptr RemoveColumn(RowGroupCollection &collection, idx_t removed_column); void CommitDrop(); - void CommitDropColumn(idx_t index); + void CommitDropColumn(const idx_t column_index); void InitializeEmpty(const vector &types); diff --git a/src/include/duckdb/storage/table/row_group_collection.hpp b/src/include/duckdb/storage/table/row_group_collection.hpp index 19aa6452038c..9940d80f45c0 100644 --- a/src/include/duckdb/storage/table/row_group_collection.hpp +++ b/src/include/duckdb/storage/table/row_group_collection.hpp @@ -108,7 +108,7 @@ class RowGroupCollection { bool schedule_vacuum); unique_ptr GetCheckpointTask(CollectionCheckpointState &checkpoint_state, idx_t segment_idx); - void CommitDropColumn(idx_t index); + void CommitDropColumn(const idx_t column_index); void CommitDropTable(); vector GetPartitionStats() const; diff --git a/src/include/duckdb/transaction/local_storage.hpp b/src/include/duckdb/transaction/local_storage.hpp index 4213fa0fadce..4119e968dc71 100644 --- a/src/include/duckdb/transaction/local_storage.hpp +++ b/src/include/duckdb/transaction/local_storage.hpp @@ -29,11 +29,12 @@ class LocalTableStorage : public enable_shared_from_this { public: // Create a new LocalTableStorage explicit LocalTableStorage(ClientContext &context, DataTable &table); - // Create a LocalTableStorage from an ALTER TYPE - LocalTableStorage(ClientContext &context, DataTable &table, LocalTableStorage &parent, idx_t changed_idx, - const LogicalType &target_type, const vector &bound_columns, Expression &cast_expr); - // Create a LocalTableStorage from a DROP COLUMN - LocalTableStorage(DataTable &table, LocalTableStorage &parent, idx_t drop_idx); + //! Create a LocalTableStorage from an ALTER TYPE. + LocalTableStorage(ClientContext &context, DataTable &new_data_table, LocalTableStorage &parent, + const idx_t alter_column_index, const LogicalType &target_type, + const vector &bound_columns, Expression &cast_expr); + //! Create a LocalTableStorage from a DROP COLUMN. + LocalTableStorage(DataTable &new_data_table, LocalTableStorage &parent, const idx_t drop_column_index); // Create a LocalTableStorage from an ADD COLUMN LocalTableStorage(ClientContext &context, DataTable &table, LocalTableStorage &parent, ColumnDefinition &new_column, ExpressionExecutor &default_executor); @@ -42,7 +43,7 @@ class LocalTableStorage : public enable_shared_from_this { reference table_ref; Allocator &allocator; - //! The main chunk collection holding the data + //! The main row group collection. shared_ptr row_groups; //! The set of unique append indexes. TableIndexList append_indexes; @@ -52,10 +53,12 @@ class LocalTableStorage : public enable_shared_from_this { IndexAppendMode index_append_mode = IndexAppendMode::DEFAULT; //! The number of deleted rows idx_t deleted_rows; - //! The main optimistic data writer + + //! The optimistic row group collections associated with this table. + vector> optimistic_collections; + //! The main optimistic data writer associated with this table. OptimisticDataWriter optimistic_writer; - //! The set of all optimistic data writers associated with this table - vector> optimistic_writers; + //! Whether or not storage was merged bool merged_storage = false; //! Whether or not the storage was dropped @@ -74,9 +77,18 @@ class LocalTableStorage : public enable_shared_from_this { const vector &table_types, row_t &start_row); void AppendToDeleteIndexes(Vector &row_ids, DataChunk &delete_chunk); - //! Creates an optimistic writer for this table - OptimisticDataWriter &CreateOptimisticWriter(); - void FinalizeOptimisticWriter(OptimisticDataWriter &writer); + //! Create an optimistic row group collection for this table. + //! Returns the index into the optimistic_collections vector for newly created collection. + PhysicalIndex CreateOptimisticCollection(unique_ptr collection); + //! Returns the optimistic row group collection corresponding to the index. + RowGroupCollection &GetOptimisticCollection(const PhysicalIndex collection_index); + //! Resets the optimistic row group collection corresponding to the index. + void ResetOptimisticCollection(const PhysicalIndex collection_index); + //! Returns the optimistic writer. + OptimisticDataWriter &GetOptimisticWriter(); + +private: + mutex collections_lock; }; class LocalTableManager { @@ -130,9 +142,15 @@ class LocalStorage { static void FinalizeAppend(LocalAppendState &state); //! Merge a row group collection into the transaction-local storage void LocalMerge(DataTable &table, RowGroupCollection &collection); - //! Create an optimistic writer for the specified table - OptimisticDataWriter &CreateOptimisticWriter(DataTable &table); - void FinalizeOptimisticWriter(DataTable &table, OptimisticDataWriter &writer); + //! Create an optimistic row group collection for this table. + //! Returns the index into the optimistic_collections vector for newly created collection. + PhysicalIndex CreateOptimisticCollection(DataTable &table, unique_ptr collection); + //! Returns the optimistic row group collection corresponding to the index. + RowGroupCollection &GetOptimisticCollection(DataTable &table, const PhysicalIndex collection_index); + //! Resets the optimistic row group collection corresponding to the index. + void ResetOptimisticCollection(DataTable &table, const PhysicalIndex collection_index); + //! Returns the optimistic writer. + OptimisticDataWriter &GetOptimisticWriter(DataTable &table); //! Delete a set of rows from the local storage idx_t Delete(DataTable &table, Vector &row_ids, idx_t count); @@ -155,7 +173,7 @@ class LocalStorage { void AddColumn(DataTable &old_dt, DataTable &new_dt, ColumnDefinition &new_column, ExpressionExecutor &default_executor); - void DropColumn(DataTable &old_dt, DataTable &new_dt, idx_t removed_column); + void DropColumn(DataTable &old_dt, DataTable &new_dt, const idx_t drop_column_index); void ChangeType(DataTable &old_dt, DataTable &new_dt, idx_t changed_idx, const LogicalType &target_type, const vector &bound_columns, Expression &cast_expr); @@ -172,6 +190,7 @@ class LocalStorage { DuckTransaction &transaction; LocalTableManager table_manager; +private: void Flush(DataTable &table, LocalTableStorage &storage, optional_ptr commit_state); }; diff --git a/src/logging/log_manager.cpp b/src/logging/log_manager.cpp index c937b3fda006..f493e2ee57b2 100644 --- a/src/logging/log_manager.cpp +++ b/src/logging/log_manager.cpp @@ -149,6 +149,11 @@ void LogManager::SetLogStorage(DatabaseInstance &db, const string &storage_name) config.storage = storage_name_to_lower; } +void LogManager::TruncateLogStorage() { + unique_lock lck(lock); + log_storage->Truncate(); +} + LogConfig LogManager::GetConfig() { unique_lock lck(lock); return config; diff --git a/src/logging/log_storage.cpp b/src/logging/log_storage.cpp index 909bddf75830..8afb3f84e174 100644 --- a/src/logging/log_storage.cpp +++ b/src/logging/log_storage.cpp @@ -25,6 +25,9 @@ bool LogStorage::ScanContexts(LogStorageScanState &state, DataChunk &result) con void LogStorage::InitializeScanContexts(LogStorageScanState &state) const { throw NotImplementedException("Not implemented for this LogStorage: InitializeScanContexts"); } +void LogStorage::Truncate() { + throw NotImplementedException("Not implemented for this LogStorage: TruncateLogStorage"); +} StdOutLogStorage::StdOutLogStorage() { } @@ -46,6 +49,10 @@ void StdOutLogStorage::WriteLogEntries(DataChunk &chunk, const RegisteredLogging throw NotImplementedException("StdOutLogStorage::WriteLogEntries"); } +void StdOutLogStorage::Truncate() { + // NOP +} + void StdOutLogStorage::Flush() { // NOP } @@ -82,6 +89,16 @@ InMemoryLogStorage::InMemoryLogStorage(DatabaseInstance &db_p) log_contexts = make_uniq(db_p.GetBufferManager(), log_context_schema); } +void InMemoryLogStorage::ResetBuffers() { + entry_buffer->Reset(); + log_context_buffer->Reset(); + + log_entries->Reset(); + log_contexts->Reset(); + + registered_contexts.clear(); +} + InMemoryLogStorage::~InMemoryLogStorage() { } @@ -122,6 +139,11 @@ void InMemoryLogStorage::Flush() { FlushInternal(); } +void InMemoryLogStorage::Truncate() { + unique_lock lck(lock); + ResetBuffers(); +} + void InMemoryLogStorage::FlushInternal() { if (entry_buffer->size() > 0) { log_entries->Append(*entry_buffer); diff --git a/src/main/config.cpp b/src/main/config.cpp index d48181cd57a6..78ec76a6f0e3 100644 --- a/src/main/config.cpp +++ b/src/main/config.cpp @@ -72,6 +72,7 @@ static const ConfigurationOption internal_options[] = { DUCKDB_GLOBAL(ArrowLargeBufferSizeSetting), DUCKDB_GLOBAL(ArrowLosslessConversionSetting), DUCKDB_GLOBAL(ArrowOutputListViewSetting), + DUCKDB_LOCAL(AsofLoopJoinThresholdSetting), DUCKDB_GLOBAL(AutoinstallExtensionRepositorySetting), DUCKDB_GLOBAL(AutoinstallKnownExtensionsSetting), DUCKDB_GLOBAL(AutoloadKnownExtensionsSetting), diff --git a/src/main/extension/extension_load.cpp b/src/main/extension/extension_load.cpp index 4be4588371cf..5499665f1a27 100644 --- a/src/main/extension/extension_load.cpp +++ b/src/main/extension/extension_load.cpp @@ -71,15 +71,11 @@ struct ExtensionAccess { static void SetError(duckdb_extension_info info, const char *error) { auto &load_state = DuckDBExtensionLoadState::Get(info); - if (error) { - load_state.has_error = true; - load_state.error_data = ErrorData(error); - } else { - load_state.has_error = true; - load_state.error_data = ErrorData( - ExceptionType::UNKNOWN_TYPE, - "Extension has indicated an error occured during initialization, but did not set an error message."); - } + load_state.has_error = true; + load_state.error_data = + error ? ErrorData(error) + : ErrorData(ExceptionType::UNKNOWN_TYPE, "Extension has indicated an error occured during " + "initialization, but did not set an error message."); } //! Called by the extension get a pointer to the database that is loading it @@ -92,9 +88,11 @@ struct ExtensionAccess { load_state.database_data->database = make_shared_ptr(load_state.db); return reinterpret_cast(load_state.database_data.get()); } catch (std::exception &ex) { + load_state.has_error = true; load_state.error_data = ErrorData(ex); return nullptr; } catch (...) { + load_state.has_error = true; load_state.error_data = ErrorData(ExceptionType::UNKNOWN_TYPE, "Unknown error in GetDatabase when trying to load extension!"); return nullptr; diff --git a/src/main/settings/autogenerated_settings.cpp b/src/main/settings/autogenerated_settings.cpp index d007da71fc52..c7c71fd0c177 100644 --- a/src/main/settings/autogenerated_settings.cpp +++ b/src/main/settings/autogenerated_settings.cpp @@ -177,6 +177,23 @@ Value ArrowOutputListViewSetting::GetSetting(const ClientContext &context) { return Value::BOOLEAN(config.options.arrow_use_list_view); } +//===----------------------------------------------------------------------===// +// Asof Loop Join Threshold +//===----------------------------------------------------------------------===// +void AsofLoopJoinThresholdSetting::SetLocal(ClientContext &context, const Value &input) { + auto &config = ClientConfig::GetConfig(context); + config.asof_loop_join_threshold = input.GetValue(); +} + +void AsofLoopJoinThresholdSetting::ResetLocal(ClientContext &context) { + ClientConfig::GetConfig(context).asof_loop_join_threshold = ClientConfig().asof_loop_join_threshold; +} + +Value AsofLoopJoinThresholdSetting::GetSetting(const ClientContext &context) { + auto &config = ClientConfig::GetConfig(context); + return Value::UBIGINT(config.asof_loop_join_threshold); +} + //===----------------------------------------------------------------------===// // Autoinstall Extension Repository //===----------------------------------------------------------------------===// diff --git a/src/optimizer/CMakeLists.txt b/src/optimizer/CMakeLists.txt index a7b881b09925..bed29e1866d8 100644 --- a/src/optimizer/CMakeLists.txt +++ b/src/optimizer/CMakeLists.txt @@ -30,6 +30,7 @@ add_library_unity( regex_range_filter.cpp remove_duplicate_groups.cpp remove_unused_columns.cpp + remove_useless_projections.cpp statistics_propagator.cpp limit_pushdown.cpp topn_optimizer.cpp diff --git a/src/optimizer/join_order/relation_manager.cpp b/src/optimizer/join_order/relation_manager.cpp index d4f7032d676d..ccd68101e5e1 100644 --- a/src/optimizer/join_order/relation_manager.cpp +++ b/src/optimizer/join_order/relation_manager.cpp @@ -65,19 +65,14 @@ void RelationManager::AddRelation(LogicalOperator &op, optional_ptr LateMaterialization::ConstructLHS(LogicalGet &get) { // we need to construct a new scan of the same table auto table_index = optimizer.binder.GenerateTableIndex(); auto new_get = make_uniq(table_index, get.function, get.bind_data->Copy(), get.returned_types, - get.names, get.GetRowIdType()); + get.names, get.virtual_columns); new_get->GetMutableColumnIds() = get.GetColumnIds(); new_get->projection_ids = get.projection_ids; return new_get; @@ -73,8 +73,7 @@ ColumnBinding LateMaterialization::ConstructRHS(unique_ptr &op) case LogicalOperatorType::LOGICAL_PROJECTION: { auto &proj = op.Cast(); // push a projection of the row-id column - proj.expressions.push_back( - make_uniq("rowid", get.GetRowIdType(), row_id_binding)); + proj.expressions.push_back(make_uniq("rowid", row_id_type, row_id_binding)); // modify the row-id-binding to push to the new projection row_id_binding = ColumnBinding(proj.table_index, proj.expressions.size() - 1); column_count = proj.expressions.size(); @@ -153,9 +152,8 @@ unique_ptr LateMaterialization::GetExpression(LogicalOperator &op, i case LogicalOperatorType::LOGICAL_GET: { auto &get = op.Cast(); auto &column_id = get.GetColumnIds()[column_index]; - auto is_row_id = column_id.IsRowIdColumn(); - auto column_name = is_row_id ? "rowid" : get.names[column_id.GetPrimaryIndex()]; - auto &column_type = is_row_id ? get.GetRowIdType() : get.returned_types[column_id.GetPrimaryIndex()]; + auto column_name = get.GetColumnName(column_id); + auto &column_type = get.GetColumnType(column_id); auto expr = make_uniq(column_name, column_type, ColumnBinding(get.table_index, column_index)); return std::move(expr); @@ -235,16 +233,20 @@ bool LateMaterialization::TryLateMaterialization(unique_ptr &op } } auto &get = child.get().Cast(); - auto table = get.GetTable(); - if (!table || !table->IsDuckTable()) { - // we can only do the late-materialization optimization for DuckDB tables currently - return false; - } if (column_references.size() >= get.GetColumnIds().size()) { // we do not benefit from late materialization // we need all of the columns to compute the root node anyway (Top-N/Limit/etc) return false; } + if (!get.function.late_materialization) { + // this function does not support late materialization + return false; + } + auto entry = get.virtual_columns.find(COLUMN_IDENTIFIER_ROW_ID); + if (entry == get.virtual_columns.end()) { + throw InternalException("Table function supports late materialization but does not expose a rowid column"); + } + row_id_type = entry->second.type; // we benefit from late materialization // we need to transform this plan into a semi-join with the row-id // we need to ensure the operator returns exactly the same column bindings as before @@ -258,8 +260,6 @@ bool LateMaterialization::TryLateMaterialization(unique_ptr &op auto lhs_row_idx = GetOrInsertRowId(lhs_get); ColumnBinding lhs_binding(lhs_index, lhs_row_idx); - auto &row_id_type = get.GetRowIdType(); - // after constructing the LHS but before constructing the RHS we construct the final projections/orders // - we do this before constructing the RHS because that alter the original plan vector> final_proj_list; diff --git a/src/optimizer/optimizer.cpp b/src/optimizer/optimizer.cpp index dc1ddfa59224..36b6162c7584 100644 --- a/src/optimizer/optimizer.cpp +++ b/src/optimizer/optimizer.cpp @@ -12,6 +12,7 @@ #include "duckdb/optimizer/cte_filter_pusher.hpp" #include "duckdb/optimizer/deliminator.hpp" #include "duckdb/optimizer/empty_result_pullup.hpp" +#include "duckdb/optimizer/remove_useless_projections.hpp" #include "duckdb/optimizer/expression_heuristics.hpp" #include "duckdb/optimizer/filter_pullup.hpp" #include "duckdb/optimizer/filter_pushdown.hpp" @@ -118,7 +119,7 @@ void Optimizer::RunBuiltInOptimizers() { // this does not change the logical plan structure, but only simplifies the expression trees RunOptimizer(OptimizerType::EXPRESSION_REWRITER, [&]() { rewriter.VisitOperator(*plan); }); - // transform ORDER BY + LIMIT to TopN + // Rewrites SUM(x + C) into SUM(x) + C * COUNT(x) RunOptimizer(OptimizerType::SUM_REWRITER, [&]() { SumRewriterOptimizer optimizer(*this); optimizer.Optimize(plan); @@ -166,6 +167,13 @@ void Optimizer::RunBuiltInOptimizers() { plan = empty_result_pullup.Optimize(std::move(plan)); }); + // Removes Unnecessary Projections + RunOptimizer(OptimizerType::REMOVE_USELESS_PROJECTIONS, [&]() { + RemoveUselessProjections remover; + plan = remover.RemoveProjections(std::move(plan)); + remover.ReplaceBindings(*plan); + }); + // then we perform the join ordering optimization // this also rewrites cross products + filters into joins and performs filter pushdowns RunOptimizer(OptimizerType::JOIN_ORDER, [&]() { diff --git a/src/optimizer/remove_unused_columns.cpp b/src/optimizer/remove_unused_columns.cpp index 48ea7cbffe83..eb93856e9e4a 100644 --- a/src/optimizer/remove_unused_columns.cpp +++ b/src/optimizer/remove_unused_columns.cpp @@ -240,8 +240,7 @@ void RemoveUnusedColumns::VisitOperator(LogicalOperator &op) { throw InternalException("Could not find column index for table filter"); } - auto column_type = - filter.first == COLUMN_IDENTIFIER_ROW_ID ? LogicalType::ROW_TYPE : get.returned_types[filter.first]; + auto column_type = get.GetColumnType(ColumnIndex(filter.first)); ColumnBinding filter_binding(get.table_index, index.GetIndex()); auto column_ref = make_uniq(std::move(column_type), filter_binding); @@ -268,7 +267,7 @@ void RemoveUnusedColumns::VisitOperator(LogicalOperator &op) { // this generally means we are only interested in whether or not anything exists in the table (e.g. // EXISTS(SELECT * FROM tbl)) in this case, we just scan the row identifier column as it means we do not // need to read any of the columns - column_ids.emplace_back(COLUMN_IDENTIFIER_ROW_ID); + column_ids.emplace_back(get.GetAnyColumn()); } get.SetColumnIds(std::move(column_ids)); diff --git a/src/optimizer/remove_useless_projections.cpp b/src/optimizer/remove_useless_projections.cpp new file mode 100644 index 000000000000..9ed9d7a93cf2 --- /dev/null +++ b/src/optimizer/remove_useless_projections.cpp @@ -0,0 +1,70 @@ +#include "duckdb/optimizer/remove_useless_projections.hpp" +#include "duckdb/planner/expression/bound_columnref_expression.hpp" +#include "duckdb/planner/operator/logical_projection.hpp" +#include "duckdb/common/enums/logical_operator_type.hpp" + +namespace duckdb { + +unique_ptr RemoveUselessProjections::RemoveProjectionsChildren(unique_ptr op) { + for (idx_t i = 0; i < op->children.size(); i++) { + op->children[i] = RemoveProjections(std::move(op->children[i])); + } + return op; +} + +unique_ptr RemoveUselessProjections::RemoveProjections(unique_ptr op) { + switch (op->type) { + case LogicalOperatorType::LOGICAL_UNION: + case LogicalOperatorType::LOGICAL_EXCEPT: + case LogicalOperatorType::LOGICAL_RECURSIVE_CTE: + case LogicalOperatorType::LOGICAL_INTERSECT: + case LogicalOperatorType::LOGICAL_MATERIALIZED_CTE: { + for (idx_t i = 0; i < op->children.size(); i++) { + first_projection = true; + op->children[i] = RemoveProjections(std::move(op->children[i])); + } + return op; + } + default: + break; + } + if (op->type != LogicalOperatorType::LOGICAL_PROJECTION) { + return RemoveProjectionsChildren(std::move(op)); + } + // operator is a projection. Remove if possible + if (first_projection) { + first_projection = false; + return RemoveProjectionsChildren(std::move(op)); + } + auto &proj = op->Cast(); + auto child_bindings = op->children[0]->GetColumnBindings(); + if (proj.GetColumnBindings().size() != child_bindings.size()) { + return op; + } + idx_t binding_index = 0; + for (auto &expr : proj.expressions) { + if (expr->type != ExpressionType::BOUND_COLUMN_REF) { + return op; + } + auto &bound_ref = expr->Cast(); + if (bound_ref.binding != child_bindings[binding_index]) { + return op; + } + binding_index++; + } + D_ASSERT(binding_index == op->GetColumnBindings().size()); + // we have a projection where every expression is a bound column ref, and they are in the same order as the + // bindings of the child. We can remove this projection + binding_index = 0; + for (auto &binding : op->GetColumnBindings()) { + replacer.replacement_bindings.push_back(ReplacementBinding(binding, child_bindings[binding_index])); + binding_index++; + } + return RemoveProjectionsChildren(std::move(op->children[0])); +} + +void RemoveUselessProjections::ReplaceBindings(LogicalOperator &op) { + replacer.VisitOperator(op); +} + +} // namespace duckdb diff --git a/src/parser/transform/expression/transform_subquery.cpp b/src/parser/transform/expression/transform_subquery.cpp index 6f6d742073ba..0403d24bc5dc 100644 --- a/src/parser/transform/expression/transform_subquery.cpp +++ b/src/parser/transform/expression/transform_subquery.cpp @@ -107,6 +107,7 @@ unique_ptr Transformer::TransformSubquery(duckdb_libpgquery::P } } // transform constants (e.g. ORDER BY 1) into positional references (ORDER BY #1) + idx_t array_idx = 0; if (aggr->order_bys) { for (auto &order : aggr->order_bys->orders) { if (order.expression->GetExpressionType() == ExpressionType::VALUE_CONSTANT) { @@ -120,8 +121,10 @@ unique_ptr Transformer::TransformSubquery(duckdb_libpgquery::P } } else if (sub_select) { // if we have a SELECT we can push the ORDER BY clause into the SELECT list and reference it + auto alias = "__array_internal_idx_" + to_string(++array_idx); + order.expression->alias = alias; sub_select->select_list.push_back(std::move(order.expression)); - order.expression = make_uniq(sub_select->select_list.size() - 1); + order.expression = make_uniq(alias); } else { // otherwise we remove order qualifications RemoveOrderQualificationRecursive(order.expression); diff --git a/src/planner/bind_context.cpp b/src/planner/bind_context.cpp index d135ed2ae604..d22fe4799b7b 100644 --- a/src/planner/bind_context.cpp +++ b/src/planner/bind_context.cpp @@ -607,20 +607,28 @@ void BindContext::AddBinding(unique_ptr binding) { void BindContext::AddBaseTable(idx_t index, const string &alias, const vector &names, const vector &types, vector &bound_column_ids, StandardEntry &entry, bool add_row_id) { - AddBinding(make_uniq(alias, types, names, bound_column_ids, &entry, index, add_row_id)); + virtual_column_map_t virtual_columns; + if (add_row_id) { + virtual_columns.insert(make_pair(COLUMN_IDENTIFIER_ROW_ID, TableColumn("rowid", LogicalType::ROW_TYPE))); + } + AddBinding( + make_uniq(alias, types, names, bound_column_ids, &entry, index, std::move(virtual_columns))); } void BindContext::AddBaseTable(idx_t index, const string &alias, const vector &names, const vector &types, vector &bound_column_ids, const string &table_name) { + virtual_column_map_t virtual_columns; + virtual_columns.insert(make_pair(COLUMN_IDENTIFIER_ROW_ID, TableColumn("rowid", LogicalType::ROW_TYPE))); AddBinding(make_uniq(alias.empty() ? table_name : alias, types, names, bound_column_ids, nullptr, - index, true)); + index, std::move(virtual_columns))); } void BindContext::AddTableFunction(idx_t index, const string &alias, const vector &names, const vector &types, vector &bound_column_ids, - optional_ptr entry) { - AddBinding(make_uniq(alias, types, names, bound_column_ids, entry, index)); + optional_ptr entry, virtual_column_map_t virtual_columns) { + AddBinding( + make_uniq(alias, types, names, bound_column_ids, entry, index, std::move(virtual_columns))); } static string AddColumnNameToBinding(const string &base_name, case_insensitive_set_t ¤t_names) { diff --git a/src/planner/binder/statement/bind_delete.cpp b/src/planner/binder/statement/bind_delete.cpp index 822f487edcd6..a27d5b9d5209 100644 --- a/src/planner/binder/statement/bind_delete.cpp +++ b/src/planner/binder/statement/bind_delete.cpp @@ -73,10 +73,15 @@ BoundStatement Binder::Bind(DeleteStatement &stmt) { del->bound_constraints = BindConstraints(table); del->AddChild(std::move(root)); + auto virtual_columns = table.GetVirtualColumns(); + auto row_id_entry = virtual_columns.find(COLUMN_IDENTIFIER_ROW_ID); + if (row_id_entry == virtual_columns.end()) { + throw InternalException("BindDelete could not find the row id column in the virtual columns list of the table"); + } // set up the delete expression auto &column_ids = get.GetColumnIds(); - del->expressions.push_back( - make_uniq(table.GetRowIdType(), ColumnBinding(get.table_index, column_ids.size()))); + del->expressions.push_back(make_uniq(row_id_entry->second.type, + ColumnBinding(get.table_index, column_ids.size()))); get.AddColumnId(COLUMN_IDENTIFIER_ROW_ID); if (!stmt.returning_list.empty()) { diff --git a/src/planner/binder/statement/bind_update.cpp b/src/planner/binder/statement/bind_update.cpp index 75dd39074e0a..eb0f0f7c5339 100644 --- a/src/planner/binder/statement/bind_update.cpp +++ b/src/planner/binder/statement/bind_update.cpp @@ -133,9 +133,14 @@ BoundStatement Binder::Bind(UpdateStatement &stmt) { table.BindUpdateConstraints(*this, *get, *proj, *update, context); // finally add the row id column to the projection list + auto virtual_columns = table.GetVirtualColumns(); + auto row_id_entry = virtual_columns.find(COLUMN_IDENTIFIER_ROW_ID); + if (row_id_entry == virtual_columns.end()) { + throw InternalException("BindDelete could not find the row id column in the virtual columns list of the table"); + } auto &column_ids = get->GetColumnIds(); - proj->expressions.push_back( - make_uniq(table.GetRowIdType(), ColumnBinding(get->table_index, column_ids.size()))); + proj->expressions.push_back(make_uniq( + row_id_entry->second.type, ColumnBinding(get->table_index, column_ids.size()))); get->AddColumnId(COLUMN_IDENTIFIER_ROW_ID); // set the projection as child of the update node and finalize the result diff --git a/src/planner/binder/tableref/bind_basetableref.cpp b/src/planner/binder/tableref/bind_basetableref.cpp index d0302a1086e5..8209b7ef85af 100644 --- a/src/planner/binder/tableref/bind_basetableref.cpp +++ b/src/planner/binder/tableref/bind_basetableref.cpp @@ -262,7 +262,7 @@ unique_ptr Binder::Bind(BaseTableRef &ref) { auto logical_get = make_uniq(table_index, scan_function, std::move(bind_data), std::move(return_types), - std::move(return_names), table.GetRowIdType()); + std::move(return_names), table.GetVirtualColumns()); auto table_entry = logical_get->GetTable(); auto &col_ids = logical_get->GetMutableColumnIds(); if (!table_entry) { diff --git a/src/planner/binder/tableref/bind_table_function.cpp b/src/planner/binder/tableref/bind_table_function.cpp index ace96207bf98..dbb162495088 100644 --- a/src/planner/binder/tableref/bind_table_function.cpp +++ b/src/planner/binder/tableref/bind_table_function.cpp @@ -236,8 +236,13 @@ unique_ptr Binder::BindTableFunctionInternal(TableFunction &tab return_names[i] = "C" + to_string(i); } } + virtual_column_map_t virtual_columns; + if (table_function.get_virtual_columns) { + virtual_columns = table_function.get_virtual_columns(context, bind_data.get()); + } - auto get = make_uniq(bind_index, table_function, std::move(bind_data), return_types, return_names); + auto get = make_uniq(bind_index, table_function, std::move(bind_data), return_types, return_names, + virtual_columns); get->parameters = parameters; get->named_parameters = named_parameters; get->input_table_types = input_table_types; @@ -249,7 +254,7 @@ unique_ptr Binder::BindTableFunctionInternal(TableFunction &tab } // now add the table function to the bind context so its columns can be bound bind_context.AddTableFunction(bind_index, function_name, return_names, return_types, get->GetMutableColumnIds(), - get->GetTable().get()); + get->GetTable().get(), std::move(virtual_columns)); return std::move(get); } diff --git a/src/planner/operator/logical_get.cpp b/src/planner/operator/logical_get.cpp index be7b5aa5d796..e894cd8fab9a 100644 --- a/src/planner/operator/logical_get.cpp +++ b/src/planner/operator/logical_get.cpp @@ -17,10 +17,11 @@ LogicalGet::LogicalGet() : LogicalOperator(LogicalOperatorType::LOGICAL_GET) { } LogicalGet::LogicalGet(idx_t table_index, TableFunction function, unique_ptr bind_data, - vector returned_types, vector returned_names, LogicalType rowid_type) + vector returned_types, vector returned_names, + virtual_column_map_t virtual_columns_p) : LogicalOperator(LogicalOperatorType::LOGICAL_GET), table_index(table_index), function(std::move(function)), bind_data(std::move(bind_data)), returned_types(std::move(returned_types)), names(std::move(returned_names)), - extra_info(), rowid_type(std::move(rowid_type)) { + virtual_columns(std::move(virtual_columns_p)), extra_info() { } optional_ptr LogicalGet::GetTable() const { @@ -118,27 +119,57 @@ vector LogicalGet::GetColumnBindings() { return result; } +const LogicalType &LogicalGet::GetColumnType(const ColumnIndex &index) const { + if (index.IsVirtualColumn()) { + auto entry = virtual_columns.find(index.GetPrimaryIndex()); + if (entry == virtual_columns.end()) { + throw InternalException("Failed to find referenced virtual column %d", index.GetPrimaryIndex()); + } + return entry->second.type; + } + return returned_types[index.GetPrimaryIndex()]; +} + +const string &LogicalGet::GetColumnName(const ColumnIndex &index) const { + if (index.IsVirtualColumn()) { + auto entry = virtual_columns.find(index.GetPrimaryIndex()); + if (entry == virtual_columns.end()) { + throw InternalException("Failed to find referenced virtual column %d", index.GetPrimaryIndex()); + } + return entry->second.name; + } + return names[index.GetPrimaryIndex()]; +} + +column_t LogicalGet::GetAnyColumn() const { + auto entry = virtual_columns.find(COLUMN_IDENTIFIER_EMPTY); + if (entry != virtual_columns.end()) { + // return the empty column if the projection supports it + return COLUMN_IDENTIFIER_EMPTY; + } + entry = virtual_columns.find(COLUMN_IDENTIFIER_ROW_ID); + if (entry != virtual_columns.end()) { + // return the rowid column if the projection supports it + return COLUMN_IDENTIFIER_ROW_ID; + } + // otherwise return the first column + return 0; +} + void LogicalGet::ResolveTypes() { if (column_ids.empty()) { - column_ids.emplace_back(COLUMN_IDENTIFIER_ROW_ID); + // no projection - we need to push a column + column_ids.emplace_back(GetAnyColumn()); } types.clear(); if (projection_ids.empty()) { for (auto &index : column_ids) { - if (index.IsRowIdColumn()) { - types.emplace_back(LogicalType(rowid_type)); - } else { - types.push_back(returned_types[index.GetPrimaryIndex()]); - } + types.push_back(GetColumnType(index)); } } else { for (auto &proj_index : projection_ids) { auto &index = column_ids[proj_index]; - if (index.IsRowIdColumn()) { - types.emplace_back(LogicalType(rowid_type)); - } else { - types.push_back(returned_types[index.GetPrimaryIndex()]); - } + types.push_back(GetColumnType(index)); } } if (!projected_input.empty()) { @@ -225,6 +256,8 @@ unique_ptr LogicalGet::Deserialize(Deserializer &deserializer) result->column_ids.emplace_back(col_id); } } + auto &context = deserializer.Get(); + virtual_column_map_t virtual_columns; if (!has_serialize) { TableFunctionRef empty_ref; TableFunctionBindInput input(result->parameters, result->named_parameters, result->input_table_types, @@ -236,24 +269,44 @@ unique_ptr LogicalGet::Deserialize(Deserializer &deserializer) if (!function.bind) { throw InternalException("Table function \"%s\" has neither bind nor (de)serialize", function.name); } - bind_data = function.bind(deserializer.Get(), input, bind_return_types, bind_names); + bind_data = function.bind(context, input, bind_return_types, bind_names); + if (function.get_virtual_columns) { + virtual_columns = function.get_virtual_columns(context, bind_data.get()); + } for (auto &col_id : result->column_ids) { - if (col_id.IsRowIdColumn()) { - // rowid - continue; - } - auto idx = col_id.GetPrimaryIndex(); - auto &ret_type = result->returned_types[idx]; - auto &col_name = result->names[idx]; - if (bind_return_types[idx] != ret_type) { - throw SerializationException("Table function deserialization failure in function \"%s\" - column with " - "name %s was serialized with type %s, but now has type %s", - function.name, col_name, ret_type, bind_return_types[idx]); + if (col_id.IsVirtualColumn()) { + auto idx = col_id.GetPrimaryIndex(); + auto ventry = virtual_columns.find(idx); + if (ventry == virtual_columns.end()) { + throw SerializationException( + "Table function deserialization failure - could not find virtual column with id %d", idx); + } + auto &ret_type = ventry->second.type; + auto &col_name = ventry->second.name; + if (bind_return_types[idx] != ret_type) { + throw SerializationException( + "Table function deserialization failure in function \"%s\" - virtual column with " + "name %s was serialized with type %s, but now has type %s", + function.name, col_name, ret_type, bind_return_types[idx]); + } + } else { + auto idx = col_id.GetPrimaryIndex(); + auto &ret_type = result->returned_types[idx]; + auto &col_name = result->names[idx]; + if (bind_return_types[idx] != ret_type) { + throw SerializationException( + "Table function deserialization failure in function \"%s\" - column with " + "name %s was serialized with type %s, but now has type %s", + function.name, col_name, ret_type, bind_return_types[idx]); + } } } result->returned_types = std::move(bind_return_types); + } else if (function.get_virtual_columns) { + virtual_columns = function.get_virtual_columns(context, bind_data.get()); } + result->virtual_columns = std::move(virtual_columns); result->bind_data = std::move(bind_data); return std::move(result); } diff --git a/src/planner/table_binding.cpp b/src/planner/table_binding.cpp index 455834a4bb0d..e18d899d60a8 100644 --- a/src/planner/table_binding.cpp +++ b/src/planner/table_binding.cpp @@ -16,9 +16,9 @@ namespace duckdb { Binding::Binding(BindingType binding_type, BindingAlias alias_p, vector coltypes, vector colnames, - idx_t index, LogicalType rowid_type) + idx_t index) : binding_type(binding_type), alias(std::move(alias_p)), index(index), types(std::move(coltypes)), - names(std::move(colnames)), rowid_type(std::move(rowid_type)) { + names(std::move(colnames)) { D_ASSERT(types.size() == names.size()); for (idx_t i = 0; i < names.size(); i++) { auto &name = names[i]; @@ -114,13 +114,23 @@ optional_ptr EntryBinding::GetStandardEntry() { TableBinding::TableBinding(const string &alias, vector types_p, vector names_p, vector &bound_column_ids, optional_ptr entry, idx_t index, - bool add_row_id) - : Binding(BindingType::TABLE, GetAlias(alias, entry), std::move(types_p), std::move(names_p), index, - (add_row_id && entry) ? entry->Cast().GetRowIdType() : LogicalType::ROW_TYPE), - bound_column_ids(bound_column_ids), entry(entry) { - if (add_row_id) { - if (name_map.find("rowid") == name_map.end()) { - name_map["rowid"] = COLUMN_IDENTIFIER_ROW_ID; + virtual_column_map_t virtual_columns_p) + : Binding(BindingType::TABLE, GetAlias(alias, entry), std::move(types_p), std::move(names_p), index), + bound_column_ids(bound_column_ids), entry(entry), virtual_columns(std::move(virtual_columns_p)) { + for (auto &ventry : virtual_columns) { + auto idx = ventry.first; + auto &name = ventry.second.name; + if (idx < VIRTUAL_COLUMN_START) { + throw BinderException( + "Virtual column index must be larger than VIRTUAL_COLUMN_START - found %d for column \"%s\"", idx, + name); + } + if (idx == COLUMN_IDENTIFIER_EMPTY) { + // the empty column cannot be queried by the user + continue; + } + if (name_map.find(name) == name_map.end()) { + name_map[name] = idx; } } } @@ -238,8 +248,10 @@ BindResult TableBinding::Bind(ColumnRefExpression &colref, idx_t depth) { } // fetch the type of the column LogicalType col_type; - if (column_index == COLUMN_IDENTIFIER_ROW_ID) { - col_type = LogicalType(rowid_type); + auto ventry = virtual_columns.find(column_index); + if (ventry != virtual_columns.end()) { + // virtual column - fetch type from there + col_type = ventry->second.type; } else { // normal column: fetch type from base column col_type = types[column_index]; diff --git a/src/storage/checkpoint/write_overflow_strings_to_disk.cpp b/src/storage/checkpoint/write_overflow_strings_to_disk.cpp index c58be310271c..37492f787cbe 100644 --- a/src/storage/checkpoint/write_overflow_strings_to_disk.cpp +++ b/src/storage/checkpoint/write_overflow_strings_to_disk.cpp @@ -87,9 +87,6 @@ void WriteOverflowStringsToDisk::Flush() { // write to disk auto &block_manager = partial_block_manager.GetBlockManager(); block_manager.Write(handle.GetFileBuffer(), block_id); - - auto lock = partial_block_manager.GetLock(); - partial_block_manager.AddWrittenBlock(block_id); } block_id = INVALID_BLOCK; offset = 0; diff --git a/src/storage/compression/zstd.cpp b/src/storage/compression/zstd.cpp index fca90c18099c..b3cac8107343 100644 --- a/src/storage/compression/zstd.cpp +++ b/src/storage/compression/zstd.cpp @@ -474,10 +474,6 @@ class ZSTDCompressionState : public CompressionState { // Write the current page to disk auto &block_manager = partial_block_manager.GetBlockManager(); block_manager.Write(buffer.GetFileBuffer(), block_id); - { - auto lock = partial_block_manager.GetLock(); - partial_block_manager.AddWrittenBlock(block_id); - } } void FlushVector() { diff --git a/src/storage/data_table.cpp b/src/storage/data_table.cpp index fb9baddf9642..eaddd112175a 100644 --- a/src/storage/data_table.cpp +++ b/src/storage/data_table.cpp @@ -854,14 +854,24 @@ void DataTable::FinalizeLocalAppend(LocalAppendState &state) { LocalStorage::FinalizeAppend(state); } -OptimisticDataWriter &DataTable::CreateOptimisticWriter(ClientContext &context) { +PhysicalIndex DataTable::CreateOptimisticCollection(ClientContext &context, unique_ptr collection) { auto &local_storage = LocalStorage::Get(context, db); - return local_storage.CreateOptimisticWriter(*this); + return local_storage.CreateOptimisticCollection(*this, std::move(collection)); } -void DataTable::FinalizeOptimisticWriter(ClientContext &context, OptimisticDataWriter &writer) { +RowGroupCollection &DataTable::GetOptimisticCollection(ClientContext &context, const PhysicalIndex collection_index) { auto &local_storage = LocalStorage::Get(context, db); - local_storage.FinalizeOptimisticWriter(*this, writer); + return local_storage.GetOptimisticCollection(*this, collection_index); +} + +void DataTable::ResetOptimisticCollection(ClientContext &context, const PhysicalIndex collection_index) { + auto &local_storage = LocalStorage::Get(context, db); + local_storage.ResetOptimisticCollection(*this, collection_index); +} + +OptimisticDataWriter &DataTable::GetOptimisticWriter(ClientContext &context) { + auto &local_storage = LocalStorage::Get(context, db); + return local_storage.GetOptimisticWriter(*this); } void DataTable::LocalMerge(ClientContext &context, RowGroupCollection &collection) { @@ -1541,8 +1551,8 @@ void DataTable::Checkpoint(TableDataWriter &writer, Serializer &serializer) { writer.FinalizeTable(global_stats, info.get(), serializer); } -void DataTable::CommitDropColumn(idx_t index) { - row_groups->CommitDropColumn(index); +void DataTable::CommitDropColumn(const idx_t column_index) { + row_groups->CommitDropColumn(column_index); } idx_t DataTable::ColumnCount() const { diff --git a/src/storage/local_storage.cpp b/src/storage/local_storage.cpp index 93dccfcba763..d8339fdadb35 100644 --- a/src/storage/local_storage.cpp +++ b/src/storage/local_storage.cpp @@ -50,31 +50,41 @@ LocalTableStorage::LocalTableStorage(ClientContext &context, DataTable &table) }); } -LocalTableStorage::LocalTableStorage(ClientContext &context, DataTable &new_dt, LocalTableStorage &parent, - idx_t changed_idx, const LogicalType &target_type, +LocalTableStorage::LocalTableStorage(ClientContext &context, DataTable &new_data_table, LocalTableStorage &parent, + const idx_t alter_column_index, const LogicalType &target_type, const vector &bound_columns, Expression &cast_expr) - : table_ref(new_dt), allocator(Allocator::Get(new_dt.db)), deleted_rows(parent.deleted_rows), - optimistic_writer(new_dt, parent.optimistic_writer), optimistic_writers(std::move(parent.optimistic_writers)), - merged_storage(parent.merged_storage) { - row_groups = parent.row_groups->AlterType(context, changed_idx, target_type, bound_columns, cast_expr); + : table_ref(new_data_table), allocator(Allocator::Get(new_data_table.db)), deleted_rows(parent.deleted_rows), + optimistic_collections(std::move(parent.optimistic_collections)), + optimistic_writer(new_data_table, parent.optimistic_writer), merged_storage(parent.merged_storage) { + + // Alter the column type. + row_groups = parent.row_groups->AlterType(context, alter_column_index, target_type, bound_columns, cast_expr); + parent.row_groups->CommitDropColumn(alter_column_index); parent.row_groups.reset(); + append_indexes.Move(parent.append_indexes); } -LocalTableStorage::LocalTableStorage(DataTable &new_dt, LocalTableStorage &parent, idx_t drop_idx) - : table_ref(new_dt), allocator(Allocator::Get(new_dt.db)), deleted_rows(parent.deleted_rows), - optimistic_writer(new_dt, parent.optimistic_writer), optimistic_writers(std::move(parent.optimistic_writers)), - merged_storage(parent.merged_storage) { - row_groups = parent.row_groups->RemoveColumn(drop_idx); +LocalTableStorage::LocalTableStorage(DataTable &new_data_table, LocalTableStorage &parent, + const idx_t drop_column_index) + : table_ref(new_data_table), allocator(Allocator::Get(new_data_table.db)), deleted_rows(parent.deleted_rows), + optimistic_collections(std::move(parent.optimistic_collections)), + optimistic_writer(new_data_table, parent.optimistic_writer), merged_storage(parent.merged_storage) { + + // Remove the column from the previous table storage. + row_groups = parent.row_groups->RemoveColumn(drop_column_index); + parent.row_groups->CommitDropColumn(drop_column_index); parent.row_groups.reset(); + append_indexes.Move(parent.append_indexes); } LocalTableStorage::LocalTableStorage(ClientContext &context, DataTable &new_dt, LocalTableStorage &parent, ColumnDefinition &new_column, ExpressionExecutor &default_executor) : table_ref(new_dt), allocator(Allocator::Get(new_dt.db)), deleted_rows(parent.deleted_rows), - optimistic_writer(new_dt, parent.optimistic_writer), optimistic_writers(std::move(parent.optimistic_writers)), - merged_storage(parent.merged_storage) { + optimistic_collections(std::move(parent.optimistic_collections)), + optimistic_writer(new_dt, parent.optimistic_writer), merged_storage(parent.merged_storage) { + row_groups = parent.row_groups->AddColumn(context, new_column, default_executor); parent.row_groups.reset(); append_indexes.Move(parent.append_indexes); @@ -220,34 +230,38 @@ void LocalTableStorage::AppendToIndexes(DuckTransaction &transaction, TableAppen } } -OptimisticDataWriter &LocalTableStorage::CreateOptimisticWriter() { - auto writer = make_uniq(table_ref.get()); - optimistic_writers.push_back(std::move(writer)); - return *optimistic_writers.back(); +PhysicalIndex LocalTableStorage::CreateOptimisticCollection(unique_ptr collection) { + lock_guard l(collections_lock); + optimistic_collections.push_back(std::move(collection)); + return PhysicalIndex(optimistic_collections.size() - 1); } -void LocalTableStorage::FinalizeOptimisticWriter(OptimisticDataWriter &writer) { - // remove the writer from the set of optimistic writers - unique_ptr owned_writer; - for (idx_t i = 0; i < optimistic_writers.size(); i++) { - if (optimistic_writers[i].get() == &writer) { - owned_writer = std::move(optimistic_writers[i]); - optimistic_writers.erase_at(i); - break; - } - } - if (!owned_writer) { - throw InternalException("Error in FinalizeOptimisticWriter - could not find writer"); - } - optimistic_writer.Merge(*owned_writer); +RowGroupCollection &LocalTableStorage::GetOptimisticCollection(const PhysicalIndex collection_index) { + lock_guard l(collections_lock); + auto &collection = optimistic_collections[collection_index.index]; + return *collection; +} + +void LocalTableStorage::ResetOptimisticCollection(const PhysicalIndex collection_index) { + lock_guard l(collections_lock); + optimistic_collections[collection_index.index].reset(); +} + +OptimisticDataWriter &LocalTableStorage::GetOptimisticWriter() { + return optimistic_writer; } void LocalTableStorage::Rollback() { - for (auto &writer : optimistic_writers) { - writer->Rollback(); - } - optimistic_writers.clear(); optimistic_writer.Rollback(); + + for (auto &collection : optimistic_collections) { + if (!collection) { + continue; + } + collection->CommitDropTable(); + } + optimistic_collections.clear(); + row_groups->CommitDropTable(); } //===--------------------------------------------------------------------===// @@ -435,14 +449,24 @@ void LocalStorage::LocalMerge(DataTable &table, RowGroupCollection &collection) storage.merged_storage = true; } -OptimisticDataWriter &LocalStorage::CreateOptimisticWriter(DataTable &table) { +PhysicalIndex LocalStorage::CreateOptimisticCollection(DataTable &table, unique_ptr collection) { auto &storage = table_manager.GetOrCreateStorage(context, table); - return storage.CreateOptimisticWriter(); + return storage.CreateOptimisticCollection(std::move(collection)); } -void LocalStorage::FinalizeOptimisticWriter(DataTable &table, OptimisticDataWriter &writer) { +RowGroupCollection &LocalStorage::GetOptimisticCollection(DataTable &table, const PhysicalIndex collection_index) { auto &storage = table_manager.GetOrCreateStorage(context, table); - storage.FinalizeOptimisticWriter(writer); + return storage.GetOptimisticCollection(collection_index); +} + +void LocalStorage::ResetOptimisticCollection(DataTable &table, const PhysicalIndex collection_index) { + auto &storage = table_manager.GetOrCreateStorage(context, table); + storage.ResetOptimisticCollection(collection_index); +} + +OptimisticDataWriter &LocalStorage::GetOptimisticWriter(DataTable &table) { + auto &storage = table_manager.GetOrCreateStorage(context, table); + return storage.GetOptimisticWriter(); } bool LocalStorage::ChangesMade() noexcept { @@ -549,7 +573,6 @@ void LocalStorage::Rollback() { continue; } storage->Rollback(); - entry.second.reset(); } } @@ -600,13 +623,13 @@ void LocalStorage::AddColumn(DataTable &old_dt, DataTable &new_dt, ColumnDefinit table_manager.InsertEntry(new_dt, std::move(new_storage)); } -void LocalStorage::DropColumn(DataTable &old_dt, DataTable &new_dt, idx_t removed_column) { +void LocalStorage::DropColumn(DataTable &old_dt, DataTable &new_dt, const idx_t drop_column_index) { // check if there are any pending appends for the old version of the table auto storage = table_manager.MoveEntry(old_dt); if (!storage) { return; } - auto new_storage = make_shared_ptr(new_dt, *storage, removed_column); + auto new_storage = make_shared_ptr(new_dt, *storage, drop_column_index); table_manager.InsertEntry(new_dt, std::move(new_storage)); } diff --git a/src/storage/partial_block_manager.cpp b/src/storage/partial_block_manager.cpp index 3dbf89760591..7c23df3da75b 100644 --- a/src/storage/partial_block_manager.cpp +++ b/src/storage/partial_block_manager.cpp @@ -46,6 +46,7 @@ PartialBlockManager::PartialBlockManager(BlockManager &block_manager, PartialBlo // Use the default maximum partial block size with a ratio of 20% free and 80% utilization. max_partial_block_size = NumericCast(block_manager.GetBlockSize() / 5 * 4); } + PartialBlockManager::~PartialBlockManager() { } @@ -132,7 +133,6 @@ void PartialBlockManager::RegisterPartialBlock(PartialBlockAllocation allocation // Flush any block that we're not going to reuse. if (block_to_free) { block_to_free->Flush(free_space); - AddWrittenBlock(block_to_free->state.block_id); } } @@ -161,21 +161,9 @@ void PartialBlockManager::Merge(PartialBlockManager &other) { partially_filled_blocks.insert(make_pair(e.first, std::move(e.second))); } } - // copy over the written blocks - for (auto &block_id : other.written_blocks) { - AddWrittenBlock(block_id); - } - other.written_blocks.clear(); other.partially_filled_blocks.clear(); } -void PartialBlockManager::AddWrittenBlock(block_id_t block) { - auto entry = written_blocks.insert(block); - if (!entry.second) { - throw InternalException("Written block already exists"); - } -} - void PartialBlockManager::ClearBlocks() { for (auto &e : partially_filled_blocks) { e.second->Clear(); @@ -196,9 +184,6 @@ BlockManager &PartialBlockManager::GetBlockManager() const { void PartialBlockManager::Rollback() { ClearBlocks(); - for (auto &block_id : written_blocks) { - block_manager.MarkBlockAsFree(block_id); - } } } // namespace duckdb diff --git a/src/storage/table/column_checkpoint_state.cpp b/src/storage/table/column_checkpoint_state.cpp index a67daa060b76..d2fce922af48 100644 --- a/src/storage/table/column_checkpoint_state.cpp +++ b/src/storage/table/column_checkpoint_state.cpp @@ -71,7 +71,6 @@ void PartialBlockForCheckpoint::Flush(const idx_t free_space_left) { } } } - Clear(); } diff --git a/src/storage/table/row_group.cpp b/src/storage/table/row_group.cpp index d5250387362b..04cb741e32f3 100644 --- a/src/storage/table/row_group.cpp +++ b/src/storage/table/row_group.cpp @@ -385,8 +385,9 @@ void RowGroup::CommitDrop() { } } -void RowGroup::CommitDropColumn(idx_t column_idx) { - GetColumn(column_idx).CommitDropColumn(); +void RowGroup::CommitDropColumn(const idx_t column_index) { + auto &column = GetColumn(column_index); + column.CommitDropColumn(); } void RowGroup::NextVector(CollectionScanState &state) { @@ -430,14 +431,13 @@ bool RowGroup::CheckZonemap(ScanFilterInfo &filters) { if (prune_result == FilterPropagateResult::FILTER_ALWAYS_FALSE) { return false; } - if (prune_result == FilterPropagateResult::FILTER_ALWAYS_TRUE) { - // filter is always true - no need to check it - // label the filter as always true so we don't need to check it anymore - filters.SetFilterAlwaysTrue(i); - } if (filter.filter_type == TableFilterType::OPTIONAL_FILTER) { // these are only for row group checking, set as always true so we don't check it filters.SetFilterAlwaysTrue(i); + } else if (prune_result == FilterPropagateResult::FILTER_ALWAYS_TRUE) { + // filter is always true - no need to check it + // label the filter as always true so we don't need to check it anymore + filters.SetFilterAlwaysTrue(i); } } return true; @@ -619,7 +619,7 @@ void RowGroup::TemplatedScan(TransactionData transaction, CollectionScanState &s if (prune_result == FilterPropagateResult::FILTER_ALWAYS_FALSE) { // We can just break out of the loop here. approved_tuple_count = 0; - break; + continue; } // Generate row ids diff --git a/src/storage/table/row_group_collection.cpp b/src/storage/table/row_group_collection.cpp index dc0a7eb47eb7..97eae11248ee 100644 --- a/src/storage/table/row_group_collection.cpp +++ b/src/storage/table/row_group_collection.cpp @@ -1082,9 +1082,9 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl //===--------------------------------------------------------------------===// // CommitDrop //===--------------------------------------------------------------------===// -void RowGroupCollection::CommitDropColumn(idx_t index) { +void RowGroupCollection::CommitDropColumn(const idx_t column_index) { for (auto &row_group : row_groups->Segments()) { - row_group.CommitDropColumn(index); + row_group.CommitDropColumn(column_index); } } diff --git a/src/storage/table/scan_state.cpp b/src/storage/table/scan_state.cpp index adeccde91b03..fdfa76433059 100644 --- a/src/storage/table/scan_state.cpp +++ b/src/storage/table/scan_state.cpp @@ -96,6 +96,9 @@ void ScanFilterInfo::CheckAllFilters() { void ScanFilterInfo::SetFilterAlwaysTrue(idx_t filter_idx) { auto &filter = filter_list[filter_idx]; + if (filter.always_true) { + return; + } filter.always_true = true; column_has_filter[filter.scan_column_index] = false; always_true_filters++; diff --git a/test/api/adbc/test_adbc.cpp b/test/api/adbc/test_adbc.cpp index afcbb596d073..935f483d52a4 100644 --- a/test/api/adbc/test_adbc.cpp +++ b/test/api/adbc/test_adbc.cpp @@ -194,6 +194,19 @@ TEST_CASE("ADBC - Test ingestion - Lineitem", "[adbc]") { REQUIRE(db.QueryAndCheck("SELECT l_partkey, l_comment FROM lineitem WHERE l_orderkey=1 ORDER BY l_linenumber")); } +TEST_CASE("ADBC - Pivot", "[adbc]") { + if (!duckdb_lib) { + return; + } + ADBCTestDatabase db; + + auto input_data = db.QueryArrow("SELECT * FROM read_csv_auto(\'data/csv/flights.csv\')"); + + db.CreateTable("flights", input_data); + + REQUIRE(db.QueryAndCheck("PIVOT flights ON UniqueCarrier USING COUNT(1) GROUP BY OriginCityName;")); +} + TEST_CASE("Test Null Error/Database", "[adbc]") { if (!duckdb_lib) { return; @@ -1364,8 +1377,8 @@ TEST_CASE("Test AdbcConnectionGetObjects", "[adbc]") { REQUIRE((res->ColumnCount() == 2)); REQUIRE((res->RowCount() == 3)); REQUIRE((res->GetValue(1, 0).ToString() == - "[{'db_schema_name': information_schema, 'db_schema_tables': NULL}, {'db_schema_name': main, " - "'db_schema_tables': NULL}, {'db_schema_name': pg_catalog, 'db_schema_tables': NULL}]")); + "[{'db_schema_name': pg_catalog, 'db_schema_tables': NULL}, {'db_schema_name': information_schema, " + "'db_schema_tables': NULL}, {'db_schema_name': main, 'db_schema_tables': NULL}]")); db.Query("Drop table result;"); AdbcConnectionGetObjects(&db.adbc_connection, ADBC_OBJECT_DEPTH_COLUMNS, nullptr, nullptr, nullptr, nullptr, diff --git a/test/extension/autoloading_base.test b/test/extension/autoloading_base.test index 591383eb80f6..80c120e8d119 100644 --- a/test/extension/autoloading_base.test +++ b/test/extension/autoloading_base.test @@ -15,6 +15,12 @@ SELECT (count(*) > 0) FROM duckdb_extensions() WHERE install_path ILIKE '%duckdb ---- false +# All extensions reported by duckdb are either statically linked or not installed +query I +SELECT count(*) FROM duckdb_extensions() WHERE install_mode != 'NOT_INSTALLED' AND install_mode != 'STATICALLY_LINKED' +---- +0 + ### No autoloading nor installing: throw error with installation hint statement ok set autoload_known_extensions=false diff --git a/test/issues/general/test_16257.test_slow b/test/issues/general/test_16257.test_slow new file mode 100644 index 000000000000..6b3faf9a7ba4 --- /dev/null +++ b/test/issues/general/test_16257.test_slow @@ -0,0 +1,25 @@ +# name: test/issues/general/test_16257.test_slow +# description: Issue 16257 - value count mismatch when writing DELTA_BINARY_PACKED +# group: [general] + +require parquet + +# Some macros to generate lorem ipsum +statement ok +CREATE OR REPLACE MACRO deterministic_random(rand) AS hash(rand) / 18446744073709551615; + +statement ok +CREATE OR REPLACE MACRO lorem_word(rand) AS ['voluptatem', 'quaerat', 'quiquia', 'non', 'dolore', 'dolorem', 'labore', 'consectetur', 'porro', 'sed', 'numquam', 'aliquam', 'sit', 'eius', 'modi', 'est', 'amet', 'magnam', 'dolor', 'etincidunt', 'velit', 'neque', 'ipsum', 'adipisci', 'quisquam', 'ut', 'tempora'][1 + floor(rand * 27 % 27)::BIGINT]; + +statement ok +CREATE OR REPLACE MACRO lorem_sentence_util(s) AS upper(s[1]) || s[2:] || '.'; + +statement ok +CREATE OR REPLACE MACRO lorem_sentence(rand, words) AS lorem_sentence_util(list_aggr([lorem_word(deterministic_random(rand + i)) for i in range(words)], 'string_agg', ' ')); + + +statement ok +SET preserve_insertion_order=false; + +statement ok +COPY (SELECT lorem_sentence(random(), 20) FROM range(1_000_000)) TO '__TEST_DIR__/16257.parquet' (PARQUET_VERSION V2, ROW_GROUP_SIZE 2_000_000); diff --git a/test/optimizer/joins/asof_join_adds_rows.test b/test/optimizer/joins/asof_join_adds_rows.test index 2b15fbcb45a3..2ebdfd145f3a 100644 --- a/test/optimizer/joins/asof_join_adds_rows.test +++ b/test/optimizer/joins/asof_join_adds_rows.test @@ -37,6 +37,12 @@ create table large_build as from values (1, '1992-03-22 01:02:19'::TIMESTAMP), (1, '1992-03-22 01:02:20'::TIMESTAMP) t(lb_const, b); +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + query I select a from (select * from small_probe, child_join where c=sp_const) asof join large_build on (lb_const = sp_const and a < b) order by a; ---- @@ -109,4 +115,6 @@ ORDER BY timepoint; ---- ID1 fqn1 fqn1 2021-01-01 00:00:00 -ID1 fqn2 fqn2 2021-03-03 00:00:00 \ No newline at end of file +ID1 fqn2 fqn2 2021-03-03 00:00:00 + +endloop diff --git a/test/optimizer/joins/cross_join_and_unnest_dont_work.test b/test/optimizer/joins/cross_join_and_unnest_dont_work.test index ee1eee4acd87..4c4c6bf5356e 100644 --- a/test/optimizer/joins/cross_join_and_unnest_dont_work.test +++ b/test/optimizer/joins/cross_join_and_unnest_dont_work.test @@ -37,6 +37,12 @@ create table large_build as from values (1, '1992-03-22 01:02:19'::TIMESTAMP), (1, '1992-03-22 01:02:20'::TIMESTAMP) t(lb_const, b); +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + query I select a from (select * from small_probe, child_join where c=sp_const) asof join large_build on (lb_const = sp_const and a < b) order by a; ---- @@ -109,4 +115,6 @@ ORDER BY timepoint; ---- ID1 fqn1 fqn1 2021-01-01 00:00:00 -ID1 fqn2 fqn2 2021-03-03 00:00:00 \ No newline at end of file +ID1 fqn2 fqn2 2021-03-03 00:00:00 + +endloop diff --git a/test/optimizer/pullup_filters.test b/test/optimizer/pullup_filters.test index 62a76de2335e..87bd01e99daf 100644 --- a/test/optimizer/pullup_filters.test +++ b/test/optimizer/pullup_filters.test @@ -6,13 +6,13 @@ statement ok PRAGMA explain_output = 'PHYSICAL_ONLY' statement ok -CREATE TABLE vals1 AS SELECT i AS i, i AS j FROM range(0, 11, 1) t1(i) +CREATE TABLE vals1 AS SELECT i AS i, i AS j FROM range(0, 11, 1) t1(i); statement ok -CREATE TABLE vals2(k BIGINT, l BIGINT) +CREATE TABLE vals2(k BIGINT, l BIGINT); statement ok -INSERT INTO vals2 SELECT * FROM vals1 +INSERT INTO vals2 SELECT * FROM vals1; ## INNER JOIN: pull up a single filter in cross product from LHS query II @@ -30,13 +30,13 @@ physical_plan :.*=5.*=5.* query II EXPLAIN SELECT * FROM (SELECT * FROM vals1, vals2 WHERE i=5 AND k=3) tbl1, (SELECT * FROM vals1, vals2) tbl2 WHERE tbl1.i=tbl2.i AND tbl1.k=tbl2.k ---- -physical_plan :(.*=5.*=3.*=5.*=3.*|.*=3.*=5.*=3.*=5.*) +physical_plan :(.*=5.*=5.*=3.*=3.*|.*=3.*=3.*=5.*=5.*) ## INNER JOIN: pull up two filters in cross product from RHS query II EXPLAIN SELECT * FROM (SELECT * FROM vals1, vals2) tbl1, (SELECT * FROM vals1, vals2 WHERE i=5 AND k=3) tbl2 WHERE tbl1.i=tbl2.i AND tbl1.k=tbl2.k ---- -physical_plan :(.*=5.*=3.*=5.*=3.*|.*=3.*=5.*=3.*=5.*) +physical_plan :(.*=5.*=5.*=3.*=3.*|.*=3.*=3.*=5.*=5.*) #### LEFT JOIN: pull up a single filter from LHS #### query II diff --git a/test/optimizer/remove_unnecessary_projections.test b/test/optimizer/remove_unnecessary_projections.test new file mode 100644 index 000000000000..89f15c3c191f --- /dev/null +++ b/test/optimizer/remove_unnecessary_projections.test @@ -0,0 +1,31 @@ +# name: test/optimizer/remove_unnecessary_projections.test +# description: Test regex to like Optimization Rules +# group: [optimizer] + +statement ok +pragma disabled_optimizers='statistics_propagation,column_lifetime'; + +statement ok +create table t1 as select range%50 a from range(10000); + +statement ok +create table t2 as select range b from range(100); + +statement ok +create table t3 as select range c from range(10000); + +statement ok +create table t4 as select range d from range(400); + +query II +explain select * from (select * from t1, t2 where a = b) t_left, (select * from t3, t4 where c = d) t_right where a = d; +---- +physical_plan :.*PROJECTION.*PROJECTION.* + +statement ok +pragma explain_output='optimized_only'; + +query II +explain select a b from (select b a from (select a b from values (1), (2), (3) t(a))); +---- +logical_opt :.*PROJECTION.*PROJECTION.*PROJECTION.* \ No newline at end of file diff --git a/test/sql/aggregate/aggregates/histogram_table_function.test b/test/sql/aggregate/aggregates/histogram_table_function.test index 3b54ef1270e8..846789118d0b 100644 --- a/test/sql/aggregate/aggregates/histogram_table_function.test +++ b/test/sql/aggregate/aggregates/histogram_table_function.test @@ -65,18 +65,9 @@ statement ok INSERT INTO integers VALUES (99999999) query II -SELECT * FROM histogram_values(integers, i, technique := 'equi-height') +SELECT COUNT(*), AVG(count) FROM histogram_values(integers, i, technique := 'equi-height') ---- -12 13 -25 13 -38 13 -50 13 -63 13 -76 13 -88 13 -101 13 -114 13 -99999999 13 +10 13 # sample integers query II diff --git a/test/sql/cast/string_to_list_cast.test b/test/sql/cast/string_to_list_cast.test index 45a84603810a..9fdd256e0ece 100644 --- a/test/sql/cast/string_to_list_cast.test +++ b/test/sql/cast/string_to_list_cast.test @@ -16,7 +16,7 @@ SELECT '[12,13,14]'::INT[]; query I SELECT '["hello", "world", "!"]'::VARCHAR[]; ---- -["hello", "world", "!"] +[hello, world, !] query I SELECT CAST('[Hello World!]' AS VARCHAR[]); @@ -110,8 +110,8 @@ INSERT INTO stringList VALUES ('["hello","world","!"]'), ('["Amazing","text"]'), query I SELECT col1::VARCHAR[] FROM stringList; ---- -["hello", "world", "!"] -["Amazing", "text"] +[hello, world, !] +[Amazing, text] [Hello World!] # --------------------------------------------------- @@ -124,8 +124,8 @@ INSERT INTO nestedStrings VALUES ('[["hello"], ["world"],["!"]]'), ('[["Amazing" query I SELECT col1::VARCHAR[][] FROM nestedStrings; ---- -[["hello"], ["world"], ["!"]] -[["Amazing"], ["text"]] +[[hello], [world], [!]] +[[Amazing], [text]] [[Hello World!]] # --------------------------------------------------- @@ -138,8 +138,8 @@ INSERT INTO superNestedStrings VALUES ('[[[[["hello"]]], [[["world"],["!"]]]]]') query I SELECT col1::VARCHAR[][][][][] FROM superNestedStrings; ---- -[[[[["hello"]]], [[["world"], ["!"]]]]] -[[[[["Amazing"]], [["text"]]]]] +[[[[[hello]]], [[[world], [!]]]]] +[[[[[Amazing]], [[text]]]]] [[[[[Hello World!]]]]] # --------------------------------------------------- @@ -201,39 +201,39 @@ SELECT col1::INT[][][][][][] FROM crazyNested; # Quote handling # --------------------------------------------------- query I -SELECT CAST('[''hello'',''world'', ''!'']' AS VARCHAR[]); +SELECT CAST($$['hello','world', '!']$$ AS VARCHAR[]); ---- -['hello', 'world', '!'] +[hello, world, !] query I -SELECT CAST('[''''hello'''',''''world'''', ''''!'''']' AS VARCHAR[]); +SELECT CAST($$[\'hello\',\'world\', \'!\']$$ AS VARCHAR[]); ---- -[''hello'', ''world'', ''!''] +['hello', 'world', '!'] query I -SELECT CAST('[[ [''🦆, 🦆, 🦆'']], [[duck, db, ''🦆''] ]]' AS VARCHAR[][][]); +SELECT CAST($$[[ ['🦆, 🦆, 🦆']], [[duck, db, '🦆'] ]]$$ AS VARCHAR[][][]); ---- -[[['🦆, 🦆, 🦆']], [[duck, db, '🦆']]] +[[[🦆, 🦆, 🦆]], [[duck, db, 🦆]]] query I -SELECT CAST('["can''t", "you''re", "i''m"]' AS VARCHAR[]); +SELECT CAST($$[can\'t, you\'re, i\'m]$$ AS VARCHAR[]); ---- -["can't", "you're", "i'm"] +[can't, you're, i'm] query I -SELECT CAST('[can''t, you''re, i''m]' AS VARCHAR[]); +SELECT CAST($$[can\'t, you\'re, i\'m]$$ AS VARCHAR[]); ---- [can't, you're, i'm] query I -SELECT CAST('["]", "hello", "world"]' AS VARCHAR[]); +SELECT CAST($$["]", "hello", "world"]$$ AS VARCHAR[]); ---- -["]", "hello", "world"] +[], hello, world] query I -SELECT CAST('['']'', "hello", "world"]' AS VARCHAR[]); +SELECT CAST($$[']', "hello", "world"]$$ AS VARCHAR[]); ---- -[']', "hello", "world"] +[], hello, world] # Test for whitespaces @@ -249,9 +249,9 @@ SELECT CAST('[ [ [12, 13,14], [8, 9 ] ],[[ 4 ] ], [[[12, 13, 14], [8, 9]], [[4]], [[2, 1, 0]]] query I -SELECT CAST('[" hello"," '' world", "! "]' AS VARCHAR[]); +SELECT CAST($$[" hello"," \"' world", "! "]$$ AS VARCHAR[]); ---- -[" hello", " ' world", "! "] +[ hello, "' world, ! ] query I SELECT CAST('[ hello , world , ! ]' AS VARCHAR[]); @@ -259,9 +259,9 @@ SELECT CAST('[ hello , world , ! ]' AS VARCHAR[]); [hello, world, !] query I -SELECT CAST('[ [ " hello"] ,[" world" ],[ "! " ] ]' AS VARCHAR[][]); +SELECT CAST($$[ [ " hello"] ,[" world" ],[ "! " ] ]$$ AS VARCHAR[][]); ---- -[[" hello"], [" world"], ["! "]] +[[ hello], [ world], [! ]] # Empty list @@ -488,7 +488,7 @@ statement ok CREATE TABLE assorted_lists(col1 INT[], col2 VARCHAR[], col3 DATE[]); statement ok -COPY (SELECT [8,7,6], '[hello, Duck''DB]', '[2022-12-2, 1929-01-25]') TO '__TEST_DIR__/assorted_lists.csv' (Header 0); +COPY (SELECT [8,7,6], $$[hello, Duck\\'DB]$$, '[2022-12-2, 1929-01-25]') TO '__TEST_DIR__/assorted_lists.csv' (Header 0); statement ok COPY assorted_lists FROM '__TEST_DIR__/assorted_lists.csv'; @@ -507,19 +507,34 @@ select '[{"bar":"\""}]'::VARCHAR[]; ---- [{"bar":"\""}] -# escaped '\', does not count as an escape for " statement error select '[{"bar":"\\""}]'::VARCHAR[]; ---- +# Unescaped doublequote ends the quote early, leaving an uneven amount of `"`, causing an error +statement error +select '[{"bar":"\\""}]'::STRUCT(bar VARCHAR)[]; +---- +can't be cast to the destination type STRUCT(bar VARCHAR)[] + # uneven amount of escapes does escape the " query I -select '[{"bar":"\\\""}]'::VARCHAR[]; +select '[{"bar":"\\\""}]'::STRUCT(bar VARCHAR)[]; ---- -[{"bar":"\\\""}] +[{'bar': \"}] # all are escaped except for the last one query I -select '[{"bar":"\"\"\\\"\"\"\\"}]'::VARCHAR[]; +select '[{"bar":"\"\"\\\"\"\"\\"}]'::STRUCT(bar VARCHAR)[]; +---- +[{'bar': ""\"""\}] + +query III +select $$[\ \\abc\\ \ , def, ghi]$$::VARCHAR[] a, a[1], len(a[1]) +---- +[\ \\abc\\ \, def, ghi] \ \\abc\\ \ 12 + +query III +select $$["\ \\abc\\ \ ", def, ghi]$$::VARCHAR[] a, a[1], len(a[1]) ---- -[{"bar":"\"\"\\\"\"\"\\"}] +[ \abc\ , def, ghi] \abc\ 9 diff --git a/test/sql/cast/string_to_list_escapes.test b/test/sql/cast/string_to_list_escapes.test new file mode 100644 index 000000000000..29f201825a00 --- /dev/null +++ b/test/sql/cast/string_to_list_escapes.test @@ -0,0 +1,243 @@ +# name: test/sql/cast/string_to_list_escapes.test +# group: [cast] + +query I +SELECT $$[hello, world]$$::VARCHAR[]; +---- +[hello, world] + +query I +SELECT $$["hello\ world", world]$$::VARCHAR[]; +---- +[hello world, world] + +query I +SELECT $$[hello\ world, world]$$::VARCHAR[]; +---- +[hello\ world, world] + +query I +SELECT $$[hello\,world, test]$$::VARCHAR[]; +---- +[hello\, world, test] + +query I +SELECT $$["hello\,world", test]$$::VARCHAR[]; +---- +[hello,world, test] + +query I +SELECT $$["hello\,", test]$$::VARCHAR[]; +---- +[hello,, test] + +query I +SELECT $$[hello\,, test]$$::VARCHAR[]; +---- +[hello\, , test] + +query I +SELECT $$[hello\"quoted\"text, more]$$::VARCHAR[]; +---- +[hello"quoted"text, more] + +query I +SELECT $$[escaped\\backslash, test]$$::VARCHAR[]; +---- +[escaped\\backslash, test] + +query I +SELECT $$["escaped\\backslash", test]$$::VARCHAR[]; +---- +[escaped\backslash, test] + +query I +SELECT $$[nested[brackets], test]$$::VARCHAR[]; +---- +[nested[brackets], test] + +statement error +SELECT $$[nested[bracket, test]$$::VARCHAR[]; +---- +can't be cast to the destination type VARCHAR[] + +query I +SELECT $$[nested"["bracket, test]$$::VARCHAR[]; +---- +[nested[bracket, test] + +query I +SELECT $$[quote\'in\'string, test]$$::VARCHAR[]; +---- +[quote'in'string, test] + +query I +SELECT $$[mix\ of\ special\,chars]$$::VARCHAR[]; +---- +[mix\ of\ special\, chars] + +query I +SELECT $$["mix\ of\ special\,chars"]$$::VARCHAR[]; +---- +[mix of special,chars] + +query I +SELECT $$["ends with space ", "trailing space "]$$::VARCHAR[]; +---- +[ends with space , trailing space ] + +query I +SELECT $$["ends with comma,", "another,"]$$::VARCHAR[]; +---- +[ends with comma,, another,] + +query I +SELECT $$["quote at end\"", "\""]$$::VARCHAR[]; +---- +[quote at end", "] + +query I +SELECT $$["ends with bracket]", "[bracket"]$$::VARCHAR[]; +---- +[ends with bracket], [bracket] + +query I +SELECT $$["backslash at end\\", "\\"]$$::VARCHAR[]; +---- +[backslash at end\, \] + +query I +SELECT $$[" space at start", " leading space"]$$::VARCHAR[]; +---- +[ space at start, leading space] + +query I +SELECT $$[",comma at start", ",leading comma"]$$::VARCHAR[]; +---- +[,comma at start, ,leading comma] + +query I +SELECT $$["\"quote at start", "\"leading quote"]$$::VARCHAR[]; +---- +["quote at start, "leading quote] + +query I +SELECT $$["[bracket at start", "[leading bracket"]$$::VARCHAR[]; +---- +[[bracket at start, [leading bracket] + +query I +SELECT $$["\\backslash at start", "\\leading backslash"]$$::VARCHAR[]; +---- +[\backslash at start, \leading backslash] + +query I +SELECT $$[" space at start and end ", " leading and trailing space "]$$::VARCHAR[]; +---- +[ space at start and end , leading and trailing space ] + +query I +SELECT $$[",comma at start and end,", ",leading and trailing comma,"]$$::VARCHAR[]; +---- +[,comma at start and end,, ,leading and trailing comma,] + +query I +SELECT $$["\"quote at start and end\"", "\"leading and trailing quote\""]$$::VARCHAR[]; +---- +["quote at start and end", "leading and trailing quote"] + +query I +SELECT $$["[bracket at start and end]", "[leading and trailing bracket]"]$$::VARCHAR[]; +---- +[[bracket at start and end], [leading and trailing bracket]] + +query I +SELECT $$["\\backslash at start and end\\", "\\leading and trailing backslash\\"]$$::VARCHAR[]; +---- +[\backslash at start and end\, \leading and trailing backslash\] + + +query I +SELECT $$[" mix, of special\ characters " , "[various] \"combinations\" "]$$::VARCHAR[]; +---- +[ mix, of special characters , [various] "combinations" ] + +query I +SELECT $$[", starts and ends with ,", "[brackets] and ,commas,"]$$::VARCHAR[]; +---- +[, starts and ends with ,, [brackets] and ,commas,] + +query I +SELECT $$["\"quotes\" and \ spaces ", "\ leading and trailing \ "]$$::VARCHAR[]; +---- +["quotes" and spaces , leading and trailing ] + +query I +SELECT $$["[complex\ combination, of\" special]", "\\all cases covered\\"]$$::VARCHAR[]; +---- +[[complex combination, of" special], \all cases covered\] + +query I +SELECT $$["hello, world"]$$::VARCHAR[]; +---- +[hello, world] + +statement error +SELECT $$["missing quote]]$$::VARCHAR[]; -- Mismatched quotes +---- +can't be cast to the destination type + +statement error +SELECT $$["backslash at end\"]$$::VARCHAR[]; -- Improper escaping +---- +can't be cast to the destination type + +statement error +SELECT $$[unescaped[bracket]$$::VARCHAR[]; -- Unescaped bracket +---- +can't be cast to the destination type + +statement error +SELECT $$[unterminated string]"]$$::VARCHAR[]; +---- +can't be cast to the destination type + +query I +SELECT $$[]$$::VARCHAR[]; -- Empty list +---- +[] + +query I +SELECT $$[""]$$::VARCHAR[]; -- List with empty string +---- +[] + +query I +SELECT $$[" "]$$::VARCHAR[]; -- List with whitespace string +---- +[ ] + +query I +SELECT $$["\\"]$$::VARCHAR[]; -- List with only a backslash +---- +[\] + +query I +SELECT $$["\""]$$::VARCHAR[]; -- List with only a quote +---- +["] + +query I +SELECT $$[\,]$$::VARCHAR[]; -- List with only a comma (not quoted) +---- +[\, ] + +query I +SELECT $$["\,"]$$::VARCHAR[]; -- List with only a comma +---- +[,] + +query I +SELECT $$[","]$$::VARCHAR[]; -- List with only a comma +---- +[,] diff --git a/test/sql/cast/string_to_map_cast.test_slow b/test/sql/cast/string_to_map_cast.test_slow index 3a5cd142860e..2c896fdc61fa 100644 --- a/test/sql/cast/string_to_map_cast.test_slow +++ b/test/sql/cast/string_to_map_cast.test_slow @@ -76,12 +76,12 @@ SELECT CAST('{''hello''=2, ''world''=50, ''!''=12}' AS MAP(VARCHAR, INT)); {hello=2, world=50, !=12} query I -SELECT CAST('{''''hello''''=hello, ''''world''''=world, ''''!''''=!}' AS MAP(VARCHAR, VARCHAR)); +SELECT CAST($${\'hello\'=hello, \'world\'=world, \'!\'=!}$$ AS MAP(VARCHAR, VARCHAR)); ---- {'hello'=hello, 'world'=world, '!'=!} query I -SELECT CAST('{[[''🦆, 🦆, 🦆'']]=100, [[duck, db, ''🦆'']]=101}' AS MAP(VARCHAR[][], INT)); +SELECT CAST($${[[\'🦆, 🦆, 🦆\']]=100, [[duck, db, \'🦆\']]=101}$$ AS MAP(VARCHAR[][], INT)); ---- {[['🦆, 🦆, 🦆']]=100, [[duck, db, '🦆']]=101} @@ -114,8 +114,8 @@ SELECT CAST('{ [12, 13,14]=val, [ 8, 9 ] =val, [ 4 ]=v {[12, 13, 14]=val, [8, 9]=val, [4]=val} query I -SELECT CAST(' { { a:[2, 3], b: Duckster }= {50.0 =50}, {a : [9,1,4], b:Duck } - ={ 1 = 0} }' AS MAP(STRUCT(a INT[], b VARCHAR), MAP(INT, DOUBLE))); +SELECT CAST($$ { { a:[2, 3], b: Duckster }= {50.0 =50}, {a : [9,1,4], b:Duck } + ={ 1 = 0} }$$ AS MAP(STRUCT(a INT[], b VARCHAR), MAP(INT, DOUBLE))); ---- {{'a': [2, 3], 'b': Duckster}={50=50.0}, {'a': [9, 1, 4], 'b': Duck}={1=0.0}} diff --git a/test/sql/cast/string_to_map_escapes.test b/test/sql/cast/string_to_map_escapes.test new file mode 100644 index 000000000000..4d3425d1cb59 --- /dev/null +++ b/test/sql/cast/string_to_map_escapes.test @@ -0,0 +1,147 @@ +# name: test/sql/cast/string_to_map_escapes.test +# group: [cast] + +# Valid: key and value with escaped space +query I +SELECT $${"key\ with\ space" = "value\ with\ space"}$$::MAP(VARCHAR, VARCHAR); +---- +{key with space=value with space} + +# Valid: key with escaped quote and value with escaped quote +query I +SELECT $${\"key\" = \"value\"}$$::MAP(VARCHAR, VARCHAR); +---- +{"key"="value"} + +# Valid: key with escaped backslash, value with escaped backslash +query I +SELECT $${"key\ with\ backslash" = "value\ with\ backslash"}$$::MAP(VARCHAR, VARCHAR); +---- +{key with backslash=value with backslash} + +# Valid: key with escaped comma, value with escaped comma +query I +SELECT $${"key\ with\, comma" = "value\ with\, comma"}$$::MAP(VARCHAR, VARCHAR); +---- +{key with, comma=value with, comma} + +# Valid: key and value with escaped colon +query I +SELECT $${"key\ with\ colon\:" = "value\ with\ colon\:"}$$::MAP(VARCHAR, VARCHAR); +---- +{key with colon:=value with colon:} + +## FIXME: not sure what to do here, maybe we shouldn't "respect scopes" if the child type is not nested +## Valid: key and value with parentheses +#query I +#SELECT $${key\ (with\ parens) = value\ (with\ parens)}$$::MAP(VARCHAR, VARCHAR); +#---- +#{key (with parens)=value (with parens)} + +# Valid: key contains unescaped space +query I +SELECT $${key with space = value with space}$$::MAP(VARCHAR, VARCHAR); +---- +{key with space=value with space} + +# Invalid: key input contains quotes +query I +SELECT $${key"with"quote = value}$$::MAP(VARCHAR, VARCHAR); +---- +{keywithquote=value} + +# Valid: value input contains quotes +query I +SELECT $${key = value"with"quote}$$::MAP(VARCHAR, VARCHAR); +---- +{key=valuewithquote} + +# Valid: key contains unescaped comma +query I +SELECT $${key,with,comma = value}$$::MAP(VARCHAR, VARCHAR); +---- +{key,with,comma=value} + +# Invalid: value contains unescaped comma +statement error +SELECT $${key = value,with,comma}$$::MAP(VARCHAR, VARCHAR); +---- +can't be cast to the destination type MAP + +# Valid: key contains unescaped curly bracket +query I +SELECT $${key{with}bracket = value}$$::MAP(VARCHAR, VARCHAR); +---- +{key{with}bracket=value} + +# Invalid: value contains unescaped curly bracket +query I +SELECT $${key = value{with}bracket}$$::MAP(VARCHAR, VARCHAR); +---- +{key=value{with}bracket} + +# Valid: key contains useless backslashes +query I +SELECT $${"key\with\backslash" = value}$$::MAP(VARCHAR, VARCHAR); +---- +{keywithbackslash=value} + +# Valid: value contains useless backslashes +query I +SELECT $${key = "value\with\backslash"}$$::MAP(VARCHAR, VARCHAR); +---- +{key=valuewithbackslash} + +# Valid: key/value contains unescaped equal sign +query II +SELECT $${key=with=equals = value}$$::MAP(VARCHAR, VARCHAR) a, a['key']; +---- +{key=with=equals = value} with=equals = value + +# Valid: key/value contains unescaped equal sign +query II +SELECT $${"key\=with" = equals = value}$$::MAP(VARCHAR, VARCHAR) a, a['key=with']; +---- +{key=with=equals = value} equals = value + +# Valid: key/value contains unescaped equal sign +query II +SELECT $${"key\=with\=equals" = value}$$::MAP(VARCHAR, VARCHAR) a, a['key=with=equals']; +---- +{key=with=equals=value} value + +# Edge Case: Empty MAP with no keys/values +query I +SELECT $${}$$::MAP(VARCHAR, VARCHAR); +---- +{} + +# Valid: MAP with empty key and value +query I +SELECT $${=}$$::MAP(VARCHAR, VARCHAR); +---- +{=} + +# Edge Case: MAP with special characters only (escaped) +statement error +SELECT $${\{escaped\brace\} = \}escaped\brace\\}$$::MAP(VARCHAR, VARCHAR); +---- +can't be cast to the destination type MAP(VARCHAR, VARCHAR) + +# Edge Case: MAP with special characters only (escaped) +query I +SELECT $${"\{escaped\brace\}" = "\}escaped\brace\\"}$$::MAP(VARCHAR, VARCHAR); +---- +{{escapedbrace}=}escapedbrace\} + +# Edge Case: MAP with only a key and no value +query I +SELECT $${key=}$$::MAP(VARCHAR, VARCHAR); +---- +{key=} + +# Valid: MAP with an empty key +query I +SELECT $${=value}$$::MAP(VARCHAR, VARCHAR); +---- +{=value} diff --git a/test/sql/cast/string_to_struct_cast.test b/test/sql/cast/string_to_struct_cast.test index f4cae07ae960..0ae4d26b5d6a 100644 --- a/test/sql/cast/string_to_struct_cast.test +++ b/test/sql/cast/string_to_struct_cast.test @@ -205,7 +205,7 @@ SELECT ' { } '::STRUCT(a INT, b DATE); statement error SELECT '{ key_A: 2, key_B: {key_C: hello world } X }'::STRUCT(key_A INT, key_B STRUCT(key_C VARCHAR)); ---- - +can't be cast to the destination type STRUCT(key_C VARCHAR) # NULL values @@ -319,50 +319,62 @@ NULL statement error SELECT CAST('[{a:3}]' AS STRUCT(a INT)); ---- +can't be cast to the destination type STRUCT(a INTEGER) statement error SELECT CAST('Hello World' AS STRUCT(a VARCHAR)); ---- +can't be cast to the destination type STRUCT(a VARCHAR) statement error SELECT CAST('{a: 3}}' AS STRUCT(a INT)); ---- +can't be cast to the destination type STRUCT(a INTEGER) statement error SELECT CAST('{a: 3, b:{c: 8}}}' AS STRUCT(a INT, b STRUCT(c INT))); ---- +can't be cast to the destination type STRUCT(a INTEGER, b STRUCT(c INTEGER)) statement error SELECT CAST('{{a: 3}' AS STRUCT(a INT)); ---- +can't be cast to the destination type STRUCT(a INTEGER) statement error SELECT CAST('{a:3}, {b:1}' AS STRUCT(a INT, b INT)); ---- +can't be cast to the destination type STRUCT(a INTEGER, b INTEGER) statement error SELECT CAST('{a:{a:3}, b:{{b:1}}}' AS STRUCT(a STRUCT(a INT), b STRUCT(b INT))); ---- +can't be cast to the destination type STRUCT(b INTEGER) statement error SELECT CAST('{a: 3 1}' AS STRUCT(a INT)); ---- +Could not convert string '3 1' to INT32 statement error SELECT CAST('{a:3,, b:1}' AS STRUCT(a INT, b INT)); ---- +can't be cast to the destination type STRUCT(a INTEGER, b INTEGER) statement error SELECT CAST('}{a:5}' AS STRUCT(a INT)); ---- +can't be cast to the destination type STRUCT(a INTEGER) statement error SELECT CAST('{a:{b:{d: 800}, {c: "Duck"}}}' AS STRUCT(a STRUCT(b STRUCT(d INT), c STRUCT(c VARCHAR)))); ---- +can't be cast to the destination type STRUCT(b STRUCT(d INTEGER), c STRUCT(c VARCHAR)) statement error SELECT CAST('{[{]}}' AS STRUCT(a VARCHAR[])); ---- +can't be cast to the destination type STRUCT(a VARCHAR[]) diff --git a/test/sql/cast/string_to_struct_escapes.test b/test/sql/cast/string_to_struct_escapes.test new file mode 100644 index 000000000000..041386e77e74 --- /dev/null +++ b/test/sql/cast/string_to_struct_escapes.test @@ -0,0 +1,252 @@ +# name: test/sql/cast/string_to_struct_escapes.test +# group: [cast] + +query I +SELECT $${name: value, age: 30}$$::STRUCT(name VARCHAR, age INT); +---- +{'name': value, 'age': 30} + +query I +SELECT $${name: John, city: "New York"}$$::STRUCT(name VARCHAR, city VARCHAR); +---- +{'name': John, 'city': New York} + +query I +SELECT $${quote_at_start: "\"test\"", age: 30}$$::STRUCT(quote_at_start VARCHAR, age INT); +---- +{'quote_at_start': "test", 'age': 30} + +query I +SELECT $${user_name: Alice, status: active}$$::STRUCT(user_name VARCHAR, status VARCHAR); +---- +{'user_name': Alice, 'status': active} + +query I +SELECT $${special_characters: "comma, semicolon; and backslash\\", age: 30}$$::STRUCT(special_characters VARCHAR, age INT); +---- +{'special_characters': comma, semicolon; and backslash\, 'age': 30} + +query I +SELECT $${a: 10, b: "hello world"}$$::STRUCT(a INT, b VARCHAR); +---- +{'a': 10, 'b': hello world} + +query I +SELECT $${first_name: "John", last_name: "Doe", age: 28}$$::STRUCT(first_name VARCHAR, last_name VARCHAR, age INT); +---- +{'first_name': John, 'last_name': Doe, 'age': 28} + +query I +SELECT $${first name: John, age: 30}$$::STRUCT("first name" VARCHAR, age INT); +---- +{'first name': John, 'age': 30} + +# Invalid: Value contains a quote that isn't escaped +statement error +SELECT $${name: "John "Doe"}$$::STRUCT(name VARCHAR); +---- +can't be cast to the destination type + +# second key has no ending character (:) +statement error +SELECT $${name: John, age, 30}$$::STRUCT(name VARCHAR, age INT); +---- +can't be cast to the destination type + +# Name is free to contain `,`, only `:` is problematic +query I +SELECT $${user,name: Alice, age: 30}$$::STRUCT("user,name" VARCHAR, age INT); +---- +{'user,name': Alice, 'age': 30} + +# Invalid: Contains an unescaped closing bracket +statement error +SELECT $${name: Alice, age: 30})$$::STRUCT(name VARCHAR, age INT); +---- +can't be cast to the destination type + +# Invalid: Name contains a backslash +statement error +SELECT $${"backslash\name": value}$$::STRUCT("backslash\name" VARCHAR); +---- +can't be cast to the destination type + +# Valid: Name contains a backslash outside of quotes, interpreted as literal +query I +SELECT $${backslash\name: value}$$::STRUCT("backslash\name" VARCHAR); +---- +{'backslash\name': value} + +# first `:` is not escaped, won't match the "name:" struct key +statement error +SELECT $${name: test, value: 30}$$::STRUCT("name:" VARCHAR, value INT); +---- +can't be cast to the destination type + +# Invalid: Name can contain escaped `:`, but only in quotes +statement error +SELECT $${name\:: test, value: 30}$$::STRUCT("name:" VARCHAR, value INT); +---- +can't be cast to the destination type STRUCT("name:" VARCHAR, "value" INTEGER) + +# Valid: Name can contain escaped `:` in quotes +query I +SELECT $${"name\:": test, value: 30}$$::STRUCT("name:" VARCHAR, value INT); +---- +{'name:': test, 'value': 30} + +# Name consists of `{}`, not a problem, with this syntax we expect a name, which is a plain string +# Only reserved character there is `:` (and quotes, and backslash of course) +query I +SELECT $${{name}: John, age: 3}$$::STRUCT("{name}" VARCHAR, age INT); +---- +{'{name}': John, 'age': 3} + +# Name has `{` which normally starts a bracket that disables interpreting escape characters +query I +SELECT $${{\"name\"}: John, age: 3}$$::STRUCT("{""name""}" VARCHAR, age INT); +---- +{'{"name"}': John, 'age': 3} + +# Invalid: Unterminated string value +statement error +SELECT $${name: "John, age: 30}$$::STRUCT(name VARCHAR, age INT); +---- +can't be cast to the destination type + +query I +SELECT $${}$$::STRUCT(name VARCHAR, age INT); +---- +{'name': NULL, 'age': NULL} + +# STRUCT with whitespace around colon (escaped) +query I +SELECT $${name : John, age : 30}$$::STRUCT(name VARCHAR, age INT); +---- +{'name': John, 'age': 30} + +# STRUCT with escaped backslash in value +query I +SELECT $${path: "C:\\Users\\John"}$$::STRUCT(path VARCHAR); +---- +{'path': C:\Users\John} + +# STRUCT with special characters in value, properly escaped +query I +SELECT $${description: "Special characters: \\, \", ;, (, )"}$$::STRUCT(description VARCHAR); +---- +{'description': Special characters: \, ", ;, (, )} + +statement error +SELECT $${first\ name: "John", age: 30}$$::STRUCT("first name" VARCHAR, age INT); +---- +can't be cast to the destination type STRUCT("first name" VARCHAR, age INTEGER) + +# Valid: Name with escaped space +query I +SELECT $${"first\ name": "John", age: 30}$$::STRUCT("first name" VARCHAR, age INT); +---- +{'first name': John, 'age': 30} + +# Valid: Name with escaped quote +query I +SELECT $${\"quote at start\": "value", age: 30}$$::STRUCT("""quote at start""" VARCHAR, age INT); +---- +{'"quote at start"': value, 'age': 30} + +statement error +SELECT $${backslash\\name: "John Doe", age: 30}$$::STRUCT("backslash\name" VARCHAR, age INT); +---- +can't be cast to the destination type STRUCT("backslash\name" VARCHAR, age INTEGER) + +# Valid: Name with escaped backslash +query I +SELECT $${"backslash\\name": "John Doe", age: 30}$$::STRUCT("backslash\name" VARCHAR, age INT); +---- +{'backslash\name': John Doe, 'age': 30} + +statement error +SELECT $${user\,name: "Alice", age: 25}$$::STRUCT("user,name" VARCHAR, age INT); +---- +can't be cast to the destination type STRUCT("user,name" VARCHAR, age INTEGER) + +# Valid: Name with escaped comma +query I +SELECT $${"user\,name": "Alice", age: 25}$$::STRUCT("user,name" VARCHAR, age INT); +---- +{'user,name': Alice, 'age': 25} + +# Valid: Name with comma +query I +SELECT $${"user,name": "Alice", age: 25}$$::STRUCT("user,name" VARCHAR, age INT); +---- +{'user,name': Alice, 'age': 25} + +statement error +SELECT $${user\(name\): "Alice", status: "active"}$$::STRUCT("user(name)" VARCHAR, status VARCHAR); +---- +can't be cast to the destination type STRUCT("user(name)" VARCHAR, status VARCHAR) + +# Valid: Name with escaped parenthesis +query I +SELECT $${"user\(name\)": "Alice", status: "active"}$$::STRUCT("user(name)" VARCHAR, status VARCHAR); +---- +{'user(name)': Alice, 'status': active} + +# Valid: Name with unescaped parenthesis +query I +SELECT $${user(name): "Alice", status: "active"}$$::STRUCT("user(name)" VARCHAR, status VARCHAR); +---- +{'user(name)': Alice, 'status': active} + +# Valid: Name with escaped space at end +query I +SELECT $${"user\ name\ ": "Alice", "age ": 25}$$::STRUCT("user name " VARCHAR, "age " INT); +---- +{'user name ': Alice, 'age ': 25} + +statement error +SELECT $${user\ name\ : "Alice", age\ : 25}$$::STRUCT("user name " VARCHAR, "age " INT); +---- +can't be cast to the destination type STRUCT("user name " VARCHAR, "age " INTEGER) + +# Invalid: Name contains unescaped quote +statement error +SELECT $${"quote"start": "value", age: 30}$$::STRUCT("quote""start" VARCHAR, age INT); +---- +can't be cast to the destination type + +# Valid: Name contains unescaped backslash outside of quotes +query I +SELECT $${backslash\name: "John", age: 30}$$::STRUCT("backslash\name" VARCHAR, age INT); +---- +{'backslash\name': John, 'age': 30} + +# Valid: Name contains (unescaped) opening parenthesis +query I +SELECT $${user(name: "Alice", age: 25}$$::STRUCT("user(name" VARCHAR, age INT); +---- +{'user(name': Alice, 'age': 25} + +# Name is single double quote +query I +SELECT $${\": "value", age: 30}$$::STRUCT("""" VARCHAR, age INTEGER) +---- +{'"': value, 'age': 30} + +statement error +SELECT $${\\: "escaped", age: 30}$$::STRUCT("\" VARCHAR, age INT); +---- +can't be cast to the destination type STRUCT("\" VARCHAR, age INTEGER) + +# Name with only a special character (escaped) +query I +SELECT $${"\\": "escaped", age: 30}$$::STRUCT("\" VARCHAR, age INT); +---- +{'\': escaped, 'age': 30} + +# Name with only a special character (not escaped) +query I +SELECT $${@: "value", age: 30}$$::STRUCT("@" VARCHAR, age INT); +---- +{'@': value, 'age': 30} diff --git a/test/sql/cast/string_to_unnamed_struct.test b/test/sql/cast/string_to_unnamed_struct.test new file mode 100644 index 000000000000..d51c01dcee58 --- /dev/null +++ b/test/sql/cast/string_to_unnamed_struct.test @@ -0,0 +1,78 @@ +# name: test/sql/cast/string_to_unnamed_struct.test +# group: [cast] + +# Basic single value struct +query I +select [row('a'), $$(abc)$$] +---- +[(a), (abc)] + +# Multiple values +query I +select [row('a', 'b', 'c'), $$(abc, def, ghi)$$] +---- +[(a, b, c), (abc, def, ghi)] + +# Empty unnamed struct +query I +select [row('a'),$$()$$] +---- +[(a), (NULL)] + +# Empty string in unnamed struct +query I +select [row('a'),$$('')$$] +---- +[(a), ()] + +# Nested regular struct inside unnamed struct +query I +select [row({'amount': 21}), $$({'amount': 42})$$] +---- +[({'amount': 21}), ({'amount': 42})] + +# Nested unnamed struct inside unnamed struct +query I +select [row(row(21)), $$((42))$$] +---- +[((21)), ((42))] + +# Nested unnamed struct AND regular struct inside unnamed struct +query I +select [row(row(21), {'amount': 42}), $$((42), {amount: 21})$$] +---- +[((21), {'amount': 42}), ((42), {'amount': 21})] + +# List inside unnamed struct +query I +select [row([7,8,9], [10,11,12]), $$([1,2,3], [4,5,6])$$] +---- +[([7, 8, 9], [10, 11, 12]), ([1, 2, 3], [4, 5, 6])] + +statement error +select [row([4,5,6]), $$([1,2,3],)$$] +---- +can't be cast to the destination type STRUCT(INTEGER[]) + +# Empty string in the second child of the unnamed struct +query I +select [row([4,5,6], 'abc'), $$([1,2,3],)$$] +---- +[([4, 5, 6], abc), ([1, 2, 3], )] + +# Empty string in the second child of a named struct +query I +select [{'a': [4,5,6], 'b': 'abc'}, $${'a': [1,2,3],'b':}$$] +---- +[{'a': [4, 5, 6], 'b': abc}, {'a': [1, 2, 3], 'b': }] + +query I +select [ + [ + row(row(' test ')), + {'a': {'inner': '\ test \'}} + ], + $$[((" test ")), {'a': (\\ test \\)}]$$ +] +---- +[[{'a': {'inner': test }}, {'a': {'inner': \ test \}}], [{'a': {'inner': test }}, {'a': {'inner': \\ test \\}}]] diff --git a/test/sql/copy/csv/afl/fuzz_20250211_crash.test b/test/sql/copy/csv/afl/fuzz_20250211_crash.test new file mode 100644 index 000000000000..7a10d16a002d --- /dev/null +++ b/test/sql/copy/csv/afl/fuzz_20250211_crash.test @@ -0,0 +1,10 @@ +# name: test/sql/copy/csv/afl/fuzz_20250211_crash.test +# description: fuzzer generated csv files - should not raise internal exception (by failed assertion). +# group: [afl] + +statement ok +PRAGMA enable_verification + +statement maybe +FROM read_csv('data/csv/afl/20250211_csv_fuzz_crash/case_53.csv', buffer_size=42); +---- diff --git a/test/sql/copy/csv/afl/test_fuzz_4172.test b/test/sql/copy/csv/afl/test_fuzz_4172.test new file mode 100644 index 000000000000..e22e66604e57 --- /dev/null +++ b/test/sql/copy/csv/afl/test_fuzz_4172.test @@ -0,0 +1,10 @@ +# name: test/sql/copy/csv/afl/test_fuzz_4172.test +# description: fuzzer generated csv files - should not raise internal exception (by failed assertion). +# group: [afl] + +statement ok +PRAGMA enable_verification + +statement maybe +FROM read_csv('data/csv/afl/4172/case_4.csv', ignore_errors=true, buffer_size=1, store_rejects=false); +---- diff --git a/test/sql/copy/csv/maximum_line_size.test_slow b/test/sql/copy/csv/maximum_line_size.test_slow index db5672073fbf..1ea62d7e6513 100644 --- a/test/sql/copy/csv/maximum_line_size.test_slow +++ b/test/sql/copy/csv/maximum_line_size.test_slow @@ -39,4 +39,4 @@ Be sure that the maximum line size is set to an appropriate value statement error select * from read_csv_auto('data/csv/issue_8320_3.csv.gz', max_line_size = 2097152, buffer_size = 10); ---- -BUFFER_SIZE option was set to 10, while MAX_LINE_SIZE was set to 2097152. BUFFER_SIZE must have always be set to value bigger than MAX_LINE_SIZE \ No newline at end of file +Buffer Size of 10 must be a higher value than the maximum line size 2097152 \ No newline at end of file diff --git a/test/sql/copy/csv/parallel/csv_parallel_buffer_size.test b/test/sql/copy/csv/parallel/csv_parallel_buffer_size.test index 466758becc6f..04f78983c3cb 100644 --- a/test/sql/copy/csv/parallel/csv_parallel_buffer_size.test +++ b/test/sql/copy/csv/parallel/csv_parallel_buffer_size.test @@ -22,7 +22,7 @@ SELECT sum(a) FROM read_csv('data/csv/test/multi_column_integer_rn.csv', COLUMN 111111111 query IIII -select * from read_csv('data/csv/test/multi_column_string.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=25) +select * from read_csv('data/csv/test/multi_column_string.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=30) ---- 1 6370 371 p1 10 214 465 p2 @@ -35,7 +35,7 @@ select * from read_csv('data/csv/test/multi_column_string.csv', COLUMNS=STRUCT_ 100000000 15519 785 p9 query IIII -select * from read_csv('data/csv/test/multi_column_string_rn.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=25) +select * from read_csv('data/csv/test/multi_column_string_rn.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=27) ---- 1 6370 371 p1 10 214 465 p2 @@ -53,7 +53,7 @@ SELECT sum(a) FROM read_csv('data/csv/test/new_line_string_rn.csv', COLUMNS=STR 111 query I -SELECT sum(a) FROM read_csv('data/csv/test/new_line_string_rn.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=80) +SELECT sum(a) FROM read_csv('data/csv/test/new_line_string_rn.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=100) ---- 111 @@ -64,7 +64,7 @@ SELECT sum(a) FROM read_csv('data/csv/test/new_line_string_rn_exc.csv', COLUMNS 111 query I -SELECT sum(a) FROM read_csv('data/csv/test/new_line_string_rn_exc.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=60) +SELECT sum(a) FROM read_csv('data/csv/test/new_line_string_rn_exc.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=80) ---- 111 @@ -75,6 +75,6 @@ SELECT sum(a) FROM read_csv('data/csv/test/new_line_string.csv', COLUMNS=STRUCT 111 query I -SELECT sum(a) FROM read_csv('data/csv/test/new_line_string.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), auto_detect='true', delim = '|', buffer_size=60) +SELECT sum(a) FROM read_csv('data/csv/test/new_line_string.csv', COLUMNS=STRUCT_PACK(a := 'INTEGER', b := 'INTEGER', c := 'INTEGER', d := 'VARCHAR'), quote ='"', escape ='"', comment = '', auto_detect='true', delim = '|', buffer_size=100, new_line = '\r\n') ---- 111 diff --git a/test/sql/copy/csv/parallel/csv_parallel_new_line.test_slow b/test/sql/copy/csv/parallel/csv_parallel_new_line.test_slow index b9654da5d729..f9fb5c45385f 100644 --- a/test/sql/copy/csv/parallel/csv_parallel_new_line.test_slow +++ b/test/sql/copy/csv/parallel/csv_parallel_new_line.test_slow @@ -9,7 +9,7 @@ PRAGMA verify_parallelism statement ok PRAGMA enable_verification -loop i 25 100 +loop i 27 100 # Test read_csv auto with \n diff --git a/test/sql/copy/csv/relaxed_quotes.test b/test/sql/copy/csv/relaxed_quotes.test index f8c6e8012c88..6bdfa8ede56b 100644 --- a/test/sql/copy/csv/relaxed_quotes.test +++ b/test/sql/copy/csv/relaxed_quotes.test @@ -78,12 +78,7 @@ statement ok drop table t; statement error -create table t as from read_csv('data/csv/unescaped_quotes/unescaped_quote_new_line_rn.csv', strict_mode=false, buffer_size = 20, header = 0) ----- - - -statement error -create table t as from read_csv('data/csv/unescaped_quotes/unescaped_quote_new_line_rn.csv', strict_mode=false, buffer_size = 20, header = 0) +create table t as from read_csv('data/csv/unescaped_quotes/unescaped_quote_new_line_rn.csv', strict_mode=false, buffer_size = 20, header = 0, delim = ';') ---- statement ok diff --git a/test/sql/copy/csv/test_union_by_name.test b/test/sql/copy/csv/test_union_by_name.test index 6d9032516759..a531f9eddda0 100644 --- a/test/sql/copy/csv/test_union_by_name.test +++ b/test/sql/copy/csv/test_union_by_name.test @@ -67,6 +67,25 @@ ORDER BY a; 102 NULL 103 9223372036854775807 NULL NULL +query IIII +SELECT a, b, c, replace(replace(filename, '__TEST_DIR__', ''), '\', '/')[2:] +FROM read_csv_auto(['__TEST_DIR__/ubn1.csv', '__TEST_DIR__/ubn2.csv', '__TEST_DIR__/ubn3.csv'], UNION_BY_NAME=TRUE) +ORDER BY a; +---- +1 NULL NULL ubn1.csv +2 NULL NULL ubn1.csv +3 4 NULL ubn2.csv +5 6 NULL ubn2.csv +100 NULL 101 ubn3.csv +102 NULL 103 ubn3.csv +9223372036854775807 NULL NULL ubn1.csv + +query IIII +SELECT COUNT(a), COUNT(b), COUNT(c), COUNT(filename) +FROM read_csv_auto(['__TEST_DIR__/ubn1.csv', '__TEST_DIR__/ubn2.csv', '__TEST_DIR__/ubn3.csv'], UNION_BY_NAME=TRUE) +---- +7 2 2 7 + query TTT SELECT typeof(a), typeof(b), typeof(c) FROM read_csv_auto(['__TEST_DIR__/ubn1.csv', '__TEST_DIR__/ubn2.csv', '__TEST_DIR__/ubn3.csv'], UNION_BY_NAME=TRUE) diff --git a/test/sql/copy/csv/test_validator.test b/test/sql/copy/csv/test_validator.test index 66a444809520..e44d97b23014 100644 --- a/test/sql/copy/csv/test_validator.test +++ b/test/sql/copy/csv/test_validator.test @@ -52,7 +52,7 @@ statement ok FROM read_csv('data/csv/validator/quoted_new_value.csv', columns = {'band': 'varchar', 'album': 'varchar', 'release': 'varchar'}, quote = '''', delim = ';', header = 0) statement ok -FROM read_csv('data/csv/validator/quoted_new_value.csv', columns = {'band': 'varchar', 'album': 'varchar', 'release': 'varchar'}, quote = '''', delim = ';', header = 0, buffer_size = 46) +FROM read_csv('data/csv/validator/quoted_new_value.csv', columns = {'band': 'varchar', 'album': 'varchar', 'release': 'varchar'}, quote = '''', delim = ';', header = 0, buffer_size = 48) statement ok FROM read_csv('data/csv/validator/single_column_quoted_newline.csv', columns = {'Raffaella Carrà': 'varchar'}, quote = '"', buffer_size = 24) diff --git a/test/sql/copy/parquet/bloom_filters.test b/test/sql/copy/parquet/bloom_filters.test index 8ca061e9dbb7..05f166c23598 100644 --- a/test/sql/copy/parquet/bloom_filters.test +++ b/test/sql/copy/parquet/bloom_filters.test @@ -226,4 +226,33 @@ statement error copy (select (r1.range*10)::BIGINT r, from range(100) r1, range(100) order by r) to '__TEST_DIR__/bloom8.parquet' (format parquet, ROW_GROUP_SIZE 10000, dictionary_size_limit 1000, bloom_filter_false_positive_ratio 0); ---- -bloom_filter_false_positive_ratio must be greater than 0 \ No newline at end of file +bloom_filter_false_positive_ratio must be greater than 0 + +# some tests for string_dictionary_page_size_limit + +# no bloom filter, limit too low +statement ok +copy (select (r1.range*10)::VARCHAR r, +from range(100) r1, range(100) order by r) to '__TEST_DIR__/bloom9.parquet' (format parquet, ROW_GROUP_SIZE 10000, string_dictionary_page_size_limit 10); + +query III +select row_group_id, bloom_filter_offset IS NOT NULL, bloom_filter_length IS NOT NULL from parquet_metadata('__TEST_DIR__/bloom9.parquet') order by row_group_id; +---- +0 false false + +# big enough +statement ok +copy (select (r1.range*10)::VARCHAR r, +from range(100) r1, range(100) order by r) to '__TEST_DIR__/bloom9.parquet' (format parquet, ROW_GROUP_SIZE 10000, string_dictionary_page_size_limit 100000); + +query III +select row_group_id, bloom_filter_offset IS NOT NULL, bloom_filter_length IS NOT NULL from parquet_metadata('__TEST_DIR__/bloom9.parquet') order by row_group_id; +---- +0 true true + +# too big +statement error +copy (select (r1.range*10)::VARCHAR r, +from range(100) r1, range(100) order by r) to '__TEST_DIR__/bloom9.parquet' (format parquet, ROW_GROUP_SIZE 10000, string_dictionary_page_size_limit 4294967295); +---- +Binder Error diff --git a/test/sql/copy/parquet/parquet_filename_filter.test b/test/sql/copy/parquet/parquet_filename_filter.test index f236ed81de2e..a10611ef267b 100644 --- a/test/sql/copy/parquet/parquet_filename_filter.test +++ b/test/sql/copy/parquet/parquet_filename_filter.test @@ -4,6 +4,10 @@ require parquet +query III +select id, value as f, date from parquet_scan('data/parquet-testing/hive-partitioning/different_order/*/*/test.parquet', HIVE_PARTITIONING=1) where filename='value1'; +---- + # requires notwindows for windows-style path backslash reasons require notwindows @@ -49,15 +53,19 @@ select id, value, date, filename from parquet_scan('data/parquet-testing/hive-pa # Ensure we don't somehow endup mixing things up query III -select id, value as filename, date from parquet_scan('data/parquet-testing/hive-partitioning/different_order/*/*/test.parquet', HIVE_PARTITIONING=1) where filename='value2'; +select id, value as f, date from parquet_scan('data/parquet-testing/hive-partitioning/different_order/*/*/test.parquet', HIVE_PARTITIONING=1) where f='value2'; ---- 2 value2 2013-01-01 query III -select id, value as filename, date from parquet_scan('data/parquet-testing/hive-partitioning/different_order/*/*/test.parquet', HIVE_PARTITIONING=1) where filename='value1'; +select id, value as f, date from parquet_scan('data/parquet-testing/hive-partitioning/different_order/*/*/test.parquet', HIVE_PARTITIONING=1) where f='value1'; ---- 1 value1 2012-01-01 +query III +select id, value as f, date from parquet_scan('data/parquet-testing/hive-partitioning/different_order/*/*/test.parquet', HIVE_PARTITIONING=1) where filename='value1'; +---- + # These tests confirm that the ParquetScanStats will properly handle the pruned files list statement ok diff --git a/test/sql/copy/parquet/parquet_virtual_columns.test b/test/sql/copy/parquet/parquet_virtual_columns.test new file mode 100644 index 000000000000..03f5f4677811 --- /dev/null +++ b/test/sql/copy/parquet/parquet_virtual_columns.test @@ -0,0 +1,32 @@ +# name: test/sql/copy/parquet/parquet_virtual_columns.test +# description: Test virtual columns +# group: [parquet] + +require parquet + +# Filename without the filename option +statement ok +select filename from 'data/parquet-testing/glob/t1.parquet' + +query III +select i, j, replace(filename, '\', '/') from 'data/parquet-testing/glob*/t?.parquet' order by i; +---- +1 a data/parquet-testing/glob/t1.parquet +2 b data/parquet-testing/glob/t2.parquet +3 c data/parquet-testing/glob2/t1.parquet + +# not projected in * +query II +select * from 'data/parquet-testing/glob*/t?.parquet' order by i; +---- +1 a +2 b +3 c + +require notwindows + +# filename in filter +query III +select i, j, replace(filename, '\', '/') from 'data/parquet-testing/glob*/t?.parquet' where filename='data/parquet-testing/glob/t1.parquet' +---- +1 a data/parquet-testing/glob/t1.parquet diff --git a/test/sql/copy/parquet/writer/parquet_write_memory_usage.test b/test/sql/copy/parquet/writer/parquet_write_memory_usage.test index 29a19bfe4609..6bfc26d0fdf6 100644 --- a/test/sql/copy/parquet/writer/parquet_write_memory_usage.test +++ b/test/sql/copy/parquet/writer/parquet_write_memory_usage.test @@ -9,7 +9,7 @@ load __TEST_DIR__/parquet_write_memory_usage.db statement ok set threads=1 -foreach memory_limit,row_group_size 0.3mb,20480 0.6mb,40960 +foreach memory_limit,row_group_size 0.6mb,20480 1.2mb,40960 statement ok set memory_limit='${memory_limit}' diff --git a/test/sql/function/generic/hash_func.test b/test/sql/function/generic/hash_func.test index 0b933e660ffc..0427e0d0ee6c 100644 --- a/test/sql/function/generic/hash_func.test +++ b/test/sql/function/generic/hash_func.test @@ -44,9 +44,9 @@ CREATE TABLE structs AS query II SELECT s, HASH(s) FROM structs ---- -{'i': 5, 's': string} 16279265163003826010 +{'i': 5, 's': string} 312378390946197788 {'i': -2, 's': NULL} 13311620765177879553 -{'i': NULL, 's': not null} 17906579446707938902 +{'i': NULL, 's': not null} 12187543307399756733 {'i': NULL, 's': NULL} 18212156630472451589 NULL 18212156630472451589 @@ -76,11 +76,11 @@ NULL 13787848793156543929 query II SELECT lg, HASH(lg) FROM lists ---- -[TGTA] 6988469852028562792 -[CGGT] 11509251853341801096 -[CCTC] 7465354080729552024 -[TCTA] 8712127848443266422 -[AGGG] 11482125973879342325 +[TGTA] 2473061308111828075 +[CGGT] 17252230290449032892 +[CCTC] 12469451733100292545 +[TCTA] 16441147910138644840 +[AGGG] 6734708784738468094 NULL 13787848793156543929 # Maps @@ -98,11 +98,11 @@ CREATE TABLE maps AS query II SELECT m, HASH(m) FROM maps ---- -{1=TGTA} 2794336106852724683 -{1=CGGT, 2=CCTC} 13102305630601287406 +{1=TGTA} 7235425910004250312 +{1=CGGT, 2=CCTC} 1011047862598495049 {} 13787848793156543929 -{1=TCTA, 2=NULL, 3=CGGT} 4782555145300717917 -{1=TGTA, 2=CGGT, 3=CCTC, 4=TCTA, 5=AGGG} 8572659779500367064 +{1=TCTA, 2=NULL, 3=CGGT} 6001596667924474868 +{1=TGTA, 2=CGGT, 3=CCTC, 4=TCTA, 5=AGGG} 16287978232011168685 NULL 13787848793156543929 statement ok @@ -189,17 +189,17 @@ SELECT r, HASH() FROM enums; query II SELECT r, HASH(r, 'capacitor') FROM enums; ---- -black 7369304742611425093 -brown 2341438809461609958 -red 8885610210938720771 -orange 10151273889449338965 -yellow 9455015799163091888 -green 5769395161578968563 -blue 264671877857503589 -violet 13697912152922098530 -grey 6956627843582995222 -white 11070700999111121301 -NULL 2712243419119719673 +black 16797622758688705282 +brown 12620868779234625953 +red 17584344400128560708 +orange 268160620305560594 +yellow 895888387990267895 +green 16089427619650030004 +blue 10156864916169405730 +violet 3549084991787980581 +grey 17281098274178594641 +white 1655957553588749778 +NULL 12320705626460735678 query II SELECT r, HASH('2022-02-12'::DATE, r) FROM enums; diff --git a/test/sql/index/art/scan/test_art_scan_normal_to_nested.test b/test/sql/index/art/scan/test_art_scan_normal_to_nested.test new file mode 100644 index 000000000000..0cd8cf886fe5 --- /dev/null +++ b/test/sql/index/art/scan/test_art_scan_normal_to_nested.test @@ -0,0 +1,38 @@ +# name: test/sql/index/art/scan/test_art_scan_normal_to_nested.test +# description: Test range scanning with an iterator moving from a normal leaf to a nested leaf. +# group: [scan] + +statement ok +PRAGMA enable_verification + +statement ok +CREATE TABLE integers (i BIGINT); + +statement ok +CREATE INDEX idx_integers ON integers (i); + +statement ok +INSERT INTO integers (i) VALUES ('1'), ('-1'), ('1'); + +# The border is exactly when moving from a non-nested leaf to a nested leaf. + +query I +SELECT i FROM integers WHERE i <= 0; +---- +-1 + +# Issue 16074. + +statement ok +CREATE TABLE t0(c1 TIMESTAMP); + +statement ok +INSERT INTO t0(c1) VALUES ('2020-02-29 12:00:00'), ('1969-12-09 09:26:38'), ('2020-02-29 12:00:00'); + +statement ok +CREATE INDEX i0 ON t0(c1); + +query I +SELECT c1 FROM t0 WHERE c1 <= '2007-07-07 07:07:07'; +---- +1969-12-09 09:26:38 \ No newline at end of file diff --git a/test/sql/join/asof/test_asof_join.test b/test/sql/join/asof/test_asof_join.test index 4f8e0ae0123e..4f1dd370ce13 100644 --- a/test/sql/join/asof/test_asof_join.test +++ b/test/sql/join/asof/test_asof_join.test @@ -30,6 +30,12 @@ create table trades("when" timestamp, symbol int); statement ok insert into trades values ('2020-01-01 00:00:03', 1); +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + query III SELECT t.*, p.price FROM trades t ASOF JOIN prices p @@ -68,13 +74,13 @@ SELECT s1.starts as s1_starts, s2.starts as s2_starts, FROM samples AS s1 ASOF JOIN samples as s2 ON s2.ends >= (s1.ends - 5) -WHERE s1_starts <> s2_starts; +WHERE s1_starts <> s2_starts +ORDER BY ALL ---- -21 14 10 5 +21 14 -# Use an ASOF join inside of a correlated subquery - +endloop # # Errors diff --git a/test/sql/join/asof/test_asof_join_doubles.test b/test/sql/join/asof/test_asof_join_doubles.test index e6c98a7aed3c..9fa3d24df568 100644 --- a/test/sql/join/asof/test_asof_join_doubles.test +++ b/test/sql/join/asof/test_asof_join_doubles.test @@ -9,6 +9,9 @@ PRAGMA enable_verification # Inequality only # +statement ok +PRAGMA asof_loop_join_threshold=0; + # Use doubles for readable infinities statement ok CREATE TABLE events0 (begin DOUBLE, value INTEGER); @@ -324,3 +327,4 @@ ASOF RIGHT JOIN USING (key, begin) ORDER BY 1 ASC NULLS FIRST, 2 ---- + diff --git a/test/sql/join/asof/test_asof_join_inequalities.test b/test/sql/join/asof/test_asof_join_inequalities.test index 8d5a3e1f312b..eef40824789c 100644 --- a/test/sql/join/asof/test_asof_join_inequalities.test +++ b/test/sql/join/asof/test_asof_join_inequalities.test @@ -37,6 +37,12 @@ foreach debug False True statement ok PRAGMA debug_asof_iejoin=${debug} +# Check NLJ results against both +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + # # Strictly Greater Than # @@ -229,3 +235,5 @@ NULL -infinity -1 NULL NULL -10 endloop + +endloop diff --git a/test/sql/join/asof/test_asof_join_integers.test b/test/sql/join/asof/test_asof_join_integers.test index 1bc4b0762a6b..77c414fb4631 100644 --- a/test/sql/join/asof/test_asof_join_integers.test +++ b/test/sql/join/asof/test_asof_join_integers.test @@ -5,7 +5,7 @@ statement ok PRAGMA enable_verification -# Join on a string range +# Join on an integer range statement ok CREATE TABLE events0 (begin INTEGER, value INTEGER); @@ -26,8 +26,11 @@ CREATE TABLE probe0 AS FROM range(0,10) ; -# This is not implemented yet because it requires a dedicated operator -# instead of LEAD(...infinity::INTEGER) +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; # INNER ON inequality only query II @@ -134,3 +137,5 @@ ORDER BY ALL 9 3 NULL -1 NULL 9 + +endloop diff --git a/test/sql/join/asof/test_asof_join_merge.test_slow b/test/sql/join/asof/test_asof_join_merge.test_slow index 544deaad4cd5..12266d3747b7 100644 --- a/test/sql/join/asof/test_asof_join_merge.test_slow +++ b/test/sql/join/asof/test_asof_join_merge.test_slow @@ -11,6 +11,10 @@ PRAGMA threads=4 statement ok SET temp_directory='__TEST_DIR__/temp.tmp' +# Force PhysicalAsOfJoin +statement ok +PRAGMA asof_loop_join_threshold = 0; + query II WITH build AS ( SELECT k, ('2021-01-01'::TIMESTAMP + INTERVAL (i) SECOND) AS t, i % 37 AS v diff --git a/test/sql/join/asof/test_asof_join_missing.test_slow b/test/sql/join/asof/test_asof_join_missing.test_slow index 57a2c64e5231..17355c981f99 100644 --- a/test/sql/join/asof/test_asof_join_missing.test_slow +++ b/test/sql/join/asof/test_asof_join_missing.test_slow @@ -13,6 +13,9 @@ PRAGMA enable_verification # * First payload bin empty # * Multiple scanned payload blocks +statement ok +PRAGMA asof_loop_join_threshold=0; + # Check results against IEJoin foreach debug False True diff --git a/test/sql/join/asof/test_asof_join_pushdown.test b/test/sql/join/asof/test_asof_join_pushdown.test index 1ef308a6eb91..8a55a0ebd400 100644 --- a/test/sql/join/asof/test_asof_join_pushdown.test +++ b/test/sql/join/asof/test_asof_join_pushdown.test @@ -14,6 +14,29 @@ INSERT INTO right_pushdown VALUES (1, NULL), ; +statement ok +CREATE TABLE issue13899(seq_no INT, amount DECIMAL(10,2)); + +statement ok +INSERT INTO issue13899 VALUES + (1,1.00), + (2,null), + (3,null), + (4,null), + (5,2.00), + (6,null), + (7,null), + (8,3.00), + (9,null), + (10,null), + (11,5.00); + +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + query IIII SELECT d1.time, @@ -24,7 +47,8 @@ FROM right_pushdown d1 ASOF JOIN ( SELECT * FROM right_pushdown WHERE value is not NULL ) d2 - ON d1.time >= d2.time; + ON d1.time >= d2.time +ORDER BY ALL; ---- 0 0 0.0 0.0 1 0 NULL 0.0 @@ -39,7 +63,8 @@ FROM right_pushdown d1 ASOF LEFT JOIN ( SELECT * FROM right_pushdown WHERE value is not NULL ) d2 - ON d1.time >= d2.time; + ON d1.time >= d2.time +ORDER BY ALL; ---- 0 0 0.0 0.0 1 0 NULL 0.0 @@ -103,23 +128,6 @@ ORDER BY ALL 5 6 10 11 -statement ok -CREATE TABLE issue13899(seq_no INT, amount DECIMAL(10,2)); - -statement ok -INSERT INTO issue13899 VALUES - (1,1.00), - (2,null), - (3,null), - (4,null), - (5,2.00), - (6,null), - (7,null), - (8,3.00), - (9,null), - (10,null), - (11,5.00); - query III select a.seq_no, @@ -142,3 +150,5 @@ ORDER BY 1 9 NULL 3.00 10 NULL 3.00 11 5.00 5.00 + +endloop diff --git a/test/sql/join/asof/test_asof_join_subquery.test b/test/sql/join/asof/test_asof_join_subquery.test index ec2f72687adb..f61c3f67e602 100644 --- a/test/sql/join/asof/test_asof_join_subquery.test +++ b/test/sql/join/asof/test_asof_join_subquery.test @@ -16,6 +16,12 @@ INSERT INTO events VALUES (8, 3) ; +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + query II SELECT begin, value IN ( SELECT e1.value @@ -34,3 +40,5 @@ ORDER BY ALL; 3.0 true 6.0 true 8.0 true + +endloop diff --git a/test/sql/join/asof/test_asof_join_timestamps.test b/test/sql/join/asof/test_asof_join_timestamps.test index 7e2a6ec0e69e..00aa276d793d 100644 --- a/test/sql/join/asof/test_asof_join_timestamps.test +++ b/test/sql/join/asof/test_asof_join_timestamps.test @@ -32,6 +32,12 @@ INSERT INTO probe0 VALUES ('infinity') ; +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + # INNER ON inequality only query II nosort SELECT p.begin, e.value @@ -204,3 +210,5 @@ ON p.begin >= e.begin ORDER BY p.begin ASC ---- 2023-03-21 12:00:00 + +endloop diff --git a/test/sql/join/asof/test_asof_join_varchar.test b/test/sql/join/asof/test_asof_join_varchar.test index ef5f31e7bfa4..b008c0e2816f 100644 --- a/test/sql/join/asof/test_asof_join_varchar.test +++ b/test/sql/join/asof/test_asof_join_varchar.test @@ -26,6 +26,12 @@ CREATE TABLE probe0 AS FROM range(0,10) ; +# Compare NLJ optimisation to operator +foreach threshold 0 32 + +statement ok +PRAGMA asof_loop_join_threshold = ${threshold}; + # INNER ON inequality only query II SELECT p.begin, e.value @@ -131,3 +137,5 @@ ORDER BY ALL 9 3 NULL -1 NULL 9 + +endloop diff --git a/test/sql/json/table/json_multi_file_reader.test b/test/sql/json/table/json_multi_file_reader.test index a56f7aee1ca3..e7b041b84ac5 100644 --- a/test/sql/json/table/json_multi_file_reader.test +++ b/test/sql/json/table/json_multi_file_reader.test @@ -32,6 +32,26 @@ select * exclude (filename), replace(filename, '\', '/') as filename from read_j 5 Raising Arizona data/json/example_r.ndjson 5 Raising Arizona data/json/example_rn.ndjson +# virtual column +query III +select *, replace(filename, '\', '/') from read_json_auto('data/json/example_*.ndjson') order by all +---- +1 O Brother, Where Art Thou? data/json/example_n.ndjson +1 O Brother, Where Art Thou? data/json/example_r.ndjson +1 O Brother, Where Art Thou? data/json/example_rn.ndjson +2 Home for the Holidays data/json/example_n.ndjson +2 Home for the Holidays data/json/example_r.ndjson +2 Home for the Holidays data/json/example_rn.ndjson +3 The Firm data/json/example_n.ndjson +3 The Firm data/json/example_r.ndjson +3 The Firm data/json/example_rn.ndjson +4 Broadcast News data/json/example_n.ndjson +4 Broadcast News data/json/example_r.ndjson +4 Broadcast News data/json/example_rn.ndjson +5 Raising Arizona data/json/example_n.ndjson +5 Raising Arizona data/json/example_r.ndjson +5 Raising Arizona data/json/example_rn.ndjson + query III select * from read_json_auto(['data/json/example_n.ndjson', 'data/json/top_level_array.json'], union_by_name=true) order by all ---- diff --git a/test/sql/logging/test_logging_function_large.test_slow b/test/sql/logging/test_logging_function_large.test_slow index 7cb6195aaf1c..4bb29995aa02 100644 --- a/test/sql/logging/test_logging_function_large.test_slow +++ b/test/sql/logging/test_logging_function_large.test_slow @@ -33,3 +33,11 @@ SELECT count(*), message FROM duckdb_logs where starts_with(message, 'hi_') grou 250000 hi_client 250000 hi_file 250000 hi_global + +statement ok +pragma truncate_duckdb_logs; + +query I +SELECT count(*) FROM duckdb_logs; +---- +0 \ No newline at end of file diff --git a/test/sql/sample/bernoulli_sampling.test b/test/sql/sample/bernoulli_sampling.test new file mode 100644 index 000000000000..95b3e3796c8f --- /dev/null +++ b/test/sql/sample/bernoulli_sampling.test @@ -0,0 +1,54 @@ +# name: test/sql/sample/bernoulli_sampling.test +# description: Test reservoir sample crash on large data sets +# group: [sample] + +statement ok +create table output (num_rows INT); + +statement ok +select setseed(0.3); + +loop i 0 500 + +statement ok +WITH some_tab AS ( + SELECT UNNEST(range(1000)) AS id +), +some_tab_unq AS ( + SELECT distinct(id) AS id FROM some_tab +), +sampled AS ( + select id from some_tab_unq + USING SAMPLE 1% (bernoulli) +) +INSERT INTO output select count(*) as n_rows FROM sampled; + +endloop + + +query II +select min(num_rows) > 0, count(*) FILTER (num_rows = 0) = 0 from output; +---- +true true + +query III +select avg(rowid), min(rowid), max(rowid) from output where num_rows = 0; +---- +NULL NULL NULL + +statement ok +create table t1 as select range id from range(1000); + +statement ok +select setseed(0.6); + +query I nosort result_1 +select id from t1 USING SAMPLE 1% (bernoulli, 5); +---- + +query I nosort result_1 +select id from t1 USING SAMPLE 1% (bernoulli, 5); +---- + + + diff --git a/test/sql/storage/optimistic_write/optimistic_write_alter_type.test_slow b/test/sql/storage/optimistic_write/optimistic_write_alter_type.test_slow index dbebdcece844..a32e866bad35 100644 --- a/test/sql/storage/optimistic_write/optimistic_write_alter_type.test_slow +++ b/test/sql/storage/optimistic_write/optimistic_write_alter_type.test_slow @@ -2,10 +2,6 @@ # description: Test optimistic write with alter type in transaction-local storage # group: [optimistic_write] -# FIXME: for smaller block sizes (16KB) the database size does not stabilize in the loop, instead, -# FIXME: it grows very slowly (only investigated up to 40 iterations) -require block_size 262144 - load __TEST_DIR__/optimistic_write_alter_type.db statement ok @@ -18,7 +14,7 @@ statement ok INSERT INTO test SELECT i FROM range(1000000) tbl(i) statement ok -ALTER TABLE test ALTER a SET TYPE BIGINT USING a+1 +ALTER TABLE test ALTER a SET TYPE BIGINT USING a + 1 statement ok COMMIT @@ -52,12 +48,12 @@ SELECT SUM(a) FROM test ---- 500000500000 -require skip_reload +# Ensure that we reclaim space correctly. -# ensure the alter type does not result in leaking blocks +require skip_reload -# for smaller block sizes (16KB) the total blocks alternate between a few values in the loop, -# therefore, we need to compare to a range of total block counts +# For smaller block sizes (16KB) the total blocks alternate between a few values in the loop. +# Therefore, we compare to a range of total block counts. statement ok CREATE TABLE total_blocks_tbl AS SELECT total_blocks FROM pragma_database_size(); @@ -76,7 +72,7 @@ statement ok INSERT INTO test SELECT i FROM range(1000000) tbl(i) statement ok -ALTER TABLE test ALTER a SET TYPE BIGINT USING a+1 +ALTER TABLE test ALTER a SET TYPE BIGINT USING a + 1 statement ok COMMIT @@ -86,8 +82,8 @@ SELECT SUM(a) FROM test ---- 500000500000 -# ensure that the total blocks don't exceed the total blocks after the first iteration -# by more than 1.2 +# Ensure that the total blocks don't exceed the total blocks after the first iteration +# by more than 1.2. query I SELECT CASE WHEN ${i} = 0 THEN True @@ -97,7 +93,7 @@ FROM pragma_database_size() AS current, total_blocks_tbl; ---- 1 -# adjust total_blocks_tbl once to the count after the first iteration +# Adjust total_blocks_tbl once to the count after the first iteration. statement ok UPDATE total_blocks_tbl SET total_blocks = ( diff --git a/test/sql/storage/optimistic_write/optimistic_write_delete.test b/test/sql/storage/optimistic_write/optimistic_write_delete.test index 5c71995862c8..b96d893fb865 100644 --- a/test/sql/storage/optimistic_write/optimistic_write_delete.test +++ b/test/sql/storage/optimistic_write/optimistic_write_delete.test @@ -2,7 +2,6 @@ # description: Test optimistic write with deletes in transaction-local storage # group: [optimistic_write] -# load the DB from disk load __TEST_DIR__/optimistic_write_delete.db statement ok diff --git a/test/sql/storage/optimistic_write/optimistic_write_drop_column.test_slow b/test/sql/storage/optimistic_write/optimistic_write_drop_column.test_slow index c91637cdc596..0427213938c0 100644 --- a/test/sql/storage/optimistic_write/optimistic_write_drop_column.test_slow +++ b/test/sql/storage/optimistic_write/optimistic_write_drop_column.test_slow @@ -2,10 +2,6 @@ # description: Test optimistic write with drop column in transaction-local storage # group: [optimistic_write] -# FIXME: for smaller block sizes (16KB) the database size does not stabilize in the loop, instead, -# FIXME: it grows very slowly (only investigated up to 40 iterations) -require block_size 262144 - load __TEST_DIR__/optimistic_write_drop.db statement ok @@ -15,7 +11,7 @@ statement ok BEGIN TRANSACTION statement ok -INSERT INTO test SELECT i, i+1, i+2 FROM range(1000000) tbl(i) +INSERT INTO test SELECT i, i + 1, i + 2 FROM range(1000000) tbl(i) statement ok ALTER TABLE test DROP COLUMN c @@ -52,12 +48,12 @@ SELECT SUM(a), SUM(b) FROM test ---- 499999500000 500000500000 -require skip_reload +# Ensure that we reclaim space correctly. -# ensure the drop column does not result in leaking blocks +require skip_reload -# for smaller block sizes (16KB) the total blocks alternate between a few values in the loop, -# therefore, we need to compare to a range of total block counts +# For smaller block sizes (16KB) the total blocks alternate between a few values in the loop. +# Therefore, we compare to a range of total block counts. statement ok CREATE TABLE total_blocks_tbl AS SELECT total_blocks FROM pragma_database_size(); @@ -73,7 +69,7 @@ statement ok BEGIN TRANSACTION statement ok -INSERT INTO test SELECT i, i+1, i+2 FROM range(1000000) tbl(i) +INSERT INTO test SELECT i, i + 1, i + 2 FROM range(1000000) tbl(i) statement ok ALTER TABLE test DROP COLUMN c @@ -86,8 +82,8 @@ SELECT SUM(a), SUM(b) FROM test ---- 499999500000 500000500000 -# ensure that the total blocks don't exceed the total blocks after the first iteration -# by more than 1.2 +# Ensure that the total blocks don't exceed the total blocks after the first iteration +# by more than 1.2. query I SELECT CASE WHEN ${i} = 0 THEN True @@ -97,7 +93,7 @@ FROM pragma_database_size() AS current, total_blocks_tbl; ---- 1 -# adjust total_blocks_tbl once to the count after the first iteration +# Adjust total_blocks_tbl once to the count after the first iteration. statement ok UPDATE total_blocks_tbl SET total_blocks = ( diff --git a/test/sql/storage/optimistic_write/optimistic_write_update.test b/test/sql/storage/optimistic_write/optimistic_write_update.test index 41bc6d1f7c6d..12c42a763824 100644 --- a/test/sql/storage/optimistic_write/optimistic_write_update.test +++ b/test/sql/storage/optimistic_write/optimistic_write_update.test @@ -2,7 +2,6 @@ # description: Test optimistic write with updates in transaction-local storage # group: [optimistic_write] -# load the DB from disk load __TEST_DIR__/optimistic_write_update.db statement ok diff --git a/test/sql/storage/parallel/insert_order_preserving_odd_sized_batches.test_slow b/test/sql/storage/parallel/insert_order_preserving_odd_sized_batches.test_slow index 3474b2e0621c..b87a2f377424 100644 --- a/test/sql/storage/parallel/insert_order_preserving_odd_sized_batches.test_slow +++ b/test/sql/storage/parallel/insert_order_preserving_odd_sized_batches.test_slow @@ -2,9 +2,6 @@ # description: Test parallel order-preserving insert # group: [parallel] -# FIXME: see internal issue 3931. -mode skip - # There are different numbers of distinct blocks for smaller block sizes, # because the segment size is bound by the block size. require block_size 262144 @@ -20,7 +17,7 @@ CREATE TABLE integers AS SELECT * FROM range(10000000) tbl(i); ---- 10000000 -# check the block count and median number of rows per row group +# Check the block count and median number of rows per row group. query I SELECT COUNT(DISTINCT block_id) < 4 FROM pragma_storage_info('integers'); ---- @@ -34,7 +31,6 @@ SELECT MEDIAN(count) FROM pragma_storage_info('integers'); statement ok COPY integers TO '__TEST_DIR__/integers.parquet' (ROW_GROUP_SIZE 77777) -# verify that reading while preserving insertion order creates the same size table statement ok CREATE TABLE integers_parquet AS FROM '__TEST_DIR__/integers.parquet'; @@ -62,11 +58,12 @@ SELECT COUNT(DISTINCT block_id) < 4 FROM pragma_storage_info('integers_parquet') true query I -SELECT MEDIAN(count)>100000 FROM pragma_storage_info('integers_parquet'); +SELECT MEDIAN(count) > 100000 FROM pragma_storage_info('integers_parquet'); ---- true -# verify that reading without preserving insertion order creates the same size table +# FIXME: does this even make sense? +# Verify that reading without preserving insertion order creates a same size table. statement ok SET preserve_insertion_order=false @@ -74,11 +71,11 @@ statement ok CREATE TABLE integers_parquet_no_order AS FROM '__TEST_DIR__/integers.parquet' query I -SELECT COUNT(DISTINCT block_id) < 4 FROM pragma_storage_info('integers_parquet_no_order'); +SELECT COUNT(DISTINCT block_id) < 12 FROM pragma_storage_info('integers_parquet_no_order'); ---- true query I -SELECT MEDIAN(count)>100000 FROM pragma_storage_info('integers_parquet_no_order'); +SELECT MEDIAN(count) > 100000 FROM pragma_storage_info('integers_parquet_no_order'); ---- true diff --git a/test/sql/storage/parallel/reclaim_space_insert_unique_idx_optimistic.test_slow b/test/sql/storage/parallel/reclaim_space_insert_unique_idx_optimistic.test_slow index c09499562f34..243f4860b205 100644 --- a/test/sql/storage/parallel/reclaim_space_insert_unique_idx_optimistic.test_slow +++ b/test/sql/storage/parallel/reclaim_space_insert_unique_idx_optimistic.test_slow @@ -2,16 +2,13 @@ # description: Test space reclamation of optimistic writing with a UNIQUE constraint violation. # group: [parallel] -# FIXME: see internal issue 3931. -mode skip - load __TEST_DIR__/reclaim_space_unique_index.db statement ok SET preserve_insertion_order=false; statement ok -CREATE TABLE integers AS SELECT * FROM range(10000000) t(i); +CREATE TABLE integers AS SELECT * FROM range(1_000_000) t(i); statement ok CREATE TABLE integers2 (i INTEGER); @@ -24,6 +21,7 @@ CREATE UNIQUE INDEX idx ON integers2(i); # For smaller block sizes (16KB) the total blocks increase (to twice the original amount) in the first # iteration, and then stay constant. + statement ok CREATE TABLE total_blocks_tbl AS SELECT total_blocks FROM pragma_database_size(); @@ -45,12 +43,12 @@ statement ok CHECKPOINT; statement ok -INSERT INTO integers2 VALUES (9999998); +INSERT INTO integers2 VALUES (999_998); # Invalidate the transaction. statement error -INSERT INTO integers2 SELECT * FROM integers WHERE i <= 9999998; +INSERT INTO integers2 SELECT * FROM integers WHERE i <= 999_998; ---- :Constraint Error.*PRIMARY KEY or UNIQUE constraint violation.* diff --git a/test/sql/storage/parallel/reclaim_space_primary_key_optimistic.test_slow b/test/sql/storage/parallel/reclaim_space_primary_key_optimistic.test_slow index 27753dd7877a..07cce7185d9a 100644 --- a/test/sql/storage/parallel/reclaim_space_primary_key_optimistic.test_slow +++ b/test/sql/storage/parallel/reclaim_space_primary_key_optimistic.test_slow @@ -2,9 +2,6 @@ # description: Test space reclamation of optimistic writing with a PK constraint violation. # group: [parallel] -# FIXME: see internal issue 3931. -mode skip - load __TEST_DIR__/reclaim_space_primary_key.db statement ok @@ -24,8 +21,20 @@ INSERT INTO integers2 SELECT * FROM integers; ---- :Constraint Error.*violates primary key constraint.* +# For smaller block sizes (16KB) the total blocks increase (to twice the original amount) in the first +# iteration, and then stay constant. + statement ok -CREATE TABLE block_count (count INT); +CREATE TABLE total_blocks_tbl AS SELECT total_blocks FROM pragma_database_size(); + +statement ok +CREATE TYPE test_result AS UNION ( + ok BOOL, + err STRUCT( + old BIGINT, + allowed_max DECIMAL(21,1), + actual BIGINT) +); loop i 0 10 @@ -59,19 +68,34 @@ INSERT INTO integers2 VALUES (10000000 + ${i}); statement ok CHECKPOINT; -statement ok -INSERT INTO block_count SELECT total_blocks FROM pragma_database_size(); - query I SELECT COUNT(*) - ${i} FROM integers2; ---- 2 -# Ensure there is only a small difference between the MIN and MAX block counts. +# Ensure that the total blocks don't exceed the total blocks after the first iteration by more than 1.2. query I -SELECT (MAX(count) - MIN(count)) < 20 FROM block_count; +SELECT + CASE WHEN ${i} = 0 THEN True::test_result + WHEN current.total_blocks <= total_blocks_tbl.total_blocks * 1.6 THEN True::test_result + ELSE { + 'old': total_blocks_tbl.total_blocks, + 'allowed_max': total_blocks_tbl.total_blocks * 1.6, + 'actual': current.total_blocks + }::test_result + END +FROM pragma_database_size() AS current, total_blocks_tbl; ---- true +# Adjust the total_blocks_tbl once to the count after the first iteration. + +statement ok +UPDATE total_blocks_tbl SET total_blocks = ( + SELECT + CASE WHEN ${i} = 0 THEN (SELECT current.total_blocks FROM pragma_database_size() AS current) + ELSE (total_blocks)END + ); + endloop diff --git a/test/sql/subquery/scalar/array_order_subquery.test b/test/sql/subquery/scalar/array_order_subquery.test index a0ca2fb4c7d0..94abd308009a 100644 --- a/test/sql/subquery/scalar/array_order_subquery.test +++ b/test/sql/subquery/scalar/array_order_subquery.test @@ -86,6 +86,16 @@ SELECT ARRAY ---- [3, 2, 1] +query I +select array(select * from unnest(['a', 'b']) as _t(u) order by if(u='a',100, 1)) as out; +---- +[b, a] + +query I +select array(select * from unnest(['a', 'b']) as _t(u) order by if(u='a',100, 1) desc) as out; +---- +[a, b] + statement error SELECT ARRAY (SELECT 1 UNION ALL diff --git a/test/sql/types/enum/test_enum_from_query.test_slow b/test/sql/types/enum/test_enum_from_query.test_slow index d9d1d68cd2e6..68697606bcb5 100644 --- a/test/sql/types/enum/test_enum_from_query.test_slow +++ b/test/sql/types/enum/test_enum_from_query.test_slow @@ -107,10 +107,9 @@ DROP TABLE number_str; statement ok DROP TYPE number_enum; -# Throw exception for NULL -statement error -CREATE TYPE number_enum AS ENUM (SELECT NULL::VARCHAR); ----- +# This just creates an empty enum type +statement ok +CREATE TYPE empty_number_enum AS ENUM (SELECT NULL::VARCHAR); # Test inserted order statement ok diff --git a/test/sql/types/nested/array/array_cast.test b/test/sql/types/nested/array/array_cast.test index 9c8abcda0260..dc5efbaba844 100644 --- a/test/sql/types/nested/array/array_cast.test +++ b/test/sql/types/nested/array/array_cast.test @@ -29,6 +29,7 @@ SELECT list_extract(array_value(1, 2, 3), 2) statement error SELECT * FROM UNNEST(array_value(1, 2, 3)) ---- +UNNEST requires a single list as input # Should be able to cast to list with unnest query I @@ -72,6 +73,7 @@ SELECT ['1.0', '2.0', '3.0']::DOUBLE[3] statement error SELECT [1, 2, 3]::BLOB[3] ---- +Unimplemented type for cast (INTEGER -> BLOB) # Should be able to cast from NULL query I @@ -138,11 +140,13 @@ NULL statement error SELECT (['2', 'abc', '3']::VARCHAR[3])::INT[] ---- +Could not convert string 'abc' to INT32 # Should not be able to cast to unrelated type statement error SELECT ([1,2,3]::INT[3])::INT; ---- +Unimplemented type for cast (INTEGER[3] -> INTEGER) # Should not be able to cast to list if child types fail query I @@ -166,4 +170,4 @@ SELECT '[1, 2, 3]'::INTEGER[3] query I SELECT TRY_CAST(l AS INTEGER[][3]) FROM VALUES (['foo']) as v(l); ---- -NULL \ No newline at end of file +NULL diff --git a/test/sql/types/nested/array/array_try_cast.test b/test/sql/types/nested/array/array_try_cast.test index d3398b54fa3d..4e90f308d6ff 100644 --- a/test/sql/types/nested/array/array_try_cast.test +++ b/test/sql/types/nested/array/array_try_cast.test @@ -53,7 +53,7 @@ NULL statement error SELECT CAST('[1,2]' as INTEGER[3]); ---- -Conversion Error: Type VARCHAR with value '[1,2]' can't be cast to the destination type ARRAY[3], the size of the array must match the destination type +Conversion Error: Type VARCHAR with value '[1,2]' can't be cast to the destination type INTEGER[3], the size of the array must match the destination type query I SELECT CAST('[NULL, [1], [NULL]]' as INTEGER[1][3]); @@ -78,7 +78,7 @@ SELECT CAST('[NULL, [1,NULL,3], [1,2,3]]' as INTEGER[3][3]); statement error SELECT CAST('[NULL, [1,NULL,3], [1,2]]' as INTEGER[3][3]); ---- -Conversion Error: Type VARCHAR with value '[1,2]' can't be cast to the destination type ARRAY[3], the size of the array must match the destination type +Conversion Error: Type VARCHAR with value '[1,2]' can't be cast to the destination type INTEGER[3], the size of the array must match the destination type query I SELECT TRY_CAST('[NULL, [1,NULL,3], [1,2]]' as INTEGER[3][3]); diff --git a/test/sql/types/struct/unnamed_struct_casts.test b/test/sql/types/struct/unnamed_struct_casts.test index 9ab777d37ffb..5b8d3d6b4a25 100644 --- a/test/sql/types/struct/unnamed_struct_casts.test +++ b/test/sql/types/struct/unnamed_struct_casts.test @@ -10,7 +10,8 @@ select row(42, 'hello') union all select '{'': 42,'': hello}'; ---- Conversion Error -statement error +query I select row(42, 'hello') union all select '(84, world)'; ---- -unsupported +(42, hello) +(84, world) diff --git a/test/sql/variables/test_variables.test b/test/sql/variables/test_variables.test index ad3c15d43f57..b67d81686222 100644 --- a/test/sql/variables/test_variables.test +++ b/test/sql/variables/test_variables.test @@ -13,6 +13,22 @@ SELECT GETVARIABLE('animal') ---- duck +statement ok +PREPARE v1 AS SELECT GETVARIABLE($1); + +query I +EXECUTE v1('animal'); +---- +duck + +statement ok +CREATE MACRO _(x) AS getvariable(x); + +query I +SELECT _('animal') +---- +duck + # overwriting statement ok SET VARIABLE animal='bird' diff --git a/tools/pythonpkg/setup.py b/tools/pythonpkg/setup.py index 0152d0d6b06b..6273472da676 100644 --- a/tools/pythonpkg/setup.py +++ b/tools/pythonpkg/setup.py @@ -173,7 +173,7 @@ def open_utf8(fpath, flags): if 'DUCKDB_BINARY_DIR' in os.environ: existing_duckdb_dir = os.environ['DUCKDB_BINARY_DIR'] if 'DUCKDB_COMPILE_FLAGS' in os.environ: - toolchain_args = ['-std=c++11'] + os.environ['DUCKDB_COMPILE_FLAGS'].split() + toolchain_args = os.environ['DUCKDB_COMPILE_FLAGS'].split() if 'DUCKDB_LIBS' in os.environ: libraries = os.environ['DUCKDB_LIBS'].split(' ')