diff --git a/src/duckdb/extension/core_functions/aggregate/algebraic/avg.cpp b/src/duckdb/extension/core_functions/aggregate/algebraic/avg.cpp index 6d5b67faf..bbe22c410 100644 --- a/src/duckdb/extension/core_functions/aggregate/algebraic/avg.cpp +++ b/src/duckdb/extension/core_functions/aggregate/algebraic/avg.cpp @@ -48,7 +48,7 @@ struct KahanAvgState { void Combine(const KahanAvgState &other) { this->count += other.count; KahanAddInternal(other.value, this->value, this->err); - KahanAddInternal(other.err, this->value, this->err); + KahanAddInternal(-other.err, this->value, this->err); } }; @@ -143,6 +143,27 @@ struct DiscreteAverageOperation : public BaseSumOperation>(states + i * stride); + if (state.count == 0) { + continue; + } + const auto max_value = hugeint_t(0, state.count) * hugeint_t(Interval::MICROS_PER_DAY); + if (state.value < 0 || state.value > max_value) { + throw InvalidInputException("Invalid avg state - the average of %s over %llu values is not a valid time", + state.value.ToString(), state.count); + } + } +} + +AggregateStateLayout TimeAverageStateLayout(AggregateLayoutInput &input) { + auto layout = AggregateFunction::StructStateLayout>(input); + layout.validate_state = ValidateTimeAverageStates; + return layout; +} + struct HugeintAverageOperation : public BaseSumOperation { template static void RepeatedCombine(const STATE &source, STATE &target, AggregateInputData &input, idx_t count) { @@ -182,7 +203,7 @@ struct KahanAverageOperation : public BaseSumOperation, int64_t, int64_t, DiscreteAverageOperation>( LogicalType::TIME, LogicalType::TIME); time_avg.GetSignature().GetParameter(0).SetName("x"); + time_avg.SetStructStateExport(TimeAverageStateLayout); avg.AddFunction(time_avg); auto time_tz_avg = AggregateFunction::UnaryAggregate, dtime_tz_t, dtime_tz_t, TimeTZAverageOperation>( LogicalType::TIME_TZ, LogicalType::TIME_TZ); time_tz_avg.GetSignature().GetParameter(0).SetName("x"); + time_tz_avg.SetStructStateExport(TimeAverageStateLayout); avg.AddFunction(time_tz_avg); return avg; diff --git a/src/duckdb/extension/core_functions/aggregate/distributive/sum.cpp b/src/duckdb/extension/core_functions/aggregate/distributive/sum.cpp index f84271134..55a67fb58 100644 --- a/src/duckdb/extension/core_functions/aggregate/distributive/sum.cpp +++ b/src/duckdb/extension/core_functions/aggregate/distributive/sum.cpp @@ -230,6 +230,23 @@ struct HugeintSumOperation } }; +//! The sum of a DECIMAL is a DECIMAL(38) - finalizing throws if the sum does not fit in it +template +struct DecimalSumOperation : public OP { + template + static void Finalize(STATE &state, T &target, AggregateFinalizeData &finalize_data) { + OP::template Finalize(state, target, finalize_data); + if (!state.is_set) { + return; + } + const auto &max_value = Hugeint::POWERS_OF_TEN[Decimal::MAX_WIDTH_DECIMAL]; + if (target >= max_value || target <= -max_value) { + throw OutOfRangeException("Overflow in SUM of DECIMAL: the result does not fit in DECIMAL(%d)", + Decimal::MAX_WIDTH_DECIMAL); + } + } +}; + unique_ptr SumNoOverflowBind(BindAggregateFunctionInput &input) { throw BinderException("sum_no_overflow is for internal use only!"); } @@ -416,11 +433,36 @@ AggregateFunction GetSumAggregate(PhysicalType type) { } } +template +AggregateFunction GetDecimalSumAggregate(const LogicalType &input_type) { + auto function = + AggregateFunction::UnaryAggregate, INPUT_TYPE, hugeint_t, DecimalSumOperation>( + input_type, LogicalType::HUGEINT); + function.GetSignature().GetParameter(0).SetName("arg"); + function.SetStatisticsCallback(SumPropagateStats); + function.SetOrderDependent(AggregateOrderDependent::NOT_ORDER_DEPENDENT); + return function; +} + +AggregateFunction GetDecimalSumAggregate(PhysicalType type) { + switch (type) { + case PhysicalType::INT32: + return GetDecimalSumAggregate(LogicalType::INTEGER); + case PhysicalType::INT64: + return GetDecimalSumAggregate(LogicalType::BIGINT); + case PhysicalType::INT128: + return GetDecimalSumAggregate(LogicalType::HUGEINT); + default: + // the sum of smaller decimals is accumulated in an int64_t, which always fits in a DECIMAL(38) + return GetSumAggregate(type); + } +} + unique_ptr BindDecimalSum(BindAggregateFunctionInput &input) { auto &function = input.GetBoundFunction(); auto &arguments = input.GetArguments(); auto decimal_type = arguments[0]->GetReturnType(); - function.ReplaceImplementation(GetSumAggregate(decimal_type.InternalType())); + function.ReplaceImplementation(GetDecimalSumAggregate(decimal_type.InternalType())); function.SetName("sum"); function.GetArguments()[0] = decimal_type; function.SetReturnType(LogicalType::DECIMAL(Decimal::MAX_WIDTH_DECIMAL, DecimalType::GetScale(decimal_type))); diff --git a/src/duckdb/extension/core_functions/aggregate/holistic/approximate_quantile.cpp b/src/duckdb/extension/core_functions/aggregate/holistic/approximate_quantile.cpp index 9f6de7324..008c81d94 100644 --- a/src/duckdb/extension/core_functions/aggregate/holistic/approximate_quantile.cpp +++ b/src/duckdb/extension/core_functions/aggregate/holistic/approximate_quantile.cpp @@ -270,13 +270,30 @@ void ApproxQuantileImportState(AggregateImportInputData &input) { } arena_vector centroids(input.allocator); centroids.reserve(centroid_list.GetListLength()); + double total_weight = 0; for (const auto centroid_entry : centroid_list.GetChildValues()) { const auto mean_entry = centroid_entry.template GetChildValue<0>(); const auto weight_entry = centroid_entry.template GetChildValue<1>(); if (!centroid_entry.IsValid() || !mean_entry.IsValid() || !weight_entry.IsValid()) { throw InvalidInputException("Invalid approx_quantile state - the centroids cannot be NULL"); } - centroids.emplace_back(mean_entry.GetValue(), weight_entry.GetValue()); + const auto mean = mean_entry.GetValue(); + const auto weight = weight_entry.GetValue(); + if (Value::IsNan(mean) || !Value::IsFinite(weight) || weight <= 0) { + throw InvalidInputException( + "Invalid approx_quantile state - centroids must have a mean and a positive finite weight"); + } + if (!centroids.empty() && mean < centroids.back().mean()) { + throw InvalidInputException("Invalid approx_quantile state - centroids must be sorted by their mean"); + } + total_weight += weight; + centroids.emplace_back(mean, weight); + } + if (!Value::IsFinite(total_weight)) { + throw InvalidInputException("Invalid approx_quantile state - the total centroid weight must be finite"); + } + if (Value::IsNan(min_entry.GetValue()) || Value::IsNan(max_entry.GetValue())) { + throw InvalidInputException("Invalid approx_quantile state - min and max cannot be NaN"); } auto digest = input.allocator.Make( std::move(centroids), duckdb::arena_vector(input.allocator), 100, 0, 0); diff --git a/src/duckdb/extension/core_functions/aggregate/holistic/quantile.cpp b/src/duckdb/extension/core_functions/aggregate/holistic/quantile.cpp index 45b0a72af..eca20a518 100644 --- a/src/duckdb/extension/core_functions/aggregate/holistic/quantile.cpp +++ b/src/duckdb/extension/core_functions/aggregate/holistic/quantile.cpp @@ -405,6 +405,10 @@ struct QuantileListFallback : QuantileOperation { //===--------------------------------------------------------------------===// template AggregateFunction GetDiscreteQuantileTemplated(const LogicalType &type) { + if (type.id() == LogicalTypeId::TIME_TZ) { + // TIMETZ is ordered by its normalized time, not by its raw bits + return OP::template GetFunction(type); + } switch (type.InternalType()) { #if !DUCKDB_SMALLER_BINARY(quantile_types) case PhysicalType::INT8: @@ -550,8 +554,10 @@ AggregateFunction GetContinuousQuantileTemplated(const LogicalType &type) { case LogicalTypeId::TIMESTAMP_NS: return OP::template GetFunction(type, type); case LogicalTypeId::TIME: - case LogicalTypeId::TIME_TZ: return OP::template GetFunction(type, type); + case LogicalTypeId::TIME_TZ: + // TIMETZ is ordered and interpolated by its normalized time, not by its raw bits + return OP::template GetFunction(type, type); case LogicalTypeId::INTERVAL: return OP::template GetFunction(type, type); default: diff --git a/src/duckdb/extension/core_functions/include/core_functions/aggregate/quantile_state.hpp b/src/duckdb/extension/core_functions/include/core_functions/aggregate/quantile_state.hpp index 50ef8a7b3..05a80ad2f 100644 --- a/src/duckdb/extension/core_functions/include/core_functions/aggregate/quantile_state.hpp +++ b/src/duckdb/extension/core_functions/include/core_functions/aggregate/quantile_state.hpp @@ -393,6 +393,8 @@ AggregateStateLayout QuantileStateLayout(AggregateLayoutInput &input) { } layout.total_state_size = AlignValue(sizeof(STATE)); layout.field = BuildStateField(); + // NULL inputs are never buffered, so the finalize assumes all values are valid + layout.field.reject_null_elements = true; AggregateStateField::PopulateListFunctions(layout.type, layout.field); if (function.GetArguments().size() == 2) { // the quantile parameter must be a constant at bind time (BindQuantile folds it into the bind data) - diff --git a/src/duckdb/extension/core_functions/include/core_functions/aggregate/sum_helpers.hpp b/src/duckdb/extension/core_functions/include/core_functions/aggregate/sum_helpers.hpp index d6409dd9c..7c35853ae 100644 --- a/src/duckdb/extension/core_functions/include/core_functions/aggregate/sum_helpers.hpp +++ b/src/duckdb/extension/core_functions/include/core_functions/aggregate/sum_helpers.hpp @@ -100,7 +100,7 @@ struct KahanSumState { void Combine(const KahanSumState &other) { this->is_set = other.is_set || this->is_set; KahanAddInternal(other.value, this->value, this->err); - KahanAddInternal(other.err, this->value, this->err); + KahanAddInternal(-other.err, this->value, this->err); } }; diff --git a/src/duckdb/extension/core_functions/scalar/enum/enum_functions.cpp b/src/duckdb/extension/core_functions/scalar/enum/enum_functions.cpp index a2893dcbd..f493d42bd 100644 --- a/src/duckdb/extension/core_functions/scalar/enum/enum_functions.cpp +++ b/src/duckdb/extension/core_functions/scalar/enum/enum_functions.cpp @@ -36,30 +36,25 @@ static void EnumRangeFunction(DataChunk &input, ExpressionState &state, Vector & static void EnumRangeBoundaryFunction(DataChunk &input, ExpressionState &state, Vector &result) { auto types = input.GetTypes(); D_ASSERT(types.size() == 2); - idx_t start, end; - auto first_param = input.GetValue(0, 0); - auto second_param = input.GetValue(1, 0); // the binder guarantees that at least one of the parameters is an ENUM auto &enum_type = types[0].id() == LogicalTypeId::ENUM ? types[0] : types[1]; auto &enum_vector = EnumType::GetValuesInsertOrder(enum_type); - - if (first_param.IsNull()) { - start = 0; - } else { - start = first_param.GetValue(); - } - if (second_param.IsNull()) { - end = EnumType::GetSize(enum_type); - } else { - end = second_param.GetValue() + 1; + auto enum_strings = enum_vector.Values(); + + const auto count = input.size(); + auto writer = FlatVector::Writer>(result, count); + for (idx_t row = 0; row < count; row++) { + // a NULL boundary means that the range starts at the first / ends at the last value of the enum + auto first_param = input.GetValue(0, row); + auto second_param = input.GetValue(1, row); + idx_t start = first_param.IsNull() ? 0 : first_param.GetValue(); + idx_t end = second_param.IsNull() ? EnumType::GetSize(enum_type) : second_param.GetValue() + 1; + idx_t enum_idx = start; + for (auto &child_writer : writer.WriteList(end > start ? end - start : 0)) { + child_writer.WriteValue(enum_strings[enum_idx++].GetValue()); + } } - vector enum_values; - for (idx_t i = start; i < end; i++) { - enum_values.emplace_back(enum_vector.GetValue(i)); - } - auto val = Value::LIST(LogicalType::VARCHAR, enum_values); - result.Reference(val, count_t(input.size())); } static void EnumCodeFunction(DataChunk &input, ExpressionState &state, Vector &result) { diff --git a/src/duckdb/extension/core_functions/scalar/list/array_slice.cpp b/src/duckdb/extension/core_functions/scalar/list/array_slice.cpp index 2f4c844a4..32cfada0a 100644 --- a/src/duckdb/extension/core_functions/scalar/list/array_slice.cpp +++ b/src/duckdb/extension/core_functions/scalar/list/array_slice.cpp @@ -194,6 +194,10 @@ void ExecuteFlatSlice(Vector &result, Vector &list_vector, Vector &begin_vector, auto begin = begin_is_empty ? 0 : UnifiedVectorFormat::GetData(begin_data)[begin_idx]; auto end = end_is_empty ? OP::ValueLength(sliced) : UnifiedVectorFormat::GetData(end_data)[end_idx]; auto step = step_vector ? UnifiedVectorFormat::GetData(step_data)[step_idx] : 1; + if (step == NumericLimits::Minimum()) { + // the minimum cannot be negated - any step that is at least as large as the list selects the same entry + step = -NumericLimits::Maximum(); + } if (step < 0) { swap(begin, end); diff --git a/src/duckdb/extension/core_functions/scalar/math/numeric.cpp b/src/duckdb/extension/core_functions/scalar/math/numeric.cpp index ab06b3557..f71b80a0e 100644 --- a/src/duckdb/extension/core_functions/scalar/math/numeric.cpp +++ b/src/duckdb/extension/core_functions/scalar/math/numeric.cpp @@ -1308,11 +1308,10 @@ struct ExpOperator { } // namespace ScalarFunction ExpFun::GetFunction() { - auto func = NameXArgument( + // not annotated monotone: libm exp is not monotone on every platform (Apple libm inverts adjacent doubles) + return NameXArgument( ScalarFunction({}, LogicalType::DOUBLE, ScalarFunction::UnaryFunction), LogicalType::DOUBLE); - func.SetUnaryArgProperties(ArgProperties().NonDecreasing()); - return func; } //===--------------------------------------------------------------------===// diff --git a/src/duckdb/extension/icu/datetime/coptic.cpp b/src/duckdb/extension/icu/datetime/coptic.cpp index f757a588d..08ac67415 100644 --- a/src/duckdb/extension/icu/datetime/coptic.cpp +++ b/src/duckdb/extension/icu/datetime/coptic.cpp @@ -54,7 +54,7 @@ int64_t CopticEthiopicCalendar::HandleComputeMonthStart(int32_t eyear, int32_t m return int64_t(GetEpochOffset()) // the day the first year starts on + 365 * year // the whole years + FloorDiv::Divide(year, int64_t(4)) // their leap days - + 30 * month // the whole months, which are all thirty days long + + 30 * int64_t(month) // the whole months, which are all thirty days long - 1; // the day before the first of the month } diff --git a/src/duckdb/extension/icu/datetime/islamic.cpp b/src/duckdb/extension/icu/datetime/islamic.cpp index 0e9bef659..afbe6d85c 100644 --- a/src/duckdb/extension/icu/datetime/islamic.cpp +++ b/src/duckdb/extension/icu/datetime/islamic.cpp @@ -215,7 +215,7 @@ void IslamicCalendar::HandleComputeFields(int32_t julian_day) { // guess how many whole months have passed, then correct the guess auto month = int32_t(std::floor(double(days) / Astronomer::SYNODIC_MONTH)); const auto start_date = int32_t(std::floor(month * Astronomer::SYNODIC_MONTH)); - if (days - start_date >= 25 && MoonAge(GetTimeInternal()) > 0) { + if (days - start_date >= 25 && MoonAge(double(GetTimeInternal())) > 0) { // the day is near the end of the month, so assume the next one and search backwards month++; } @@ -291,7 +291,7 @@ int64_t IslamicUmalquraCalendar::YearStart(int32_t year) const { year -= TABLE_START; // a rounded least squares fit of the dates that follow from the observed month lengths, // with a correction for the years it does not land on - const auto estimate = int64_t((354.36720 * double(year)) + 460322.05 + 0.5); + const auto estimate = int64_t(std::llround((354.36720 * double(year)) + 460322.05)); return estimate + UMALQURA_YEAR_START_FIX[year]; } diff --git a/src/duckdb/extension/icu/icu-datefunc.cpp b/src/duckdb/extension/icu/icu-datefunc.cpp index 04943c8f6..1b681ff47 100644 --- a/src/duckdb/extension/icu/icu-datefunc.cpp +++ b/src/duckdb/extension/icu/icu-datefunc.cpp @@ -160,7 +160,7 @@ bool ICUDateFunc::TryGetTimeNS(Calendar *calendar, uint64_t nanos, timestamp_tz_ nanos %= Interval::NANOS_PER_MICRO; timestamp_t us(tstz_micros); timestamp_ns_t ns; - if (!Timestamp::TryFromTimestampNanos(us, nanos, ns)) { + if (!Timestamp::TryFromTimestampNanos(us, int32_t(nanos), ns)) { return false; } result.value = ns.value; diff --git a/src/duckdb/extension/json/json_common.cpp b/src/duckdb/extension/json/json_common.cpp index 5aa54a8fc..fd8ab6c02 100644 --- a/src/duckdb/extension/json/json_common.cpp +++ b/src/duckdb/extension/json/json_common.cpp @@ -127,6 +127,10 @@ static inline idx_t ReadInteger(const char *ptr, const char *const end, idx_t &i } ptr++; } + if (ptr == end || *ptr != ']') { + // No closing ']', or too many digits + return 0; + } // Invalid if overflow return idx >= (idx_t)IDX_T_MAX ? 0 : ptr - before; } diff --git a/src/duckdb/extension/json/json_functions/json_merge_patch.cpp b/src/duckdb/extension/json/json_functions/json_merge_patch.cpp index f8d1c1e8b..c203ea280 100644 --- a/src/duckdb/extension/json/json_functions/json_merge_patch.cpp +++ b/src/duckdb/extension/json/json_functions/json_merge_patch.cpp @@ -36,6 +36,7 @@ static inline yyjson_mut_val *MergePatch(yyjson_mut_doc *doc, yyjson_mut_val *or if (nodes.orig) { idx_t idx, max; yyjson_mut_val *key, *orig_val; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast): yyjson iteration macro yyjson_mut_obj_foreach(nodes.orig, idx, max, key, orig_val) { auto patch_val = yyjson_mut_obj_getn(nodes.patch, unsafe_yyjson_get_str(key), unsafe_yyjson_get_len(key)); @@ -49,6 +50,7 @@ static inline yyjson_mut_val *MergePatch(yyjson_mut_doc *doc, yyjson_mut_val *or // Merge patch keys; null removes the key idx_t idx, max; yyjson_mut_val *key, *patch_val; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast): yyjson iteration macro yyjson_mut_obj_foreach(nodes.patch, idx, max, key, patch_val) { if (unsafe_yyjson_is_null(patch_val)) { continue; diff --git a/src/duckdb/extension/json/json_functions/read_single_json_file.cpp b/src/duckdb/extension/json/json_functions/read_single_json_file.cpp index 2d394b53f..8e46f2d1c 100644 --- a/src/duckdb/extension/json/json_functions/read_single_json_file.cpp +++ b/src/duckdb/extension/json/json_functions/read_single_json_file.cpp @@ -309,7 +309,7 @@ static unique_ptr ReadSingleJSONFileCardinality(ClientContext &c TableFunction JSONFunctions::GetReadSingleJSONFileTableFunction(shared_ptr function_info) { const auto scan_type = function_info->type; TableFunction table_function( - "read_single_json_file", FunctionSignature().AddPositionalOnly("path", LogicalType::VARCHAR), + "read_single_json_file", FunctionSignature().AddPositionalOnly("file", LogicalType::ANY), ReadSingleJSONFileFunction, ReadSingleJSONFileBind, ReadSingleJSONFileInitGlobal, ReadSingleJSONFileInitLocal); JSONScan::TableFunctionDefaults(table_function); if (scan_type != JSONScanType::READ_JSON_OBJECTS) { diff --git a/src/duckdb/extension/parquet/include/parquet_timestamp.hpp b/src/duckdb/extension/parquet/include/parquet_timestamp.hpp index 14cce5d9d..0f8d9491c 100644 --- a/src/duckdb/extension/parquet/include/parquet_timestamp.hpp +++ b/src/duckdb/extension/parquet/include/parquet_timestamp.hpp @@ -30,6 +30,8 @@ timestamp_t ParquetTimestampMsToTimestamp(const int64_t &raw_ts); timestamp_ns_t ParquetTimestampNsToTimestampNs(const int64_t &raw_ns); date_t ParquetIntToDate(const int32_t &raw_date); +//! Whether a raw TIME value is within a day, given the number of units in a day +bool ParquetTimeIsValid(const int64_t &raw, const int64_t day); dtime_t ParquetMsIntToTime(const int32_t &raw_millis); dtime_t ParquetIntToTime(const int64_t &raw_micros); diff --git a/src/duckdb/extension/parquet/parquet_reader.cpp b/src/duckdb/extension/parquet/parquet_reader.cpp index c9e9af2fc..2b45188c3 100644 --- a/src/duckdb/extension/parquet/parquet_reader.cpp +++ b/src/duckdb/extension/parquet/parquet_reader.cpp @@ -527,6 +527,9 @@ LogicalType ParquetReader::DeriveLogicalType(const SchemaElement &s_ele, const P if (!s_ele.__isset.precision || !s_ele.__isset.scale) { throw IOException("DECIMAL requires a length and scale specifier!"); } + if (s_ele.precision < 1 || s_ele.scale < 0 || s_ele.scale > s_ele.precision) { + throw IOException("Invalid DECIMAL precision %d and scale %d", s_ele.precision, s_ele.scale); + } schema.type_scale = NumericCast(s_ele.scale); if (s_ele.precision > DecimalType::MaxWidth()) { schema.type_info = ParquetExtraTypeInfo::DECIMAL_BYTE_ARRAY; diff --git a/src/duckdb/extension/parquet/parquet_statistics.cpp b/src/duckdb/extension/parquet/parquet_statistics.cpp index 8770db274..2d4a8d062 100644 --- a/src/duckdb/extension/parquet/parquet_statistics.cpp +++ b/src/duckdb/extension/parquet/parquet_statistics.cpp @@ -126,6 +126,17 @@ Value ParquetStatisticsUtils::ConvertValue(const LogicalType &type, const Parque } return std::move(*result); } +//! Statistics values that do not fit in the width of the decimal are ignored +template +static Value ParquetDecimalStatsValue(T value, uint8_t width, uint8_t scale) { + const auto max_value = Hugeint::POWERS_OF_TEN[width]; + const auto hugeint_value = hugeint_t(value); + if (hugeint_value >= max_value || hugeint_value <= -max_value) { + return Value(); + } + return Value::DECIMAL(value, width, scale); +} + Value ParquetStatisticsUtils::ConvertValueInternal(const LogicalType &type, const ParquetColumnSchema &schema_ele, const std::string &stats) { auto stats_data = const_data_ptr_cast(stats.c_str()); @@ -200,25 +211,25 @@ Value ParquetStatisticsUtils::ConvertValueInternal(const LogicalType &type, cons if (stats.size() != sizeof(int32_t)) { throw InvalidInputException("Incorrect stats size for type %s", type.ToString()); } - return Value::DECIMAL(Load(stats_data), width, scale); + return ParquetDecimalStatsValue(Load(stats_data), width, scale); case ParquetExtraTypeInfo::DECIMAL_INT64: if (stats.size() != sizeof(int64_t)) { throw InvalidInputException("Incorrect stats size for type %s", type.ToString()); } - return Value::DECIMAL(Load(stats_data), width, scale); + return ParquetDecimalStatsValue(Load(stats_data), width, scale); case ParquetExtraTypeInfo::DECIMAL_BYTE_ARRAY: switch (type.InternalType()) { case PhysicalType::INT16: - return Value::DECIMAL( + return ParquetDecimalStatsValue( ParquetDecimalUtils::ReadDecimalValue(stats_data, stats.size(), schema_ele), width, scale); case PhysicalType::INT32: - return Value::DECIMAL( + return ParquetDecimalStatsValue( ParquetDecimalUtils::ReadDecimalValue(stats_data, stats.size(), schema_ele), width, scale); case PhysicalType::INT64: - return Value::DECIMAL( + return ParquetDecimalStatsValue( ParquetDecimalUtils::ReadDecimalValue(stats_data, stats.size(), schema_ele), width, scale); case PhysicalType::INT128: - return Value::DECIMAL( + return ParquetDecimalStatsValue( ParquetDecimalUtils::ReadDecimalValue(stats_data, stats.size(), schema_ele), width, scale); default: @@ -250,9 +261,15 @@ Value ParquetStatisticsUtils::ConvertValueInternal(const LogicalType &type, cons } switch (schema_ele.type_info) { case ParquetExtraTypeInfo::UNIT_MS: + if (!ParquetTimeIsValid(val, Interval::MSECS_PER_SEC * Interval::SECS_PER_DAY)) { + return Value(); + } return Value::TIME(Time::FromTimeMs(val)); case ParquetExtraTypeInfo::UNIT_MICROS: default: + if (!ParquetTimeIsValid(val, Interval::MICROS_PER_DAY)) { + return Value(); + } return Value::TIME(dtime_t(val)); } } @@ -263,7 +280,11 @@ Value ParquetStatisticsUtils::ConvertValueInternal(const LogicalType &type, cons if (schema_ele.type_info != ParquetExtraTypeInfo::UNIT_NS) { throw InternalException("TIME_NS requires nanosecond type info"); } - return Value::TIME_NS(ParquetIntToTimeNs(Load(stats_data))); + const auto nanos = Load(stats_data); + if (!ParquetTimeIsValid(nanos, Interval::NANOS_PER_DAY)) { + return Value(); + } + return Value::TIME_NS(ParquetIntToTimeNs(nanos)); } case LogicalTypeId::TIME_TZ: { int64_t val; diff --git a/src/duckdb/extension/parquet/parquet_timestamp.cpp b/src/duckdb/extension/parquet/parquet_timestamp.cpp index 5caba4dd8..70302136c 100644 --- a/src/duckdb/extension/parquet/parquet_timestamp.cpp +++ b/src/duckdb/extension/parquet/parquet_timestamp.cpp @@ -77,15 +77,29 @@ static T ParquetWrapTime(const T &raw, const T day) { return modulus + (modulus < 0) * day; } +bool ParquetTimeIsValid(const int64_t &raw, const int64_t day) { + return raw >= 0 && raw <= day; +} + +template +static void CheckParquetTime(const T &raw, const int64_t day) { + if (!ParquetTimeIsValid(raw, day)) { + throw InvalidInputException("Invalid TIME value %d in Parquet file - TIME values must be within a day", raw); + } +} + dtime_t ParquetMsIntToTime(const int32_t &raw_millis) { + CheckParquetTime(raw_millis, Interval::MSECS_PER_SEC * Interval::SECS_PER_DAY); return Time::FromTimeMs(raw_millis); } dtime_t ParquetIntToTime(const int64_t &raw_micros) { + CheckParquetTime(raw_micros, Interval::MICROS_PER_DAY); return dtime_t(raw_micros); } dtime_ns_t ParquetIntToTimeNs(const int64_t &raw_nanos) { + CheckParquetTime(raw_nanos, Interval::NANOS_PER_DAY); return dtime_ns_t(raw_nanos); } diff --git a/src/duckdb/extension/parquet/read_single_parquet_file.cpp b/src/duckdb/extension/parquet/read_single_parquet_file.cpp index 4f5c4e577..9447a8aaa 100644 --- a/src/duckdb/extension/parquet/read_single_parquet_file.cpp +++ b/src/duckdb/extension/parquet/read_single_parquet_file.cpp @@ -510,7 +510,7 @@ const ParquetOptions &ParquetScanFunction::GetFileOptions(const FunctionData &fi TableFunction ParquetScanFunction::GetSingleFileFunction() { TableFunction read_parquet("read_single_parquet_file", - FunctionSignature().AddPositionalOnly("path", LogicalType::VARCHAR), + FunctionSignature().AddPositionalOnly("file", LogicalType::ANY), ReadSingleParquetFileFunction, ReadSingleParquetFileBind, ReadSingleParquetFileInitGlobal, ReadSingleParquetFileInitLocal); read_parquet.table_scan_progress = ReadSingleParquetFileProgress; diff --git a/src/duckdb/generated_extension_loader_package_build.cpp b/src/duckdb/generated_extension_loader_package_build.cpp index 88776d2c9..d155c2d67 100644 --- a/src/duckdb/generated_extension_loader_package_build.cpp +++ b/src/duckdb/generated_extension_loader_package_build.cpp @@ -45,7 +45,7 @@ int32_t duckdb_extension_core_functions_describe(duckdb_extension_descriptor *de descriptor->version = 1; descriptor->name = "core_functions"; descriptor->extension_version = EXT_VERSION_CORE_FUNCTIONS; - descriptor->api_version = "v2.0.0-alpha45672"; + descriptor->api_version = "v2.0.0-alpha46057"; descriptor->entry_cpp = (void (*)(void))core_functions_duckdb_cpp_init; return 0; } @@ -78,7 +78,7 @@ int32_t duckdb_extension_parquet_describe(duckdb_extension_descriptor *descripto descriptor->version = 1; descriptor->name = "parquet"; descriptor->extension_version = EXT_VERSION_PARQUET; - descriptor->api_version = "v2.0.0-alpha45672"; + descriptor->api_version = "v2.0.0-alpha46057"; descriptor->entry_cpp = (void (*)(void))parquet_duckdb_cpp_init; return 0; } @@ -111,7 +111,7 @@ int32_t duckdb_extension_icu_describe(duckdb_extension_descriptor *descriptor) { descriptor->version = 1; descriptor->name = "icu"; descriptor->extension_version = EXT_VERSION_ICU; - descriptor->api_version = "v2.0.0-alpha45672"; + descriptor->api_version = "v2.0.0-alpha46057"; descriptor->entry_cpp = (void (*)(void))icu_duckdb_cpp_init; return 0; } @@ -144,7 +144,7 @@ int32_t duckdb_extension_json_describe(duckdb_extension_descriptor *descriptor) descriptor->version = 1; descriptor->name = "json"; descriptor->extension_version = EXT_VERSION_JSON; - descriptor->api_version = "v2.0.0-alpha45672"; + descriptor->api_version = "v2.0.0-alpha46057"; descriptor->entry_cpp = (void (*)(void))json_duckdb_cpp_init; return 0; } diff --git a/src/duckdb/src/catalog/catalog_entry/duck_schema_entry.cpp b/src/duckdb/src/catalog/catalog_entry/duck_schema_entry.cpp index e7ef5a234..f4b28ec39 100644 --- a/src/duckdb/src/catalog/catalog_entry/duck_schema_entry.cpp +++ b/src/duckdb/src/catalog/catalog_entry/duck_schema_entry.cpp @@ -281,13 +281,6 @@ optional_ptr DuckSchemaEntry::CreateIndex(CatalogTransaction trans // indexes do not require CASCADE to be dropped, they are simply always dropped along with the table info.dependencies.AddDependency(table, DependencyDependentFlags()); - // currently, we can not alter PK/FK/UNIQUE constraints - // concurrency-safe name checks against other INDEX catalog entries happens in the catalog - if (info.on_conflict != OnCreateConflict::IGNORE_ON_CONFLICT && - !table.GetStorage().IndexNameIsUnique(info.GetIndexName().GetIdentifierName())) { - throw CatalogException("An index with the name " + info.GetIndexName() + " already exists!"); - } - auto index = make_uniq(catalog, *this, info, table); auto dependencies = index->dependencies; return AddEntryInternal(transaction, std::move(index), info.on_conflict, dependencies); diff --git a/src/duckdb/src/catalog/catalog_entry/duck_table_entry.cpp b/src/duckdb/src/catalog/catalog_entry/duck_table_entry.cpp index 5d5336797..a8cd4fb87 100644 --- a/src/duckdb/src/catalog/catalog_entry/duck_table_entry.cpp +++ b/src/duckdb/src/catalog/catalog_entry/duck_table_entry.cpp @@ -1415,6 +1415,18 @@ unique_ptr DuckTableEntry::AddConstraint(ClientContext &context, A auto existing_name = existing_pk->ToString(); throw CatalogException("table %s can have only one primary key: %s", name, existing_name); } + + // Constraint index names are not unique, detect a duplicate constraint by its kind and columns. + for (const auto &constraint : GetConstraints()) { + if (constraint->type != ConstraintType::UNIQUE) { + continue; + } + auto &existing = constraint->Cast(); + if (existing.is_primary_key == unique.is_primary_key && + existing.GetLogicalIndexes(columns) == unique.GetLogicalIndexes(columns)) { + throw CatalogException("table %s already has the constraint %s", name, existing.ToString()); + } + } table_info.constraints.push_back(info.constraint->Copy()); } else { diff --git a/src/duckdb/src/common/adbc/adbc.cpp b/src/duckdb/src/common/adbc/adbc.cpp index c5a063553..715060399 100644 --- a/src/duckdb/src/common/adbc/adbc.cpp +++ b/src/duckdb/src/common/adbc/adbc.cpp @@ -5,8 +5,11 @@ #include "duckdb/common/string_util.hpp" #include "duckdb.h" +#include "duckdb/common/arrow/arrow_format.hpp" #include "duckdb/common/arrow/arrow_wrapper.hpp" #include "duckdb/common/arrow/nanoarrow/nanoarrow.hpp" +#include "duckdb/main/query_parameters.hpp" +#include "duckdb/main/query_result_stream.hpp" #include "duckdb/common/exception.hpp" #include "duckdb/main/connection.hpp" @@ -129,58 +132,117 @@ struct DuckDBAdbcStatementWrapper { uint64_t plan_length; }; -struct MaterializedData { - ArrowArray *batches; - idx_t count; - idx_t current; -}; - struct DuckDBAdbcStreamWrapper { - duckdb_result result; - char *last_error; - AdbcStatusCode status_code; - AdbcError adbc_error; - MaterializedData *materialized; - duckdb::DuckDBAdbcConnectionWrapper *conn_wrapper; + ~DuckDBAdbcStreamWrapper() { + free(last_error); + InitializeADBCError(&adbc_error); + } + + //! Drained into `materialized` and reset when another statement runs on the connection + duckdb::unique_ptr> stream; + //! A result that completed at submission, which no later statement on the connection affects + duckdb::unique_ptr result; + duckdb::ArrowSchemaWrapper schema; + //! What MaterializeStreams drained, handed out before the error it ran into, if any + duckdb::vector> materialized; + idx_t materialized_index = 0; + char *last_error = nullptr; + AdbcStatusCode status_code = ADBC_STATUS_OK; + AdbcError adbc_error = {}; + duckdb::DuckDBAdbcConnectionWrapper *conn_wrapper = nullptr; }; -class DuckDBAdbcStreamWrapperGuard { -public: - explicit DuckDBAdbcStreamWrapperGuard(DuckDBAdbcStreamWrapper *ptr_p) : ptr(ptr_p) { +static bool IsCancellation(duckdb::optional_ptr conn_wrapper, + duckdb::ExceptionType type) { + return type == duckdb::ExceptionType::INTERRUPT && conn_wrapper && conn_wrapper->cancel_requested; +} + +static void SetStreamError(DuckDBAdbcStreamWrapper &wrapper, const duckdb::ErrorData &error) { + free(wrapper.last_error); + wrapper.last_error = strdup(error.Message().c_str()); + wrapper.status_code = + IsCancellation(wrapper.conn_wrapper, error.Type()) ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INTERNAL; + wrapper.adbc_error.message = wrapper.last_error; + wrapper.adbc_error.vendor_code = ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA; + delete static_cast(wrapper.adbc_error.private_data); + wrapper.adbc_error.private_data = nullptr; + wrapper.adbc_error.release = nullptr; + auto *details = new (std::nothrow) DuckDBErrorDetails(); + if (details) { + details->entries.emplace_back("duckdb:error_type", DuckDBErrorTypeToString(duckdb::ErrorTypeToC(error.Type()))); + wrapper.adbc_error.private_data = details; + wrapper.adbc_error.release = ::ReleaseStreamErrorDetails; } - DuckDBAdbcStreamWrapperGuard(const DuckDBAdbcStreamWrapperGuard &) = delete; - DuckDBAdbcStreamWrapperGuard &operator=(const DuckDBAdbcStreamWrapperGuard &) = delete; +} - ~DuckDBAdbcStreamWrapperGuard() { - if (ptr) { - duckdb_destroy_result(&ptr->result); - free(ptr); +//! The next array, null at the end of the result and on an error, which goes to `error` +static duckdb::unique_ptr FetchArray(DuckDBAdbcStreamWrapper &wrapper, + duckdb::ErrorData &error) { + try { + if (wrapper.stream) { + auto array = wrapper.stream->Fetch(); + if (!array && wrapper.stream->HasError()) { + error = wrapper.stream->GetErrorObject(); + } + return array; } + if (wrapper.result) { + return wrapper.result->Fetch(); + } + } catch (std::exception &ex) { + error = duckdb::ErrorData(ex); + } catch (...) { + error = duckdb::ErrorData("Unknown error in Fetch"); } + return nullptr; +} - DuckDBAdbcStreamWrapper *release() { - auto tmp = ptr; - ptr = nullptr; - return tmp; - } - - DuckDBAdbcStreamWrapper *get() const { - return ptr; +static void CopySchema(const ArrowSchema &source, duckdb::ArrowSchemaWrapper &target) { + if (target.arrow_schema.release) { + target.arrow_schema.release(&target.arrow_schema); } - - DuckDBAdbcStreamWrapper *operator->() const { - return ptr; + if (duckdb_nanoarrow::ArrowSchemaDeepCopy(&source, &target.arrow_schema) != NANOARROW_OK) { + throw duckdb::OutOfMemoryException("Failed to copy the Arrow schema of the result"); } +} -private: - DuckDBAdbcStreamWrapper *ptr; -}; - -static bool IsInterruptError(const char *message) { - if (!message) { - return false; +//! A result that cannot stream completes here, so that the next statement on the connection leaves it intact. +//! rows_affected stays -1, which ADBC defines as unknown, unless the statement reports a changed-row count +static AdbcStatusCode ExecuteArrow(duckdb::PreparedStatementWrapper &prepared, DuckDBAdbcStreamWrapper &out, + int64_t &rows_affected, struct AdbcError *error) { + out.stream.reset(); + out.result.reset(); + rows_affected = -1; + duckdb::ErrorData error_data; + try { + duckdb::QueryParameters parameters(duckdb::make_shared_ptr(STANDARD_VECTOR_SIZE)); + auto result = prepared.statement->Submit(prepared.values, parameters); + if (!result->HasError() && + result->GetStatementProperties().result_eagerness != duckdb::ResultEagerness::FORCED) { + out.stream = duckdb::make_uniq>(std::move(result)); + CopySchema(out.stream->FormatState().Schema(), out.schema); + return ADBC_STATUS_OK; + } + result->Complete(); + if (!result->HasError()) { + CopySchema(result->FormatState().Schema(), out.schema); + if (result->GetStatementProperties().return_type == duckdb::StatementReturnType::CHANGED_ROWS) { + auto &arrays = result->Collection(); + auto rows_changed = arrays.empty() ? 0 : duckdb::ArrowFormat::ChangedRows(arrays.front()->arrow_array); + rows_affected = static_cast(rows_changed); + } + out.result = std::move(result); + return ADBC_STATUS_OK; + } + error_data = result->GetErrorObject(); + } catch (std::exception &ex) { + error_data = duckdb::ErrorData(ex); + } catch (...) { + error_data = duckdb::ErrorData("Unknown error in Execute"); } - return std::strcmp(message, duckdb::InterruptException::INTERRUPT_MESSAGE) == 0; + SetError(error, error_data.Message()); + AppendDuckDBErrorDetails(error, duckdb::ErrorTypeToC(error_data.Type())); + return IsCancellation(out.conn_wrapper, error_data.Type()) ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INVALID_ARGUMENT; } static AdbcStatusCode QueryInternal(struct AdbcConnection *connection, struct ArrowArrayStream *out, const char *query, @@ -839,6 +901,7 @@ AdbcStatusCode ConnectionCancel(struct AdbcConnection *connection, struct AdbcEr SetError(error, "Connection is not initialized"); return ADBC_STATUS_INVALID_STATE; } + conn_wrapper->cancel_requested = true; duckdb_interrupt(conn_wrapper->connection); return ADBC_STATUS_OK; } @@ -1406,29 +1469,8 @@ static int get_schema(struct ArrowArrayStream *stream, struct ArrowSchema *out) if (!stream || !stream->private_data || !out) { return DuckDBError; } - auto result_wrapper = static_cast(stream->private_data); - auto count = duckdb_column_count(&result_wrapper->result); - std::vector types(count); - - std::vector owned_names; - owned_names.reserve(count); - duckdb::vector names(count); - for (idx_t i = 0; i < count; i++) { - types[i] = duckdb_column_logical_type(&result_wrapper->result, i); - auto column_name = duckdb_column_name(&result_wrapper->result, i); - owned_names.emplace_back(column_name); - names[i] = owned_names.back().c_str(); - } - - auto arrow_options = duckdb_result_get_arrow_options(&result_wrapper->result); - - auto res = duckdb_to_arrow_schema(arrow_options, types.data(), names.data(), count, out); - duckdb_destroy_arrow_options(&arrow_options); - for (auto &type : types) { - duckdb_destroy_logical_type(&type); - } - if (res) { - duckdb_destroy_error_data(&res); + auto &result_wrapper = *static_cast(stream->private_data); + if (duckdb_nanoarrow::ArrowSchemaDeepCopy(&result_wrapper.schema.arrow_schema, out) != NANOARROW_OK) { return DuckDBError; } return DuckDBSuccess; @@ -1439,88 +1481,24 @@ static int get_next(struct ArrowArrayStream *stream, struct ArrowArray *out) { return DuckDBError; } out->release = nullptr; - auto result_wrapper = static_cast(stream->private_data); - - // If the stream has been materialized, return from stored batches - if (result_wrapper->materialized) { - auto mat = result_wrapper->materialized; - if (mat->current >= mat->count) { - // Surface any error that was encountered during materialization - if (result_wrapper->last_error) { - return DuckDBError; - } - return DuckDBSuccess; // end of stream - } - // Transfer ownership of the batch to the caller - *out = mat->batches[mat->current]; - mat->batches[mat->current].release = nullptr; - mat->current++; + auto &result_wrapper = *static_cast(stream->private_data); + if (result_wrapper.materialized_index < result_wrapper.materialized.size()) { + auto array = std::move(result_wrapper.materialized[result_wrapper.materialized_index++]); + array->MoveTo(*out); return DuckDBSuccess; } - - auto duckdb_chunk = duckdb_fetch_chunk(result_wrapper->result); - if (!duckdb_chunk) { - // End of stream or error; distinguish by checking the result error message. - auto err = duckdb_result_error(&result_wrapper->result); - if (err && err[0] != '\0') { - if (result_wrapper->last_error) { - free(result_wrapper->last_error); - } - result_wrapper->last_error = strdup(err); - result_wrapper->status_code = IsInterruptError(err) ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INTERNAL; - // Populate adbc_error for AdbcErrorFromArrayStream with rich metadata - result_wrapper->adbc_error.message = result_wrapper->last_error; - result_wrapper->adbc_error.vendor_code = ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA; - if (result_wrapper->adbc_error.private_data) { - delete static_cast(result_wrapper->adbc_error.private_data); - result_wrapper->adbc_error.private_data = nullptr; - } - auto *details = new (std::nothrow) DuckDBErrorDetails(); - if (details) { - details->entries.emplace_back( - "duckdb:error_type", DuckDBErrorTypeToString(duckdb_result_error_type(&result_wrapper->result))); - result_wrapper->adbc_error.private_data = details; - result_wrapper->adbc_error.release = ::ReleaseStreamErrorDetails; - } else { - result_wrapper->adbc_error.release = nullptr; - } - return DuckDBError; - } - return DuckDBSuccess; + if (result_wrapper.last_error) { + return DuckDBError; } - auto arrow_options = duckdb_result_get_arrow_options(&result_wrapper->result); - - auto conversion_success = duckdb_data_chunk_to_arrow(arrow_options, duckdb_chunk, out); - duckdb_destroy_arrow_options(&arrow_options); - duckdb_destroy_data_chunk(&duckdb_chunk); - - if (conversion_success) { - auto conv_err_msg = duckdb_error_data_message(conversion_success); - if (conv_err_msg && conv_err_msg[0] != '\0') { - if (result_wrapper->last_error) { - free(result_wrapper->last_error); - } - result_wrapper->last_error = strdup(conv_err_msg); - result_wrapper->status_code = ADBC_STATUS_INTERNAL; - result_wrapper->adbc_error.message = result_wrapper->last_error; - result_wrapper->adbc_error.vendor_code = ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA; - if (result_wrapper->adbc_error.private_data) { - delete static_cast(result_wrapper->adbc_error.private_data); - result_wrapper->adbc_error.private_data = nullptr; - } - auto *details = new (std::nothrow) DuckDBErrorDetails(); - if (details) { - details->entries.emplace_back( - "duckdb:error_type", DuckDBErrorTypeToString(duckdb_error_data_error_type(conversion_success))); - result_wrapper->adbc_error.private_data = details; - result_wrapper->adbc_error.release = ::ReleaseStreamErrorDetails; - } else { - result_wrapper->adbc_error.release = nullptr; - } - } - duckdb_destroy_error_data(&conversion_success); + duckdb::ErrorData error; + auto array = FetchArray(result_wrapper, error); + if (error.HasError()) { + SetStreamError(result_wrapper, error); return DuckDBError; } + if (array) { + array->MoveTo(*out); + } return DuckDBSuccess; } @@ -1528,33 +1506,13 @@ void release(struct ArrowArrayStream *stream) { if (!stream || !stream->release) { return; } - auto result_wrapper = reinterpret_cast(stream->private_data); + auto result_wrapper = static_cast(stream->private_data); if (result_wrapper) { - // Unregister from connection's active streams if (result_wrapper->conn_wrapper) { result_wrapper->conn_wrapper->UnregisterStream(result_wrapper); } - // Clean up materialized data if present - if (result_wrapper->materialized) { - auto mat = result_wrapper->materialized; - for (idx_t i = mat->current; i < mat->count; i++) { - if (mat->batches[i].release) { - mat->batches[i].release(&mat->batches[i]); - } - } - free(mat->batches); - free(mat); - result_wrapper->materialized = nullptr; - } - duckdb_destroy_result(&result_wrapper->result); - if (result_wrapper->last_error) { - free(result_wrapper->last_error); - result_wrapper->last_error = nullptr; - } - // Release any error that was set on the stream wrapper - InitializeADBCError(&result_wrapper->adbc_error); + delete result_wrapper; } - free(stream->private_data); stream->private_data = nullptr; stream->release = nullptr; } @@ -1644,9 +1602,10 @@ static std::string BuildCreateTableSQL(const char *catalog, const char *schema, return create_table.str(); } -AdbcStatusCode Ingest(duckdb_connection connection, const char *catalog, const char *table_name, const char *schema, - struct ArrowArrayStream *input, struct AdbcError *error, IngestionMode ingestion_mode, - bool temporary, int64_t *rows_affected) { +AdbcStatusCode Ingest(duckdb_connection connection, + duckdb::optional_ptr conn_wrapper, const char *catalog, + const char *table_name, const char *schema, struct ArrowArrayStream *input, + struct AdbcError *error, IngestionMode ingestion_mode, bool temporary, int64_t *rows_affected) { if (!connection) { SetError(error, "Missing connection object"); return ADBC_STATUS_INVALID_ARGUMENT; @@ -1741,7 +1700,7 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *catalog, const c if (duckdb_query(connection, sql.c_str(), &result) == DuckDBError) { const char *error_msg = duckdb_result_error(&result); bool already_exists = error_msg && std::string(error_msg).find("already exists") != std::string::npos; - bool interrupted = IsInterruptError(error_msg); + bool interrupted = IsCancellation(conn_wrapper, duckdb::ErrorTypeFromC(duckdb_result_error_type(&result))); SetError(error, error_msg); AppendDuckDBErrorDetails(error, duckdb_result_error_type(&result)); duckdb_destroy_result(&result); @@ -1769,7 +1728,7 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *catalog, const c auto err = duckdb_result_error(&result); SetError(error, err); AppendDuckDBErrorDetails(error, duckdb_result_error_type(&result)); - bool interrupted = IsInterruptError(err); + bool interrupted = IsCancellation(conn_wrapper, duckdb::ErrorTypeFromC(duckdb_result_error_type(&result))); duckdb_destroy_result(&result); return interrupted ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INTERNAL; } @@ -1784,7 +1743,7 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *catalog, const c auto err = duckdb_result_error(&result); SetError(error, err); AppendDuckDBErrorDetails(error, duckdb_result_error_type(&result)); - bool interrupted = IsInterruptError(err); + bool interrupted = IsCancellation(conn_wrapper, duckdb::ErrorTypeFromC(duckdb_result_error_type(&result))); duckdb_destroy_result(&result); return interrupted ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INTERNAL; } @@ -1802,6 +1761,20 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *catalog, const c } return ADBC_STATUS_INTERNAL; } + auto appender_error = [&]() { + auto error_data = duckdb_appender_error_data(appender.Get()); + auto err = duckdb_error_data_message(error_data); + if (err && err[0] != '\0') { + set_ingest_error(err); + } else { + SetError(error, missing_table_error); + } + bool interrupted = + IsCancellation(conn_wrapper, duckdb::ErrorTypeFromC(duckdb_error_data_error_type(error_data))); + AppendDuckDBErrorDetails(error, error_data); + duckdb_destroy_error_data(&error_data); + return interrupted ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INTERNAL; + }; duckdb::ArrowArrayWrapper arrow_array_wrapper; // Initialize rows_affected counter if requested @@ -1823,21 +1796,15 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *catalog, const c affected += static_cast(chunk->size()); } if (duckdb_append_data_chunk(appender.Get(), out_chunk.chunk) != DuckDBSuccess) { - auto error_data = duckdb_appender_error_data(appender.Get()); - auto err = duckdb_error_data_message(error_data); - if (err && err[0] != '\0') { - set_ingest_error(err); - } else { - SetError(error, missing_table_error); - } - bool interrupted = IsInterruptError(err); - AppendDuckDBErrorDetails(error, error_data); - duckdb_destroy_error_data(&error_data); - return interrupted ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INTERNAL; + return appender_error(); } arrow_array_wrapper = duckdb::ArrowArrayWrapper(); input->get_next(input, &arrow_array_wrapper.arrow_array); } + // The appender writes the rows it still holds when it closes, so a failure there must fail the ingestion + if (duckdb_appender_close(appender.Get()) != DuckDBSuccess) { + return appender_error(); + } if (rows_affected) { *rows_affected = affected; } @@ -1929,6 +1896,9 @@ AdbcStatusCode StatementCancel(struct AdbcStatement *statement, struct AdbcError SetError(error, "Invalid statement object"); return ADBC_STATUS_INVALID_ARGUMENT; } + if (wrapper->conn_wrapper) { + wrapper->conn_wrapper->cancel_requested = true; + } duckdb_interrupt(wrapper->connection); return ADBC_STATUS_OK; } @@ -2058,9 +2028,9 @@ static AdbcStatusCode IngestToTableFromBoundStream(DuckDBAdbcStatementWrapper *s auto stream = statement->ingestion_stream; // Ingest into a table from the bound stream - return Ingest(statement->connection, statement->target_catalog, statement->ingestion_table_name, - statement->db_schema, &stream, error, statement->ingestion_mode, statement->temporary_table, - rows_affected); + return Ingest(statement->connection, statement->conn_wrapper, statement->target_catalog, + statement->ingestion_table_name, statement->db_schema, &stream, error, statement->ingestion_mode, + statement->temporary_table, rows_affected); } AdbcStatusCode StatementExecuteQuery(struct AdbcStatement *statement, struct ArrowArrayStream *out, @@ -2084,6 +2054,7 @@ AdbcStatusCode StatementExecuteQuery(struct AdbcStatement *statement, struct Arr // same connection. if (wrapper->conn_wrapper) { wrapper->conn_wrapper->MaterializeStreams(); + wrapper->conn_wrapper->cancel_requested = false; } // TODO: Set affected rows, careful with early return @@ -2113,21 +2084,16 @@ AdbcStatusCode StatementExecuteQuery(struct AdbcStatement *statement, struct Arr return ADBC_STATUS_OK; } - auto *raw_stream_wrapper = static_cast(malloc(sizeof(DuckDBAdbcStreamWrapper))); - if (!raw_stream_wrapper) { + duckdb::unique_ptr stream_wrapper(new (std::nothrow) DuckDBAdbcStreamWrapper()); + if (!stream_wrapper) { SetError(error, "Allocation error"); return ADBC_STATUS_INVALID_ARGUMENT; } - raw_stream_wrapper->last_error = nullptr; - raw_stream_wrapper->status_code = ADBC_STATUS_OK; - raw_stream_wrapper->materialized = nullptr; - raw_stream_wrapper->conn_wrapper = wrapper->conn_wrapper; - std::memset(&raw_stream_wrapper->adbc_error, 0, sizeof(raw_stream_wrapper->adbc_error)); - std::memset(&raw_stream_wrapper->result, 0, sizeof(raw_stream_wrapper->result)); - DuckDBAdbcStreamWrapperGuard stream_wrapper(raw_stream_wrapper); + stream_wrapper->conn_wrapper = wrapper->conn_wrapper; + auto &prepared = *reinterpret_cast(wrapper->statement); + int64_t affected = -1; // Only process the stream if there are parameters to bind - auto prepared_statement_params = - reinterpret_cast(wrapper->statement)->statement->GetParameterCount(); + auto prepared_statement_params = prepared.statement->GetParameterCount(); if (has_stream && prepared_statement_params > 0) { // A stream was bound to the statement, use that to bind parameters ArrowArrayStream stream = wrapper->ingestion_stream; @@ -2190,44 +2156,28 @@ AdbcStatusCode StatementExecuteQuery(struct AdbcStatement *statement, struct Arr return ADBC_STATUS_INVALID_ARGUMENT; } } - // Destroy any previous result before overwriting to avoid leaks - duckdb_destroy_result(&stream_wrapper->result); - auto res = duckdb_execute_prepared_streaming(wrapper->statement, &stream_wrapper->result); - if (res != DuckDBSuccess) { - auto err = duckdb_result_error(&stream_wrapper->result); - SetError(error, err); - AppendDuckDBErrorDetails(error, duckdb_result_error_type(&stream_wrapper->result)); - bool interrupted = IsInterruptError(err); - return interrupted ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INVALID_ARGUMENT; + auto status = ExecuteArrow(prepared, *stream_wrapper, affected, error); + if (status != ADBC_STATUS_OK) { + return status; } // Recreate wrappers for next iteration arrow_array_wrapper = duckdb::ArrowArrayWrapper(); stream.get_next(&stream, &arrow_array_wrapper.arrow_array); } + if (!stream_wrapper->stream && !stream_wrapper->result) { + SetError(error, "Please provide a non-empty chunk to be bound"); + return ADBC_STATUS_INVALID_ARGUMENT; + } } else { - auto res = duckdb_execute_prepared_streaming(wrapper->statement, &stream_wrapper->result); - if (res != DuckDBSuccess) { - auto err = duckdb_result_error(&stream_wrapper->result); - SetError(error, err); - AppendDuckDBErrorDetails(error, duckdb_result_error_type(&stream_wrapper->result)); - bool interrupted = IsInterruptError(err); - return interrupted ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INVALID_ARGUMENT; + auto status = ExecuteArrow(prepared, *stream_wrapper, affected, error); + if (status != ADBC_STATUS_OK) { + return status; } } // Set rows_affected for queries (if not already set by ingestion path) if (rows_affected && !(has_stream && to_table)) { - // For DML queries (INSERT/UPDATE/DELETE), duckdb_rows_changed() returns the count - // For SELECT queries, duckdb_rows_changed() returns 0 - auto rows_changed = duckdb_rows_changed(&stream_wrapper->result); - if (rows_changed > 0) { - // This was a DML query - *rows_affected = static_cast(rows_changed); - } else { - // This is a SELECT or other query that returns a result set - // Return -1 to indicate unknown, as results are streamed - *rows_affected = -1; - } + *rows_affected = affected; } if (out) { @@ -3488,119 +3438,27 @@ void duckdb::DuckDBAdbcConnectionWrapper::UnregisterStream(duckdb_adbc::DuckDBAd void duckdb::DuckDBAdbcConnectionWrapper::MaterializeStreams() { const duckdb::lock_guard guard(stream_mutex); for (auto *result_wrapper : active_streams) { - if (!result_wrapper || result_wrapper->materialized) { + if (!result_wrapper || !result_wrapper->stream) { continue; } - - // Collect remaining batches from the streaming result. Errors encountered mid-stream - // are stored on result_wrapper so that get_next can return buffered batches first - // and then surface the error once they are exhausted. - duckdb::vector batches; - auto arrow_options = duckdb_result_get_arrow_options(&result_wrapper->result); - while (true) { - ArrowArray array; - std::memset(&array, 0, sizeof(ArrowArray)); - - auto duckdb_chunk = duckdb_fetch_chunk(result_wrapper->result); - if (!duckdb_chunk) { - // End of stream or error; distinguish by checking the result error message. - auto err = duckdb_result_error(&result_wrapper->result); - if (err && err[0] != '\0') { - if (result_wrapper->last_error) { - free(result_wrapper->last_error); - } - result_wrapper->last_error = strdup(err); - result_wrapper->status_code = - duckdb_adbc::IsInterruptError(err) ? ADBC_STATUS_CANCELLED : ADBC_STATUS_INTERNAL; - result_wrapper->adbc_error.message = result_wrapper->last_error; - result_wrapper->adbc_error.vendor_code = ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA; - if (result_wrapper->adbc_error.private_data) { - delete static_cast(result_wrapper->adbc_error.private_data); - result_wrapper->adbc_error.private_data = nullptr; - } - auto *details = new (std::nothrow) DuckDBErrorDetails(); - if (details) { - details->entries.emplace_back( - "duckdb:error_type", - DuckDBErrorTypeToString(duckdb_result_error_type(&result_wrapper->result))); - result_wrapper->adbc_error.private_data = details; - result_wrapper->adbc_error.release = ::ReleaseStreamErrorDetails; - } else { - result_wrapper->adbc_error.release = nullptr; - } - } - break; - } - auto conversion_err = duckdb_data_chunk_to_arrow(arrow_options, duckdb_chunk, &array); - duckdb_destroy_data_chunk(&duckdb_chunk); - - if (conversion_err) { - // Store error before freeing so get_next can surface it after buffered batches - auto conv_err_msg = duckdb_error_data_message(conversion_err); - if (conv_err_msg && conv_err_msg[0] != '\0') { - if (result_wrapper->last_error) { - free(result_wrapper->last_error); - } - result_wrapper->last_error = strdup(conv_err_msg); - result_wrapper->status_code = ADBC_STATUS_INTERNAL; - result_wrapper->adbc_error.message = result_wrapper->last_error; - result_wrapper->adbc_error.vendor_code = ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA; - if (result_wrapper->adbc_error.private_data) { - delete static_cast(result_wrapper->adbc_error.private_data); - result_wrapper->adbc_error.private_data = nullptr; - } - auto *details = new (std::nothrow) DuckDBErrorDetails(); - if (details) { - details->entries.emplace_back( - "duckdb:error_type", DuckDBErrorTypeToString(duckdb_error_data_error_type(conversion_err))); - result_wrapper->adbc_error.private_data = details; - result_wrapper->adbc_error.release = ::ReleaseStreamErrorDetails; - } else { - result_wrapper->adbc_error.release = nullptr; - } - } - duckdb_destroy_error_data(&conversion_err); - if (array.release) { - array.release(&array); - } - break; - } - batches.push_back(array); - } - duckdb_destroy_arrow_options(&arrow_options); - - // Store materialized data - auto mat = static_cast(malloc(sizeof(duckdb_adbc::MaterializedData))); - if (!mat) { - // Allocation failed — release fetched batches and skip materialization - for (auto &batch : batches) { - if (batch.release) { - batch.release(&batch); + // An error found while draining is reported once the arrays read before it are handed out + try { + while (!result_wrapper->last_error) { + duckdb::ErrorData error; + auto array = duckdb_adbc::FetchArray(*result_wrapper, error); + if (error.HasError()) { + duckdb_adbc::SetStreamError(*result_wrapper, error); + break; } - } - continue; - } - mat->current = 0; - mat->count = static_cast(batches.size()); - if (!batches.empty()) { - mat->batches = static_cast(malloc(sizeof(ArrowArray) * batches.size())); - if (!mat->batches) { - // Allocation failed — release fetched batches and skip materialization - for (auto &batch : batches) { - if (batch.release) { - batch.release(&batch); - } + if (!array) { + break; } - free(mat); - continue; - } - for (idx_t i = 0; i < batches.size(); i++) { - mat->batches[i] = batches[i]; + result_wrapper->materialized.push_back(std::move(array)); } - } else { - mat->batches = nullptr; + } catch (std::exception &ex) { + duckdb_adbc::SetStreamError(*result_wrapper, duckdb::ErrorData(ex)); } - result_wrapper->materialized = mat; + result_wrapper->stream.reset(); } } diff --git a/src/duckdb/src/common/arrow/arrow_format.cpp b/src/duckdb/src/common/arrow/arrow_format.cpp index 83da3ebdf..e7afb634b 100644 --- a/src/duckdb/src/common/arrow/arrow_format.cpp +++ b/src/duckdb/src/common/arrow/arrow_format.cpp @@ -199,6 +199,16 @@ unique_ptr ArrowFormat::ShareArray(const ArrowArrayOwner &own return result; } +idx_t ArrowFormat::ChangedRows(const ArrowArray &array) { + if (array.length == 0) { + return 0; + } + D_ASSERT(array.length == 1 && array.n_children == 1); + auto &count = *array.children[0]; + auto values = static_cast(count.buffers[1]); + return NumericCast(values[array.offset + count.offset]); +} + //===--------------------------------------------------------------------===// // ArrowRetainedCollection //===--------------------------------------------------------------------===// diff --git a/src/duckdb/src/common/csv_writer.cpp b/src/duckdb/src/common/csv_writer.cpp index 3d1986da6..7ca1f8804 100644 --- a/src/duckdb/src/common/csv_writer.cpp +++ b/src/duckdb/src/common/csv_writer.cpp @@ -39,10 +39,11 @@ CSVWriterOptions::CSVWriterOptions(const string &delim, const char "e, const requires_quotes['\r'] = true; // quote values containing the comment char so they are not read back as comments (see #17744) if (comment_char != '\0') { - requires_quotes[NumericCast(comment_char)] = true; + requires_quotes[static_cast(comment_char)] = true; } - requires_quotes[NumericCast(delim[0])] = true; - requires_quotes[NumericCast(quote)] = true; + // values containing the (first byte of the) delimiter are quoted + requires_quotes[static_cast(delim[0])] = true; + requires_quotes[static_cast(quote)] = true; if (!write_newline.empty()) { newline = TransformNewLine(write_newline); @@ -371,8 +372,9 @@ void CSVWriter::WriteChunk(DataChunk &input, MemoryStream &writer, CSVReaderOpti D_ASSERT(options.null_str.size() == 1); for (idx_t col_idx = 0; col_idx < input.ColumnCount(); col_idx++) { if (col_idx != 0) { - CSVWriter::WriteQuoteOrEscape(writer, - options.dialect_options.state_machine_options.delimiter.GetValue()[0]); + // the delimiter can consist of multiple bytes + auto &delimiter = options.dialect_options.state_machine_options.delimiter.GetValue(); + writer.WriteData(const_data_ptr_cast(delimiter.c_str()), delimiter.size()); } auto input_val = input_iterators[col_idx][row_idx]; if (!input_val.IsValid()) { @@ -398,7 +400,8 @@ void CSVWriter::WriteChunk(DataChunk &input, MemoryStream &writer, CSVReaderOpti void CSVWriter::WriteHeader(MemoryStream &stream, CSVReaderOptions &options, CSVWriterOptions &writer_options) { for (idx_t i = 0; i < options.name_list.size(); i++) { if (i != 0) { - WriteQuoteOrEscape(stream, options.dialect_options.state_machine_options.delimiter.GetValue()[0]); + auto &delimiter = options.dialect_options.state_machine_options.delimiter.GetValue(); + stream.WriteData(const_data_ptr_cast(delimiter.c_str()), delimiter.size()); } WriteQuotedString(stream, options.name_list[i].c_str(), options.name_list[i].size(), i, options, diff --git a/src/duckdb/src/common/enum_util.cpp b/src/duckdb/src/common/enum_util.cpp index 703b23d2a..0934e0e89 100644 --- a/src/duckdb/src/common/enum_util.cpp +++ b/src/duckdb/src/common/enum_util.cpp @@ -2468,6 +2468,25 @@ ExtensionUpdateResultTag EnumUtil::FromString(const ch return static_cast(StringUtil::StringToEnum(GetExtensionUpdateResultTagValues(), 8, "ExtensionUpdateResultTag", value)); } +const StringUtil::EnumStringLiteral *GetExternalFileCacheRequestSizingValues() { + static constexpr StringUtil::EnumStringLiteral values[] { + { static_cast(ExternalFileCacheRequestSizing::AUTO), "AUTO" }, + { static_cast(ExternalFileCacheRequestSizing::GRID), "GRID" }, + { static_cast(ExternalFileCacheRequestSizing::EXACT), "EXACT" } + }; + return values; +} + +template<> +const char* EnumUtil::ToChars(ExternalFileCacheRequestSizing value) { + return StringUtil::EnumToString(GetExternalFileCacheRequestSizingValues(), 3, "ExternalFileCacheRequestSizing", static_cast(value)); +} + +template<> +ExternalFileCacheRequestSizing EnumUtil::FromString(const char *value) { + return static_cast(StringUtil::StringToEnum(GetExternalFileCacheRequestSizingValues(), 3, "ExternalFileCacheRequestSizing", value)); +} + const StringUtil::EnumStringLiteral *GetExternalResourceOperationValues() { static constexpr StringUtil::EnumStringLiteral values[] { { static_cast(ExternalResourceOperation::CREATE), "CREATE" }, diff --git a/src/duckdb/src/common/multi_file/multi_file_column_mapper.cpp b/src/duckdb/src/common/multi_file/multi_file_column_mapper.cpp index b3b0c13ee..3dbd80db0 100644 --- a/src/duckdb/src/common/multi_file/multi_file_column_mapper.cpp +++ b/src/duckdb/src/common/multi_file/multi_file_column_mapper.cpp @@ -325,7 +325,7 @@ static ColumnMapResult MapColumnList(ClientContext &context, const MultiFileColu } result.column_index = make_uniq(local_id.GetIndex(), std::move(child_indexes)); if (global_index.HasType()) { - result.column_index->SetType(global_column.type); + result.column_index->SetType(local_column.type); } result.mapping = std::move(mapping); return result; @@ -452,7 +452,7 @@ static ColumnMapResult MapColumnMap(ClientContext &context, const MultiFileColum result.column_index = make_uniq(local_id.GetIndex(), std::move(map_indexes)); if (global_index.HasType()) { - result.column_index->SetType(global_column.type); + result.column_index->SetType(local_column.type); } result.mapping = std::move(mapping); return result; @@ -479,7 +479,7 @@ static ColumnMapResult MapColumnStruct(ClientContext &context, const MultiFileCo vector single_child; single_child.push_back(std::move(*child_mapping.column_index)); child_mapping.column_index = make_uniq(local_id.GetIndex(), single_child); - child_mapping.column_index->SetType(global_column.type); + child_mapping.column_index->SetType(local_column.type); child_mapping.column_index->SetPushdownExtract(); mapping->child_mapping.emplace(MultiFileGlobalIndex(0), std::move(child_mapping.mapping)); @@ -557,7 +557,7 @@ static ColumnMapResult MapColumnStruct(ClientContext &context, const MultiFileCo } result.column_index = make_uniq(local_id.GetIndex(), std::move(child_indexes)); if (global_index.HasType()) { - result.column_index->SetType(global_column.type); + result.column_index->SetType(local_column.type); } result.mapping = std::move(mapping); return result; @@ -627,6 +627,11 @@ static unique_ptr ConstructMapExpression(ClientContext &context, Mul ColumnMapResult &mapping, const LogicalType &global_column_type, const LogicalType &local_column_type, bool is_trivially_mappable) { unique_ptr expr = make_uniq(local_column_type, local_idx.GetIndex()); + if (mapping.column_map.type().id() == LogicalTypeId::TUPLE) { + // A pushed-down extract already selects the source column; retain only its child mapping. + auto child_mapping = StructValue::GetChildren(mapping.column_map)[1]; + mapping.column_map = std::move(child_mapping); + } const bool can_use_remap_struct = global_column_type.IsNested() && (mapping.column_map.IsNull() || mapping.column_map.type().id() == LogicalTypeId::STRUCT) && diff --git a/src/duckdb/src/common/multi_file/multi_file_reader.cpp b/src/duckdb/src/common/multi_file/multi_file_reader.cpp index 0f102bf5e..0799f025f 100644 --- a/src/duckdb/src/common/multi_file/multi_file_reader.cpp +++ b/src/duckdb/src/common/multi_file/multi_file_reader.cpp @@ -112,53 +112,64 @@ void MultiFileReader::AddParameters(TableFunction &table_function, MultiFilePara } } -OpenFileInfo MultiFileReader::ParseFileEntry(const Value &input) { - if (input.IsNull()) { - throw ParserException("%s reader cannot take NULL input as parameter", function_name); - } - if (input.type().id() == LogicalTypeId::VARCHAR) { - return OpenFileInfo(StringValue::Get(input)); - } - if (input.type().id() == LogicalTypeId::VARIANT) { - // a VARIANT lets every file carry its own set of open options - unpack it to its logical value - // a variant never unpacks to another variant, so this recurses at most once - return ParseFileEntry(VariantValue::GetValue(input)); - } - if (input.type().id() != LogicalTypeId::STRUCT) { - throw ParserException("%s reader can only take a list of strings, structs or variants as a parameter", - function_name); - } - // a file specified as a struct holds the path in the "filename" field - every other field is an open option - auto &child_types = StructType::GetChildTypes(input.type()); - auto &children = StructValue::GetChildren(input); - OpenFileInfo result; - auto extended_info = make_shared_ptr(); - bool found_path = false; - for (idx_t child_idx = 0; child_idx < children.size(); child_idx++) { - auto &name = child_types[child_idx].first; - auto &child = children[child_idx]; - if (name == MultiFileReader::FILE_PATH_FIELD) { - if (child.IsNull() || child.type().id() != LogicalTypeId::VARCHAR) { - throw ParserException("%s reader requires the \"%s\" field of a file struct to be a non-NULL VARCHAR", - function_name, MultiFileReader::FILE_PATH_FIELD); +MultiFileColumnDefinition MultiFileColumnDefinition::CreateNested(const Identifier &name, const LogicalType &type) { + MultiFileColumnDefinition result(name, type); + switch (type.id()) { + case LogicalTypeId::STRUCT: + for (auto &child : StructType::GetChildTypes(type)) { + result.children.push_back(CreateNested(child.first, child.second)); + } + break; + case LogicalTypeId::LIST: + result.children.push_back(CreateNested(Identifier("list"), ListType::GetChildType(type))); + break; + case LogicalTypeId::ARRAY: + result.children.push_back(CreateNested(Identifier("list"), ArrayType::GetChildType(type))); + break; + case LogicalTypeId::MAP: { + // the multi-file reader expects the entries of a MAP as a "key_value" STRUCT of the keys and the values + MultiFileColumnDefinition key_value(Identifier("key_value"), ListType::GetChildType(type)); + key_value.children.push_back(CreateNested(Identifier("key"), MapType::KeyType(type))); + key_value.children.push_back(CreateNested(Identifier("value"), MapType::ValueType(type))); + result.children.push_back(std::move(key_value)); + break; + } + case LogicalTypeId::UNION: + for (idx_t i = 0; i < UnionType::GetMemberCount(type); i++) { + result.children.push_back( + CreateNested(UnionType::GetMemberName(type, i), UnionType::GetMemberType(type, i))); + } + break; + default: + break; + } + return result; +} + +MultiFileColumnDefinition &MultiFileColumnDefinition::ResolveChildPath(const vector &child_path) { + reference current(*this); + for (auto child_index : child_path) { + auto &definition = current.get(); + if (definition.type.id() == LogicalTypeId::MAP) { + // the keys and the values are the children of the "key_value" entry + if (child_index > 1) { + throw InvalidInputException("A MAP has two children: its keys (0) and its values (1), not %llu", + child_index); } - result.path = StringValue::Get(child); - found_path = true; + current = definition.children[0].children[child_index]; continue; } - if (child.IsNull()) { - // a NULL option is an option that was not specified - a list of structs is typed by unifying the - // structs of its entries, which fills the options an entry did not specify with NULL - continue; + if (child_index >= definition.children.size()) { + throw InvalidInputException("Child index %llu is out of range for a field of type %s", child_index, + definition.type.ToString()); } - extended_info->options[name.GetIdentifierName()] = child; - } - if (!found_path) { - throw ParserException("%s reader requires a file struct to have a \"%s\" field holding the path of the file", - function_name, MultiFileReader::FILE_PATH_FIELD); + current = definition.children[child_index]; } - result.extended_info = std::move(extended_info); - return result; + return current.get(); +} + +OpenFileInfo MultiFileReader::ParseFileEntry(const Value &input) { + return OpenFileInfo::FromValue(input, function_name); } vector MultiFileReader::ParseFileList(const Value &input) { diff --git a/src/duckdb/src/common/multi_file/table_function_multi_file.cpp b/src/duckdb/src/common/multi_file/table_function_multi_file.cpp index 589c678f9..500215099 100644 --- a/src/duckdb/src/common/multi_file/table_function_multi_file.cpp +++ b/src/duckdb/src/common/multi_file/table_function_multi_file.cpp @@ -1,5 +1,7 @@ #include "duckdb/common/multi_file/table_function_multi_file.hpp" +#include + #include "duckdb/common/bind_helpers.hpp" #include "duckdb/execution/execution_context.hpp" @@ -73,8 +75,14 @@ void TableFunctionFileReader::BindFunction(ClientContext &context, const TableFu throw InternalException("Table function %s cannot be wrapped in a multi file function - it has no bind", function.name); } + // a function that takes the file as ANY gets it together with the options to open it with - one that takes a + // VARCHAR only gets its path vector inputs; - inputs.emplace_back(file.path); + if (function.GetSignature().GetParameter(0).GetType().id() == LogicalTypeId::ANY) { + inputs.emplace_back(file.ToValue()); + } else { + inputs.emplace_back(file.path); + } auto parameters = named_parameters; function.GetSignature().FillNamedDefaults(context, parameters); vector input_table_types; @@ -84,7 +92,6 @@ void TableFunctionFileReader::BindFunction(ClientContext &context, const TableFu TableFunctionBindInput bind_input(inputs, parameters, input_table_types, input_table_names, function.function_info.get(), nullptr, bound_function, empty_ref); TableFunctionFileBindInput file_input; - file_input.file = file; file_input.multi_file_options = file_options; file_input.multi_file_scan = options.multi_file_scan; file_input.schema_only = schema_only; @@ -96,25 +103,48 @@ void TableFunctionFileReader::BindFunction(ClientContext &context, const TableFu file_input.expected_bind_data = options.schema_bind_data.get(); } bind_input.multi_file_input = file_input; - TableFunctionFileBindInfo file_info; - bind_input.file_info = file_info; names.clear(); types.clear(); bind_data = function.bind(context, bind_input, types, names); + // a function can describe what it bound in more detail than its names and types - e.g. with the field ids of its + // columns, and options that describe the file, which are exposed as the metadata of the reader of the file + optional bind_info; + if (function.get_bind_info && bind_data) { + TableFunctionGetBindInfoInput bind_info_input(context, bind_data.get()); + bind_info = function.get_bind_info(bind_info_input); + } if (settings.get_file_columns && bind_data) { // the function describes the columns of its file itself - names and types alone would lose their nested // structure and the identifiers that map the files of a scan onto one another columns = settings.get_file_columns(context, *bind_data); - } else if (!file_info.columns.empty()) { - if (file_info.columns.size() != names.size()) { - throw InternalException("Table function %s described %llu columns of file \"%s\", but bound %llu", - function.name, file_info.columns.size(), file.path, names.size()); + } else if (bind_info && !bind_info->column_identifiers.empty()) { + columns.clear(); + for (idx_t i = 0; i < names.size(); i++) { + columns.push_back(MultiFileColumnDefinition::CreateNested(names[i], types[i])); + } + for (auto &entry : bind_info->column_identifiers) { + if (entry.column_index >= columns.size()) { + throw InternalException("Table function %s attached an identifier to column %llu of file \"%s\", but " + "bound %llu", + function.name, entry.column_index, file.path, columns.size()); + } + columns[entry.column_index].ResolveChildPath(entry.child_path).identifier = entry.identifier; } - columns = std::move(file_info.columns); } else { columns = MultiFileColumnDefinition::ColumnsFromNamesAndTypes(names, types); } - metadata = std::move(file_info.metadata); + metadata.clear(); + if (bind_info) { + // the options are kept unordered - sort them, so the same file always has the same metadata + vector keys; + for (auto &option : bind_info->options) { + keys.push_back(option.first); + } + std::sort(keys.begin(), keys.end()); + for (auto &key : keys) { + metadata.insert(key, bind_info->options[key]); + } + } cardinality = optional_idx(); if (function.cardinality) { @@ -771,9 +801,10 @@ TableFunction TableFunctionMultiFileWrapper::CreateFunction(TableFunction single table_function_bind_t bind) { auto &wrapped_signature = single_file_function.GetSignature(); if (wrapped_signature.GetPositionalParameterCount() != 1 || - wrapped_signature.GetParameter(0).GetType() != LogicalType::VARCHAR) { - throw InternalException("Only table functions taking a single VARCHAR file path can be wrapped in a multi " - "file function, %s does not", + (wrapped_signature.GetParameter(0).GetType() != LogicalType::VARCHAR && + wrapped_signature.GetParameter(0).GetType().id() != LogicalTypeId::ANY)) { + throw InternalException("Only table functions taking a single file - an ANY file or a VARCHAR file path - can " + "be wrapped in a multi file function, %s does not", single_file_function.name); } if (settings.reader_type.empty()) { diff --git a/src/duckdb/src/common/open_file_info.cpp b/src/duckdb/src/common/open_file_info.cpp index eb255c61b..d17aa1c64 100644 --- a/src/duckdb/src/common/open_file_info.cpp +++ b/src/duckdb/src/common/open_file_info.cpp @@ -1,9 +1,79 @@ #include "duckdb/common/open_file_info.hpp" #include "duckdb/common/exception.hpp" +#include "duckdb/common/exception/parser_exception.hpp" + +#include namespace duckdb { +OpenFileInfo OpenFileInfo::FromValue(const Value &input, const Identifier &function_name) { + if (input.IsNull()) { + throw ParserException("%s reader cannot take NULL input as parameter", function_name); + } + if (input.type().id() == LogicalTypeId::VARCHAR) { + return OpenFileInfo(StringValue::Get(input)); + } + if (input.type().id() == LogicalTypeId::VARIANT) { + // a VARIANT lets every file carry its own set of open options - unpack it to its logical value + // a variant never unpacks to another variant, so this recurses at most once + return FromValue(VariantValue::GetValue(input), function_name); + } + if (input.type().id() != LogicalTypeId::STRUCT) { + throw ParserException("%s reader can only take a list of strings, structs or variants as a parameter", + function_name); + } + // a file specified as a struct holds the path in the "filename" field - every other field is an open option + auto &child_types = StructType::GetChildTypes(input.type()); + auto &children = StructValue::GetChildren(input); + OpenFileInfo result; + auto extended_info = make_shared_ptr(); + bool found_path = false; + for (idx_t child_idx = 0; child_idx < children.size(); child_idx++) { + auto &name = child_types[child_idx].first; + auto &child = children[child_idx]; + if (name == PATH_FIELD) { + if (child.IsNull() || child.type().id() != LogicalTypeId::VARCHAR) { + throw ParserException("%s reader requires the \"%s\" field of a file struct to be a non-NULL VARCHAR", + function_name, PATH_FIELD); + } + result.path = StringValue::Get(child); + found_path = true; + continue; + } + if (child.IsNull()) { + // a NULL option is an option that was not specified - a list of structs is typed by unifying the + // structs of its entries, which fills the options an entry did not specify with NULL + continue; + } + extended_info->options[name.GetIdentifierName()] = child; + } + if (!found_path) { + throw ParserException("%s reader requires a file struct to have a \"%s\" field holding the path of the file", + function_name, PATH_FIELD); + } + result.extended_info = std::move(extended_info); + return result; +} + +Value OpenFileInfo::ToValue() const { + if (!extended_info || extended_info->options.empty()) { + return Value(path); + } + // the options are kept unordered - sort them, so the same file always becomes the same value + vector names; + for (auto &option : extended_info->options) { + names.push_back(option.first); + } + std::sort(names.begin(), names.end()); + child_list_t children; + children.emplace_back(PATH_FIELD, Value(path)); + for (auto &name : names) { + children.emplace_back(name, extended_info->options.at(name)); + } + return Value::STRUCT(std::move(children)); +} + template <> bool ExtendedOpenFileInfo::TryGetOption(const string &name, bool &result) const { auto entry = options.find(name); diff --git a/src/duckdb/src/common/operator/cast_operators.cpp b/src/duckdb/src/common/operator/cast_operators.cpp index 1de07b58f..1a78e8b0e 100644 --- a/src/duckdb/src/common/operator/cast_operators.cpp +++ b/src/duckdb/src/common/operator/cast_operators.cpp @@ -1618,12 +1618,15 @@ string_t CastFromBlob::Operation(string_t input, StringHeap &heap) { } template <> -string_t CastFromBlobToBit::Operation(string_t input, StringHeap &heap) { - idx_t result_size = input.GetSize() + 1; - if (result_size <= 1) { - throw ConversionException("Cannot cast empty BLOB to BIT"); +bool TryCastBlobToBit::Operation(string_t input, string_t &result, Vector &result_vector, CastParameters ¶meters) { + if (input.GetSize() == 0) { + HandleCastError::AssignError("Cannot cast empty BLOB to BIT", parameters); + return false; } - return heap.AddBlob(Bit::BlobToBit(input)); + result = StringVector::EmptyString(result_vector, input.GetSize() + 1); + Bit::BlobToBit(input, result); + result.Finalize(); + return true; } //===--------------------------------------------------------------------===// @@ -1981,7 +1984,11 @@ bool TryCast::Operation(string_t input, dtime_ns_t &result, bool strict) { if (!TryCast::Operation(micros, result)) { return false; } - return TryAddOperator::Operation(result.value, nanos, result.value); + if (!TryAddOperator::Operation(result.value, nanos, result.value)) { + return false; + } + // the nanoseconds can push e.g. 24:00:00 past the end of the day + return result.value <= Interval::NANOS_PER_DAY; } template <> @@ -3311,7 +3318,8 @@ static void FillDecimalDigits(hugeint_t input, duckdb_fast_float::decimal &decim negative = false; } - char buffer[DecimalWidth::max]; + // a hugeint can have one more digit than the maximum decimal width + char buffer[DecimalWidth::max + 1]; auto end = buffer + sizeof(buffer); auto begin = NumericHelper::FormatUnsigned(input, end); diff --git a/src/duckdb/src/common/string_util.cpp b/src/duckdb/src/common/string_util.cpp index 4c5b5c8f1..e560b5e65 100644 --- a/src/duckdb/src/common/string_util.cpp +++ b/src/duckdb/src/common/string_util.cpp @@ -942,6 +942,14 @@ string StringUtil::GetFilePath(const string &file_path) { return file_path.substr(0, pos + 1); } +string StringUtil::ReplaceFileName(const string &file_path, const string &file_name) { + auto pos = file_path.find_last_of("/\\"); + if (pos == string::npos) { + return file_name; + } + return file_path.substr(0, pos + 1) + file_name; +} + struct URLEncodeLength { using RESULT_TYPE = idx_t; diff --git a/src/duckdb/src/common/types/column/column_data_collection.cpp b/src/duckdb/src/common/types/column/column_data_collection.cpp index c281921af..e17de0f66 100644 --- a/src/duckdb/src/common/types/column/column_data_collection.cpp +++ b/src/duckdb/src/common/types/column/column_data_collection.cpp @@ -81,13 +81,11 @@ ColumnDataCollection::ColumnDataCollection(shared_ptr alloc ColumnDataCollection::ColumnDataCollection(ClientContext &context, vector types_p, ColumnDataAllocatorType type, ColumnDataCollectionLifetime lifetime) : ColumnDataCollection(make_shared_ptr(context, type, lifetime), std::move(types_p)) { - D_ASSERT(!types.empty()); } ColumnDataCollection::ColumnDataCollection(ColumnDataCollection &other) : ColumnDataCollection(other.allocator, other.types) { other.finished_append = true; - D_ASSERT(!types.empty()); } ColumnDataCollection::~ColumnDataCollection() { @@ -97,7 +95,6 @@ void ColumnDataCollection::Initialize(vector types_p) { this->types = std::move(types_p); this->count = 0; this->finished_append = false; - D_ASSERT(!types.empty()); copy_functions.reserve(types.size()); for (auto &type : types) { copy_functions.push_back(GetCopyFunction(type)); @@ -1161,7 +1158,6 @@ void ColumnDataCollection::InitializeScanChunk(Allocator &allocator, DataChunk & } void ColumnDataCollection::InitializeScanChunk(ColumnDataScanState &state, DataChunk &chunk) const { - D_ASSERT(!state.column_ids.empty()); vector chunk_types; chunk_types.reserve(state.column_ids.size()); for (idx_t i = 0; i < state.column_ids.size(); i++) { diff --git a/src/duckdb/src/common/types/geometry.cpp b/src/duckdb/src/common/types/geometry.cpp index 7dcf75da2..6c366b727 100644 --- a/src/duckdb/src/common/types/geometry.cpp +++ b/src/duckdb/src/common/types/geometry.cpp @@ -982,13 +982,37 @@ struct WKBAnalysis { bool has_trailing_data = false; }; +//! A collection whose parts are being analyzed +struct WKBAnalysisFrame { + //! The number of parts that remain to be analyzed + uint32_t parts_remaining; + //! The type the parts must have, or 0 if they can have any type + uint32_t part_type; +}; + WKBAnalysis AnalyzeWKB(BlobReader &reader) { WKBAnalysis result; // Collection children are complete WKB objects embedded in the root object. - uint64_t geometries_remaining = 1; - - while (geometries_remaining > 0) { - geometries_remaining--; + vector collections; + bool is_root = true; + bool root_has_z = false; + bool root_has_m = false; + + while (is_root || !collections.empty()) { + uint32_t required_type = 0; + if (!is_root) { + auto &collection = collections.back(); + if (collection.parts_remaining == 0) { + collections.pop_back(); + continue; + } + collection.parts_remaining--; + required_type = collection.part_type; + if (collections.size() >= Geometry::MAX_RECURSION_DEPTH) { + throw InvalidInputException("Geometry exceeds maximum recursion depth of %d", + Geometry::MAX_RECURSION_DEPTH); + } + } const auto le = reader.Read() == 1; const auto meta = reader.Read(le); @@ -1003,6 +1027,21 @@ WKBAnalysis AnalyzeWKB(BlobReader &reader) { const auto has_z = ((flag_id & 0x01) != 0) || has_extz; const auto has_m = ((flag_id & 0x02) != 0) || has_extm; + if (is_root) { + root_has_z = has_z; + root_has_m = has_m; + is_root = false; + } else { + if (has_z != root_has_z || has_m != root_has_m) { + throw InvalidInputException("Geometry has inconsistent Z/M dimensions, starting at position %zu", + reader.GetPosition()); + } + if (required_type != 0 && type_id != required_type) { + throw InvalidInputException("Unexpected geometry type %d in a collection of type %d at position %zu", + type_id, required_type + 3, reader.GetPosition()); + } + } + if (has_srid) { result.any_ewkb = true; reader.Skip(sizeof(uint32_t)); // Skip SRID @@ -1046,7 +1085,8 @@ WKBAnalysis AnalyzeWKB(BlobReader &reader) { case 6: // MULTIPOLYGON case 7: { // GEOMETRYCOLLECTION const auto part_count = reader.Read(le); - geometries_remaining += part_count; + // MULTIPOINT, MULTILINESTRING and MULTIPOLYGON contain POINT, LINESTRING and POLYGON parts + collections.push_back({part_count, type_id == 7 ? 0 : type_id - 3}); result.size += sizeof(uint32_t); // part count } break; default: { diff --git a/src/duckdb/src/common/types/hyperloglog.cpp b/src/duckdb/src/common/types/hyperloglog.cpp index 5978cccf7..e88b3b39f 100644 --- a/src/duckdb/src/common/types/hyperloglog.cpp +++ b/src/duckdb/src/common/types/hyperloglog.cpp @@ -199,6 +199,12 @@ unique_ptr HyperLogLog::Deserialize(Deserializer &deserializer) { default: throw SerializationException("Unknown HyperLogLog storage type!"); } + for (idx_t i = 0; i < M; i++) { + // registers hold the number of leading zeros (plus one) of the hashes + if (result->k[i] > Q + 1) { + throw SerializationException("Invalid HyperLogLog register value %d", result->k[i]); + } + } return result; } diff --git a/src/duckdb/src/common/types/row/partitioned_tuple_data.cpp b/src/duckdb/src/common/types/row/partitioned_tuple_data.cpp index 7f19b7609..85c33683b 100644 --- a/src/duckdb/src/common/types/row/partitioned_tuple_data.cpp +++ b/src/duckdb/src/common/types/row/partitioned_tuple_data.cpp @@ -405,6 +405,14 @@ idx_t PartitionedTupleData::SizeInBytes() const { return data_size + stl_allocator->AllocationSize(); } +idx_t PartitionedTupleData::GetAllocatedSizeInBytes() const { + idx_t result = stl_allocator->AllocationSize(); + for (const auto &partition : partitions) { + result += partition->GetBlockAllocationSize(); + } + return result; +} + idx_t PartitionedTupleData::PartitionCount() const { return partitions.size(); } diff --git a/src/duckdb/src/common/types/row/tuple_data_allocator.cpp b/src/duckdb/src/common/types/row/tuple_data_allocator.cpp index 4a0e98537..fb1ead696 100644 --- a/src/duckdb/src/common/types/row/tuple_data_allocator.cpp +++ b/src/duckdb/src/common/types/row/tuple_data_allocator.cpp @@ -60,6 +60,7 @@ void TupleDataAllocator::Reset() { SetDestroyBufferUponUnpin(); row_blocks.clear(); heap_blocks.clear(); + block_allocation_size = 0; } void TupleDataAllocator::DestroyRowBlocks(const idx_t row_block_begin, const idx_t row_block_end) { @@ -119,6 +120,10 @@ idx_t TupleDataAllocator::HeapBlockCount() const { return heap_blocks.size(); } +idx_t TupleDataAllocator::GetBlockAllocationSize() const { + return block_allocation_size; +} + void TupleDataAllocator::SetPartitionIndex(const idx_t index) { D_ASSERT(!partition_index.IsValid()); D_ASSERT(row_blocks.empty() && heap_blocks.empty()); @@ -769,6 +774,7 @@ void TupleDataAllocator::ReleaseOrStoreHandlesInternal(TupleDataSegment &segment // Prevent it from being added to the eviction queue blocks[block_id].handle->GetMemory().SetDestroyBufferUpon(DestroyBufferUpon::UNPIN); // Destroy + block_allocation_size -= blocks[block_id].handle->GetMemory().GetMemoryUsage(); blocks[block_id].handle.reset(); break; default: @@ -791,6 +797,7 @@ void TupleDataAllocator::CreateRowBlock(TupleDataSegment &segment, TupleDataPinS } auto block_index = NumericCast(row_blocks.size()); row_blocks.emplace_back(std::move(block_handle), block_size); + block_allocation_size += row_blocks.back().handle->GetMemory().GetMemoryUsage(); segment.pinned_row_handles.resize(row_blocks.size()); pin_state.row_handles.emplace(block_index, std::move(buffer_handle)); } @@ -803,6 +810,7 @@ void TupleDataAllocator::CreateHeapBlock(TupleDataSegment &segment, TupleDataPin } auto block_index = NumericCast(heap_blocks.size()); heap_blocks.emplace_back(std::move(block_handle), size); + block_allocation_size += heap_blocks.back().handle->GetMemory().GetMemoryUsage(); segment.pinned_heap_handles.resize(heap_blocks.size()); pin_state.heap_handles.emplace(block_index, std::move(buffer_handle)); } diff --git a/src/duckdb/src/common/types/row/tuple_data_collection.cpp b/src/duckdb/src/common/types/row/tuple_data_collection.cpp index 4df088042..e6a72a3a9 100644 --- a/src/duckdb/src/common/types/row/tuple_data_collection.cpp +++ b/src/duckdb/src/common/types/row/tuple_data_collection.cpp @@ -8,6 +8,7 @@ #include "duckdb/common/printer.hpp" #include "duckdb/common/row_operations/row_operations.hpp" +#include "duckdb/common/reference_map.hpp" #include "duckdb/common/type_visitor.hpp" #include "duckdb/common/types/row/tuple_data_allocator.hpp" #include "duckdb/main/database.hpp" @@ -111,6 +112,24 @@ idx_t TupleDataCollection::SizeInBytes() const { return data_size + stl_allocator->AllocationSize(); } +idx_t TupleDataCollection::GetBlockAllocationSize() const { + if (segments.empty() || (segments.size() == 1 && segments[0] && segments[0]->allocator == allocator)) { + return allocator->GetBlockAllocationSize(); + } + reference_set_t allocators; + allocators.insert(*allocator); + for (const auto &segment : segments) { + if (segment) { + allocators.insert(*segment->allocator); + } + } + idx_t result = 0; + for (auto &entry : allocators) { + result += entry.get().GetBlockAllocationSize(); + } + return result; +} + void TupleDataCollection::Unpin() { for (auto &segment : segments) { segment->Unpin(); diff --git a/src/duckdb/src/common/types/time.cpp b/src/duckdb/src/common/types/time.cpp index d19041fce..34ebec740 100644 --- a/src/duckdb/src/common/types/time.cpp +++ b/src/duckdb/src/common/types/time.cpp @@ -316,6 +316,12 @@ bool Time::IsValidTime(int32_t hour, int32_t minute, int32_t second, int32_t mic if (microseconds < 0 || microseconds > 1000000) { return false; } + // a leap second (or a full second of microseconds) is allowed, as long as the time does not exceed 24:00:00 + const int64_t total_micros = + ((int64_t(hour) * 60 + minute) * 60 + second) * Interval::MICROS_PER_SEC + microseconds; + if (total_micros > Interval::MICROS_PER_DAY) { + return false; + } return true; } diff --git a/src/duckdb/src/common/types/value.cpp b/src/duckdb/src/common/types/value.cpp index dc98cd968..22611ee68 100644 --- a/src/duckdb/src/common/types/value.cpp +++ b/src/duckdb/src/common/types/value.cpp @@ -159,6 +159,12 @@ Value::Value(const Identifier &val) : Value(val.GetIdentifierName()) { Value::Value(const char *val) : Value(val ? string(val) : string()) { } +static void VerifyStringValue(const Value &value) { + if (!value.IsValid()) { + throw ErrorManager::InvalidUnicodeError(StringValue::Get(value), "value construction"); + } +} + Value::Value(std::nullptr_t val) : Value(LogicalType::VARCHAR) { } @@ -166,24 +172,18 @@ Value::Value(string_t val) : Value(val.GetString()) { } Value::Value(string val) : type_(LogicalType::VARCHAR), is_null(false) { - if (!Value::StringIsValid(val.c_str(), val.size())) { - throw ErrorManager::InvalidUnicodeError(val, "value construction"); - } value_info_ = make_shared_ptr(std::move(val)); + VerifyStringValue(*this); } Value::Value(String val) : type_(LogicalType::VARCHAR), is_null(false) { - if (!Value::StringIsValid(val.c_str(), val.size())) { - throw ErrorManager::InvalidUnicodeError(val, "value construction"); - } value_info_ = make_shared_ptr(val.ToStdString()); + VerifyStringValue(*this); } Value::Value(std::string_view val) : type_(LogicalType::VARCHAR), is_null(false) { - if (!Value::StringIsValid(val.data(), val.size())) { - throw ErrorManager::InvalidUnicodeError(string(val), "value construction"); - } value_info_ = make_shared_ptr(string(val)); + VerifyStringValue(*this); } Value::~Value() { @@ -2347,6 +2347,167 @@ void Value::Serialize(Serializer &serializer) const { SerializeInternal(serializer, true); } +//! Whether the value (excluding its children) is within the domain of its type +//! Whether a child value has the type its parent expects - ANY accepts values of any type (e.g. in secrets) +static bool ChildTypeMatches(const Value &child, const LogicalType &expected_type) { + return expected_type.id() == LogicalTypeId::ANY || child.type() == expected_type; +} + +static bool ValueIsValidShallow(const Value &value) { + if (value.IsNull()) { + return true; + } + auto &type = value.type(); + switch (type.id()) { + case LogicalTypeId::VARCHAR: + return Value::StringIsValid(StringValue::Get(value)); + case LogicalTypeId::ENUM: { + uint64_t index; + switch (type.InternalType()) { + case PhysicalType::UINT8: + index = value.GetValueUnsafe(); + break; + case PhysicalType::UINT16: + index = value.GetValueUnsafe(); + break; + case PhysicalType::UINT32: + index = value.GetValueUnsafe(); + break; + default: + throw InternalException("Invalid physical type for ENUM"); + } + return index < EnumType::GetSize(type); + } + case LogicalTypeId::TIME: { + const auto micros = value.GetValueUnsafe().value; + return micros >= 0 && micros <= Interval::MICROS_PER_DAY; + } + case LogicalTypeId::TIME_NS: { + const auto nanos = value.GetValueUnsafe().value; + return nanos >= 0 && nanos <= Interval::NANOS_PER_DAY; + } + case LogicalTypeId::TIME_TZ: { + const auto time_tz = value.GetValueUnsafe(); + const auto micros = time_tz.time().value; + const auto offset = time_tz.offset(); + return micros >= 0 && micros <= Interval::MICROS_PER_DAY && offset >= dtime_tz_t::MIN_OFFSET && + offset <= dtime_tz_t::MAX_OFFSET; + } + case LogicalTypeId::DECIMAL: { + const auto width = DecimalType::GetWidth(type); + switch (type.InternalType()) { + case PhysicalType::INT16: + case PhysicalType::INT32: + case PhysicalType::INT64: { + int64_t decimal_value; + if (type.InternalType() == PhysicalType::INT16) { + decimal_value = value.GetValueUnsafe(); + } else if (type.InternalType() == PhysicalType::INT32) { + decimal_value = value.GetValueUnsafe(); + } else { + decimal_value = value.GetValueUnsafe(); + } + return decimal_value > -NumericHelper::POWERS_OF_TEN[width] && + decimal_value < NumericHelper::POWERS_OF_TEN[width]; + } + case PhysicalType::INT128: { + const auto decimal_value = value.GetValueUnsafe(); + return decimal_value > -Hugeint::POWERS_OF_TEN[width] && decimal_value < Hugeint::POWERS_OF_TEN[width]; + } + default: + throw InternalException("Invalid physical type for DECIMAL"); + } + } + case LogicalTypeId::BIT: { + // a padding byte followed by at least one data byte, with the padding bits set + auto &bits = StringValue::Get(value); + if (bits.size() < 2) { + return false; + } + const auto padding = static_cast(bits[0]); + if (padding >= 8) { + return false; + } + const auto padding_mask = static_cast(0xFF << (8 - padding)); + return padding == 0 || (static_cast(bits[1]) & padding_mask) == padding_mask; + } + case LogicalTypeId::UNION: { + auto &children = StructValue::GetChildren(value); + if (children.size() != UnionType::GetMemberCount(type) + 1 || + children[0].type().id() != LogicalTypeId::UTINYINT || children[0].IsNull() || + children[0].GetValueUnsafe() >= UnionType::GetMemberCount(type)) { + return false; + } + for (idx_t member_idx = 0; member_idx < UnionType::GetMemberCount(type); member_idx++) { + if (!ChildTypeMatches(children[member_idx + 1], UnionType::GetMemberType(type, member_idx))) { + return false; + } + } + return true; + } + case LogicalTypeId::STRUCT: { + auto &children = StructValue::GetChildren(value); + if (children.size() != StructType::GetChildCount(type)) { + return false; + } + for (idx_t child_idx = 0; child_idx < children.size(); child_idx++) { + if (!ChildTypeMatches(children[child_idx], StructType::GetChildType(type, child_idx))) { + return false; + } + } + return true; + } + case LogicalTypeId::LIST: + case LogicalTypeId::MAP: { + auto &child_type = ListType::GetChildType(type); + for (auto &child : ListValue::GetChildren(value)) { + if (!ChildTypeMatches(child, child_type)) { + return false; + } + } + return true; + } + case LogicalTypeId::ARRAY: { + auto &children = ArrayValue::GetChildren(value); + if (children.size() != ArrayType::GetSize(type)) { + return false; + } + auto &child_type = ArrayType::GetChildType(type); + for (auto &child : children) { + if (!ChildTypeMatches(child, child_type)) { + return false; + } + } + return true; + } + default: + return true; + } +} + +bool Value::IsValid() const { + if (!ValueIsValidShallow(*this)) { + return false; + } + if (is_null) { + return true; + } + switch (type_.InternalType()) { + case PhysicalType::STRUCT: + case PhysicalType::LIST: + case PhysicalType::ARRAY: + for (auto &child : value_info_->Get().GetValues()) { + if (!child.IsValid()) { + return false; + } + } + break; + default: + break; + } + return true; +} + Value Value::Deserialize(Deserializer &deserializer) { auto type = deserializer.ReadPropertyWithExplicitDefault(100, "type", LogicalTypeId::INVALID); if (type.InternalType() == PhysicalType::INVALID) { @@ -2449,6 +2610,10 @@ Value Value::Deserialize(Deserializer &deserializer) { deserializer.ReadObject(102, "value", [&](Deserializer &obj) { vector children; obj.ReadList(100, "children", [&](Deserializer::List &list, idx_t i) { + if (i >= StructType::GetChildCount(type)) { + throw SerializationException("Failed to deserialize value: too many children for type %s", + type.ToString()); + } deserializer.Set(StructType::GetChildType(type, i)); auto child = list.ReadElement(); deserializer.Unset(); @@ -2468,6 +2633,10 @@ Value Value::Deserialize(Deserializer &deserializer) { default: throw NotImplementedException("Unimplemented type for Deserialize"); } + // the children are deserialized (and checked) through Value::Deserialize as well + if (!ValueIsValidShallow(new_value)) { + throw SerializationException("Failed to deserialize value: value is not valid for type %s", type.ToString()); + } return new_value; } diff --git a/src/duckdb/src/common/types/variant/variant_binary_decoder.cpp b/src/duckdb/src/common/types/variant/variant_binary_decoder.cpp index 71c127778..5114fe5ab 100644 --- a/src/duckdb/src/common/types/variant/variant_binary_decoder.cpp +++ b/src/duckdb/src/common/types/variant/variant_binary_decoder.cpp @@ -1,4 +1,5 @@ #include "duckdb/common/types/variant/variant_binary_decoder.hpp" +#include "utf8proc_wrapper.hpp" #include @@ -88,7 +89,11 @@ VariantMetadata::VariantMetadata(const string_t &metadata) : metadata(metadata) if (data_start + last_offset + string_size > metadata_buffer_capacity) { throw IOException("Corrupted VARIANT 'metadata' buffer"); } - strings.emplace_back(reinterpret_cast(metadata_data + data_start + last_offset), string_size); + auto key_data = reinterpret_cast(metadata_data + data_start + last_offset); + if (!Utf8Proc::IsValid(key_data, string_size)) { + throw IOException("Corrupted VARIANT 'metadata' buffer, a dictionary key isn't valid UTF8"); + } + strings.emplace_back(key_data, string_size); last_offset = next_offset; } //! header byte + offsets region + string bytes diff --git a/src/duckdb/src/common/types/vector.cpp b/src/duckdb/src/common/types/vector.cpp index 8d37df736..ff8f572e7 100644 --- a/src/duckdb/src/common/types/vector.cpp +++ b/src/duckdb/src/common/types/vector.cpp @@ -690,6 +690,19 @@ class StringDeserializeHolder : public AuxiliaryDataHolder { unique_ptr data; }; +static void CheckDeserializedListIndex(idx_t i, idx_t count) { + if (i >= count) { + throw SerializationException("Failed to deserialize vector: more entries than the vector size %llu", count); + } +} + +static void CheckDeserializedListCount(idx_t list_count, idx_t count) { + if (list_count != count) { + throw SerializationException("Failed to deserialize vector: expected %llu entries but got %llu", count, + list_count); + } +} + void Vector::Deserialize(Deserializer &deserializer, idx_t count) { auto &logical_type = GetType(); @@ -734,20 +747,28 @@ void Vector::Deserialize(Deserializer &deserializer, idx_t count) { auto blobs = FlatVector::GetDataMutable(*this); if (geometry_format == GeometryStorageType::WKB) { + idx_t entry_count = 0; deserializer.ReadList(102, "data", [&](Deserializer::List &list, idx_t i) { + CheckDeserializedListIndex(i, count); + entry_count++; auto geom = list.ReadElement(); if (validity.RowIsValid(i)) { blobs[i] = StringVector::AddStringOrBlob(*this, geom); } }); + CheckDeserializedListCount(entry_count, count); } else if (geometry_format == GeometryStorageType::SPATIAL) { // Try to read old SPATIAL format and convert to new GEOMETRY format + idx_t entry_count = 0; deserializer.ReadList(102, "data", [&](Deserializer::List &list, idx_t i) { + CheckDeserializedListIndex(i, count); + entry_count++; auto blob = list.ReadElement(); if (validity.RowIsValid(i)) { Geometry::FromSpatialGeometry(blob, blobs[i], *this); } }); + CheckDeserializedListCount(entry_count, count); } else { throw InternalException("Unsupported geometry format in vector serialization"); } @@ -770,29 +791,43 @@ void Vector::Deserialize(Deserializer &deserializer, idx_t count) { auto byte_read_ptr = reinterpret_cast(byte_data_buffer->data.get()); StringVector::AddAuxiliaryData(*this, std::move(byte_data_buffer)); + idx_t remaining_bytes = byte_data_length.GetIndex(); for (idx_t i = 0; i < count; ++i) { if (!validity.RowIsValid(i)) { continue; } + if (lengths_read_ptr[i] > remaining_bytes) { + throw SerializationException( + "Failed to deserialize vector: string lengths exceed the serialized string data"); + } + remaining_bytes -= lengths_read_ptr[i]; strings[i] = string_t(byte_read_ptr, lengths_read_ptr[i]); byte_read_ptr += lengths_read_ptr[i]; } } else { // this is ye olde way of string serialization + idx_t entry_count = 0; deserializer.ReadList(102, "data", [&](Deserializer::List &list, idx_t i) { + CheckDeserializedListIndex(i, count); + entry_count++; auto str = list.ReadElement(); if (validity.RowIsValid(i)) { strings[i] = StringVector::AddStringOrBlob(*this, str); } }); + CheckDeserializedListCount(entry_count, count); } break; } case PhysicalType::STRUCT: { auto &entries = StructVector::GetEntries(*this); // Deserialize entries as a list + idx_t child_count = 0; deserializer.ReadList(103, "children", [&](Deserializer::List &list, idx_t i) { + CheckDeserializedListIndex(i, entries.size()); + child_count++; list.ReadObject([&](Deserializer &obj) { entries[i].Deserialize(obj, count); }); }); + CheckDeserializedListCount(child_count, entries.size()); break; } case PhysicalType::LIST: { @@ -803,12 +838,24 @@ void Vector::Deserialize(Deserializer &deserializer, idx_t count) { // Read the entries auto list_entries = FlatVector::GetDataMutable(*this); + idx_t entry_count = 0; deserializer.ReadList(105, "entries", [&](Deserializer::List &list, idx_t i) { + CheckDeserializedListIndex(i, count); + entry_count++; list.ReadObject([&](Deserializer &obj) { list_entries[i].offset = obj.ReadProperty(100, "offset"); list_entries[i].length = obj.ReadProperty(101, "length"); }); + if (!validity.RowIsValid(i)) { + list_entries[i] = list_entry_t(0, 0); + } else if (list_entries[i].offset > list_size || + list_entries[i].length > list_size - list_entries[i].offset) { + throw SerializationException("Failed to deserialize vector: list entry is out of range of the " + "child vector of size %llu", + list_size); + } }); + CheckDeserializedListCount(entry_count, count); // Read the child vector deserializer.ReadObject(106, "child", [&](Deserializer &obj) { @@ -819,6 +866,10 @@ void Vector::Deserialize(Deserializer &deserializer, idx_t count) { } case PhysicalType::ARRAY: { auto array_size = deserializer.ReadProperty(103, "array_size"); + if (array_size != ArrayType::GetSize(logical_type)) { + throw SerializationException("Failed to deserialize vector: array size %llu does not match type %s", + array_size, logical_type.ToString()); + } deserializer.ReadObject(104, "child", [&](Deserializer &obj) { auto &child = ArrayVector::GetChildMutable(*this); child.Deserialize(obj, array_size * count); diff --git a/src/duckdb/src/common/vector/dictionary_vector.cpp b/src/duckdb/src/common/vector/dictionary_vector.cpp index bb0c8e40a..9cee519e8 100644 --- a/src/duckdb/src/common/vector/dictionary_vector.cpp +++ b/src/duckdb/src/common/vector/dictionary_vector.cpp @@ -241,6 +241,13 @@ buffer_ptr DictionaryBuffer::Deserialize(Deserializer &deserialize SelectionVector sel(count); deserializer.ReadProperty(91, "sel_vector", reinterpret_cast(sel.data()), sizeof(sel_t) * count); const auto dict_count = deserializer.ReadProperty(92, "dict_count"); + for (idx_t i = 0; i < count; i++) { + if (sel.get_index(i) >= dict_count) { + throw SerializationException("Failed to deserialize dictionary vector: selection index %llu is out of " + "range of the dictionary of size %llu", + sel.get_index(i), dict_count); + } + } Vector dict(type, MaxValue(dict_count, STANDARD_VECTOR_SIZE)); dict.Deserialize(deserializer, dict_count); FlatVector::SetSize(dict, dict_count); diff --git a/src/duckdb/src/execution/aggregate_hashtable.cpp b/src/duckdb/src/execution/aggregate_hashtable.cpp index f845335a1..b5a2c9977 100644 --- a/src/duckdb/src/execution/aggregate_hashtable.cpp +++ b/src/duckdb/src/execution/aggregate_hashtable.cpp @@ -55,8 +55,8 @@ GroupedAggregateHashTable::GroupedAggregateHashTable(ClientContext &context_p, A idx_t initial_capacity, idx_t radix_bits, TupleDataValidityType group_validity) : BaseAggregateHashTable(context_p, allocator, aggregate_objects_p, std::move(payload_types_p)), context(context_p), - radix_bits(radix_bits), count(0), capacity(0), sink_count(0), skip_lookups(false), enable_hll(false), - aggregate_allocator(make_shared_ptr(allocator)), state(*aggregate_allocator) { + radix_bits(radix_bits), count(0), capacity(0), aggregate_allocator(make_shared_ptr(allocator)), + state(*aggregate_allocator) { state.owner = this; state.initialized = true; clustered_state.all_clustered = AllAggregatesClustered(aggregate_objects_p); @@ -93,6 +93,7 @@ GroupedAggregateHashTable::GroupedAggregateHashTable(ClientContext &context_p, A } void GroupedAggregateHashTable::InitializePartitionedData() { + tuple_size_cache.count.SetInvalid(); if (!partitioned_data || RadixPartitioning::RadixBitsOfPowerOfTwo(partitioned_data->PartitionCount()) != radix_bits) { D_ASSERT(!partitioned_data || partitioned_data->Count() == 0); @@ -111,6 +112,7 @@ void GroupedAggregateHashTable::InitializePartitionedData() { } void GroupedAggregateHashTable::InitializeUnpartitionedData() { + tuple_size_cache.count.SetInvalid(); D_ASSERT(radix_bits >= UNPARTITIONED_RADIX_BITS_THRESHOLD); if (!unpartitioned_data) { unpartitioned_data = make_uniq(buffer_manager, layout_ptr, MemoryTag::HASH_TABLE, @@ -126,6 +128,16 @@ const PartitionedTupleData &GroupedAggregateHashTable::GetPartitionedData() cons return *partitioned_data; } +idx_t GroupedAggregateHashTable::GetSizeInBytes() const { + const auto materialized_count = GetMaterializedCount(); + if (!tuple_size_cache.count.IsValid() || tuple_size_cache.count.GetIndex() != materialized_count) { + tuple_size_cache.size = partitioned_data->GetAllocatedSizeInBytes() + + (unpartitioned_data ? unpartitioned_data->GetAllocatedSizeInBytes() : 0); + tuple_size_cache.count = materialized_count; + } + return tuple_size_cache.size + aggregate_allocator->AllocationSize() + hash_map.GetSize(); +} + unique_ptr GroupedAggregateHashTable::AcquirePartitionedData() { if (radix_bits >= UNPARTITIONED_RADIX_BITS_THRESHOLD) { // Flush/unpin unpartitioned data and append to partitioned data @@ -133,6 +145,8 @@ unique_ptr GroupedAggregateHashTable::AcquirePartitionedDa unpartitioned_data->FlushAppendState(state.unpartitioned_append_state); unpartitioned_data->Unpin(); unpartitioned_data->Repartition(context, *partitioned_data); + // Release scratch metadata along with the rows transferred to partitioned_data. + unpartitioned_data.reset(); } InitializeUnpartitionedData(); } @@ -161,6 +175,7 @@ void GroupedAggregateHashTable::Abandon() { // Start over ClearPointerTable(); count = 0; + adaptivity.BeginCycle(); // Resetting the id ensures the dict state is reset properly when needed state.dict_state.dictionary_id = string(); @@ -250,7 +265,7 @@ idx_t GroupedAggregateHashTable::ApplyBitMask(hash_t hash) const { void GroupedAggregateHashTable::Verify() { #ifdef DEBUG - if (skip_lookups) { + if (adaptivity.LookupsSkipped()) { return; } idx_t total_count = 0; @@ -279,10 +294,6 @@ idx_t GroupedAggregateHashTable::GetRadixBits() const { return radix_bits; } -idx_t GroupedAggregateHashTable::GetSinkCount() const { - return sink_count; -} - idx_t GroupedAggregateHashTable::GetMaterializedCount() const { auto result = partitioned_data->Count(); if (unpartitioned_data) { @@ -292,20 +303,17 @@ idx_t GroupedAggregateHashTable::GetMaterializedCount() const { } void GroupedAggregateHashTable::SkipLookups() { - skip_lookups = true; + adaptivity.SkipLookups(); } -void GroupedAggregateHashTable::EnableHLL(bool enable) { - enable_hll = enable; -} - -bool GroupedAggregateHashTable::HLLEnabled() const { - return enable_hll; +void GroupedAggregateHashTable::ResumeLookups() { + D_ASSERT(Count() == 0); + adaptivity.ResumeLookups(); } -idx_t GroupedAggregateHashTable::GetHLLUpperBound() const { - D_ASSERT(enable_hll); - return LossyNumericCast((1 + HyperLogLog::GetErrorRate()) * static_cast(hll.Count())); +void GroupedAggregateHashTable::EnableHLL(bool enable) { + D_ASSERT(!enable || adaptivity.HLLEnabled() || GetMaterializedCount() == 0); + adaptivity.EnableHLL(enable); } void GroupedAggregateHashTable::Resize(idx_t size) { @@ -314,9 +322,13 @@ void GroupedAggregateHashTable::Resize(idx_t size) { throw InternalException("Cannot downsize a non-empty hash table!"); } D_ASSERT(Count() == 0 || Count() == GetMaterializedCount()); + if (Count() == 0 && size == capacity && hash_map.GetSize() == size * sizeof(ht_entry_t)) { + return; + } + auto new_hash_map = buffer_manager.GetBufferAllocator().TryAllocateHuge(size * sizeof(ht_entry_t)); capacity = size; - hash_map = buffer_manager.GetBufferAllocator().TryAllocateHuge(capacity * sizeof(ht_entry_t)); + hash_map = std::move(new_hash_map); entries = reinterpret_cast(hash_map.get()); ClearPointerTable(); bitmask = capacity - 1; @@ -394,7 +406,7 @@ idx_t GroupedAggregateHashTable::AddChunk(DataChunk &groups, DataChunk &payload, return 0; } - sink_count += groups.size(); + adaptivity.AddInput(groups.size()); groups.Hash(state.hashes); const auto new_group_count = FindOrCreateGroups(groups, state.hashes, state.addresses, state.new_groups); before_update(state.addresses, state.new_groups, new_group_count); @@ -416,7 +428,7 @@ idx_t GroupedAggregateHashTable::AddChunkAndGetNewGroups(DataChunk &groups, Data return 0; } - sink_count += groups.size(); + adaptivity.AddInput(groups.size()); groups.Hash(state.hashes); const auto new_group_count = FindOrCreateGroups(groups, state.hashes, state.addresses, state.new_groups); auto source_addresses = FlatVector::GetData(state.addresses); @@ -628,7 +640,7 @@ optional_idx GroupedAggregateHashTable::TryAddCompressedGroups(DataChunk &groups } idx_t GroupedAggregateHashTable::AddChunk(DataChunk &groups, DataChunk &payload, const unsafe_vector &filter) { - sink_count += groups.size(); + adaptivity.AddInput(groups.size()); // check if we can use an optimized path that utilizes compressed vectors auto result = TryAddCompressedGroups(groups, payload, filter); @@ -643,7 +655,7 @@ idx_t GroupedAggregateHashTable::AddChunk(DataChunk &groups, DataChunk &payload, bool GroupedAggregateHashTable::UpdateAggregatesClustered(DataChunk &payload, const unsafe_vector &filter, idx_t count, bool ht_offsets_valid) { - if (skip_lookups) { + if (adaptivity.LookupsSkipped()) { return false; } if (sizeof(uintptr_t) < sizeof(uint64_t)) { @@ -834,16 +846,15 @@ idx_t GroupedAggregateHashTable::FindOrCreateGroupsInternal(DataChunk &groups, V // convert all vectors to unified format TupleDataCollection::ToUnifiedFormat(state.partitioned_append_state.chunk_state, state.group_chunk); - if (enable_hll) { - hll.Update(group_hashes_v); - } - const auto hashes = group_hashes_v.Values(); addresses_v.Flatten(); const auto addresses = FlatVector::GetDataMutable(addresses_v); - if (skip_lookups) { + if (adaptivity.LookupsSkipped()) { + if (adaptivity.HLLEnabled()) { + adaptivity.ObserveHashes(group_hashes_v); + } // Just appending now partitioned_data->AppendUnified(state.partitioned_append_state, state.group_chunk, *FlatVector::IncrementalSelectionVector(), chunk_size); @@ -971,6 +982,14 @@ idx_t GroupedAggregateHashTable::FindOrCreateGroupsInternal(DataChunk &groups, V throw InternalException("Maximum outer iteration count reached in GroupedAggregateHashTable"); } + if (adaptivity.HLLEnabled() && new_group_count > 0) { + if (new_group_count == chunk_size) { + adaptivity.ObserveHashes(group_hashes_v); + } else { + adaptivity.ObserveHashes(hashes, new_groups_out, new_group_count); + } + } + FlatVector::SetSize(addresses_v, chunk_size); count += new_group_count; return new_group_count; @@ -1239,6 +1258,7 @@ bool GroupedAggregateHashTable::Scan(AggregateHTScanState &scan_state, DataChunk } void GroupedAggregateHashTable::ResetForNewIteration(idx_t radix_bits_p) { + tuple_size_cache.count.SetInvalid(); // Save the previous iteration's group count before destroying aggregate states. // This lets us size the pointer table based on actual prior data rather than the // global sink capacity, which is typically much larger than recursive iteration sizes. @@ -1278,10 +1298,7 @@ void GroupedAggregateHashTable::ResetForNewIteration(idx_t radix_bits_p) { } count = 0; - sink_count = 0; - skip_lookups = false; - enable_hll = false; - hll = HyperLogLog(); + adaptivity.Reset(); state.dict_state.dictionary_id = string(); // Compute effective capacity based on the previous iteration's actual group count. diff --git a/src/duckdb/src/execution/index/fixed_size_allocator.cpp b/src/duckdb/src/execution/index/fixed_size_allocator.cpp index 361506dfe..fd9db7bd4 100644 --- a/src/duckdb/src/execution/index/fixed_size_allocator.cpp +++ b/src/duckdb/src/execution/index/fixed_size_allocator.cpp @@ -70,10 +70,9 @@ IndexPointer FixedSizeAllocator::New() { D_ASSERT(buffers.find(buffer_id) != buffers.end()); auto &buffer = buffers.find(buffer_id)->second; - auto offset = buffer->GetOffset(bitmask_count, available_segments_per_buffer); + auto offset = buffer->AllocateSegment(bitmask_count, available_segments_per_buffer); total_segment_count++; - buffer->segment_count++; // If the buffer is full, we cache the next buffer that we're going to fill. if (buffer->segment_count == available_segments_per_buffer) { @@ -92,22 +91,11 @@ void FixedSizeAllocator::Free(const IndexPointer ptr) { D_ASSERT(buffer_it != buffers.end()); auto &buffer = buffer_it->second; - { - // Get a handle to the buffer's validity mask (offset 0). - SegmentHandle handle(*buffer, 0); - const auto bitmask_ptr = handle.GetPtr(); - - ValidityMask mask(bitmask_ptr, offset + 1); // FIXME - D_ASSERT(!mask.RowIsValid(offset)); - mask.SetValid(offset); - } - - D_ASSERT(total_segment_count > 0); - D_ASSERT(buffer->segment_count > 0); + buffer->FreeSegment(offset, available_segments_per_buffer); // Adjust the allocator fields. + D_ASSERT(total_segment_count > 0); total_segment_count--; - buffer->segment_count--; // Early-out, if the buffer is not empty. if (buffer->segment_count != 0) { diff --git a/src/duckdb/src/execution/index/fixed_size_buffer.cpp b/src/duckdb/src/execution/index/fixed_size_buffer.cpp index 51f0470cc..a1f4483ea 100644 --- a/src/duckdb/src/execution/index/fixed_size_buffer.cpp +++ b/src/duckdb/src/execution/index/fixed_size_buffer.cpp @@ -1,5 +1,6 @@ #include "duckdb/execution/index/fixed_size_buffer.hpp" +#include "duckdb/common/typedefs.hpp" #include "duckdb/storage/block_manager.hpp" #include "duckdb/storage/buffer_manager.hpp" @@ -165,9 +166,12 @@ void FixedSizeBuffer::LoadFromDisk() { block_handle = std::move(new_block_handle); } -uint32_t FixedSizeBuffer::GetOffset(const idx_t bitmask_count, const idx_t available_segments) { +uint32_t FixedSizeBuffer::AllocateSegment(const idx_t bitmask_count, const idx_t available_segments) { + D_ASSERT(segment_count < available_segments); + // Get a handle to the buffer's validity mask (offset 0). SegmentHandle handle(*this, 0); + handle.MarkModified(); const auto bitmask_ptr = handle.GetPtr(); ValidityMask mask(bitmask_ptr, available_segments); @@ -175,8 +179,10 @@ uint32_t FixedSizeBuffer::GetOffset(const idx_t bitmask_count, const idx_t avail // fills up a buffer sequentially before searching for free bits if (mask.RowIsValid(segment_count)) { - mask.SetInvalid(segment_count); - return UnsafeNumericCast(segment_count); + const auto offset = segment_count; + mask.SetInvalid(offset); + segment_count++; + return UnsafeNumericCast(offset); } for (idx_t entry_idx = 0; entry_idx < bitmask_count; entry_idx++) { @@ -206,15 +212,31 @@ uint32_t FixedSizeBuffer::GetOffset(const idx_t bitmask_count, const idx_t avail } D_ASSERT(entry); - auto prev_bits = entry_idx * sizeof(validity_t) * 8; - D_ASSERT(mask.RowIsValid(prev_bits + first_valid_bit)); - mask.SetInvalid(prev_bits + first_valid_bit); - return UnsafeNumericCast(prev_bits + first_valid_bit); + const auto offset = entry_idx * sizeof(validity_t) * 8 + first_valid_bit; + D_ASSERT(mask.RowIsValid(offset)); + mask.SetInvalid(offset); + segment_count++; + return UnsafeNumericCast(offset); } throw InternalException("Invalid bitmask for FixedSizeAllocator"); } +void FixedSizeBuffer::FreeSegment(const idx_t offset, const idx_t available_segments) { + D_ASSERT(segment_count > 0); + D_ASSERT(offset < available_segments); + + SegmentHandle handle(*this, 0); + handle.MarkModified(); + const auto bitmask_ptr = handle.GetPtr(); + + ValidityMask mask(bitmask_ptr, available_segments); + D_ASSERT(!mask.RowIsValid(offset)); + mask.SetValid(offset); + + segment_count--; +} + void FixedSizeBuffer::SetAllocationSize(const idx_t available_segments, const idx_t segment_size, const idx_t bitmask_offset) { if (!dirty) { diff --git a/src/duckdb/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp b/src/duckdb/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp index 4853f279f..dfec932ad 100644 --- a/src/duckdb/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp +++ b/src/duckdb/src/execution/operator/csv_scanner/scanner/string_value_scanner.cpp @@ -167,7 +167,8 @@ bool StringValueResult::HandleTooManyColumnsError(const char *value_ptr, const i // we make an exception if the first over-value is null bool is_value_null = false; for (idx_t i = 0; i < null_str_count; i++) { - is_value_null = is_value_null || IsValueNull(null_str_ptr[i], value_ptr, size); + is_value_null = + is_value_null || (size == null_str_size[i] && IsValueNull(null_str_ptr[i], value_ptr, size)); } error = !is_value_null; } @@ -248,7 +249,8 @@ void StringValueResult::AddValueToVector(const char *value_ptr, idx_t size, bool // we make an exception if the first over-value is null bool is_value_null = false; for (idx_t i = 0; i < null_str_count; i++) { - is_value_null = is_value_null || IsValueNull(null_str_ptr[i], value_ptr, size); + is_value_null = + is_value_null || (size == null_str_size[i] && IsValueNull(null_str_ptr[i], value_ptr, size)); } error = !is_value_null; } @@ -747,7 +749,10 @@ bool LineError::HandleErrors(StringValueResult &result) { result.current_line_position.begin.GetGlobalPosition(result.requested_size, first_nl), line_pos.GetGlobalPosition(result.requested_size), result.path); } - if (!StringValueScanner::CanDirectlyCast(result.csv_file_scan->file_types[col_idx], result.icu_loaded)) { + // the file types are those of the projected columns + auto &file_types = result.csv_file_scan->file_types; + if (cur_error.chunk_idx < file_types.size() && + !StringValueScanner::CanDirectlyCast(file_types[cur_error.chunk_idx], result.icu_loaded)) { result.number_of_rows--; } break; diff --git a/src/duckdb/src/execution/operator/csv_scanner/table_function/read_single_csv_file.cpp b/src/duckdb/src/execution/operator/csv_scanner/table_function/read_single_csv_file.cpp index 814ebca41..9330822ee 100644 --- a/src/duckdb/src/execution/operator/csv_scanner/table_function/read_single_csv_file.cpp +++ b/src/duckdb/src/execution/operator/csv_scanner/table_function/read_single_csv_file.cpp @@ -473,7 +473,7 @@ static unique_ptr ReadSingleCSVFileCardinality(ClientContext &co } TableFunction ReadCSVTableFunction::GetSingleFileFunction() { - TableFunction read_csv("read_single_csv_file", FunctionSignature().AddPositionalOnly("path", LogicalType::VARCHAR), + TableFunction read_csv("read_single_csv_file", FunctionSignature().AddPositionalOnly("file", LogicalType::ANY), ReadSingleCSVFileFunction, ReadSingleCSVFileBind, ReadSingleCSVFileInitGlobal, ReadSingleCSVFileInitLocal); read_csv.table_scan_progress = ReadSingleCSVFileProgress; diff --git a/src/duckdb/src/execution/operator/join/outer_join_marker.cpp b/src/duckdb/src/execution/operator/join/outer_join_marker.cpp index 86a69dbbd..f79f6262b 100644 --- a/src/duckdb/src/execution/operator/join/outer_join_marker.cpp +++ b/src/duckdb/src/execution/operator/join/outer_join_marker.cpp @@ -112,10 +112,7 @@ void OuterJoinMarker::Scan(OuterJoinGlobalScanState &gstate, OuterJoinLocalScanS for (idx_t i = 0; i < left_column_count; i++) { ConstantVector::SetNull(result.data[i], count_t(result_count)); } - for (idx_t col_idx = left_column_count; col_idx < result.ColumnCount(); col_idx++) { - result.data[col_idx].Slice(lstate.scan_chunk.data[col_idx - left_column_count], lstate.match_sel, - result_count); - } + result.Slice(lstate.scan_chunk, lstate.match_sel, result_count, left_column_count); } gstate.rows_scanned.fetch_add(lstate.scan_chunk.size(), std::memory_order_relaxed); if (result_count > 0) { diff --git a/src/duckdb/src/execution/operator/persistent/physical_copy_to_file.cpp b/src/duckdb/src/execution/operator/persistent/physical_copy_to_file.cpp index 6aff67a49..263cca686 100644 --- a/src/duckdb/src/execution/operator/persistent/physical_copy_to_file.cpp +++ b/src/duckdb/src/execution/operator/persistent/physical_copy_to_file.cpp @@ -3864,9 +3864,6 @@ void PhysicalCopyToFile::MoveTmpFile(ClientContext &context, const string &tmp_f } string PhysicalCopyToFile::GetNonTmpFile(ClientContext &context, const string &tmp_file_path) { - auto &fs = FileSystem::GetFileSystem(context); - - auto path = StringUtil::GetFilePath(tmp_file_path); auto base = StringUtil::GetFileName(tmp_file_path); auto prefix = base.find("tmp_"); @@ -3874,7 +3871,7 @@ string PhysicalCopyToFile::GetNonTmpFile(ClientContext &context, const string &t base = base.substr(4); } - return fs.JoinPath(path, base); + return StringUtil::ReplaceFileName(tmp_file_path, base); } void PhysicalCopyToFile::ReturnStatistics(DataChunk &chunk, CopyToFileInfo &info) { diff --git a/src/duckdb/src/execution/operator/persistent/physical_export.cpp b/src/duckdb/src/execution/operator/persistent/physical_export.cpp index 28cca0224..c211b317b 100644 --- a/src/duckdb/src/execution/operator/persistent/physical_export.cpp +++ b/src/duckdb/src/execution/operator/persistent/physical_export.cpp @@ -71,6 +71,10 @@ static void WriteCopyStatement(FileSystem &fs, stringstream &ss, CopyInfo &info, for (auto ¬_null_column : exported_table.not_null_columns) { info.options["force_not_null"].push_back(not_null_column); } + // empty strings are written as quoted empty values, which must not be read back as NULL + if (info.options.find("allow_quoted_nulls") == info.options.end()) { + ss << ", allow_quoted_nulls false"; + } } for (auto ©_option : info.options) { if (copy_option.second.empty()) { diff --git a/src/duckdb/src/execution/operator/schema/physical_create_index.cpp b/src/duckdb/src/execution/operator/schema/physical_create_index.cpp index 1f549d923..93b88dd7f 100644 --- a/src/duckdb/src/execution/operator/schema/physical_create_index.cpp +++ b/src/duckdb/src/execution/operator/schema/physical_create_index.cpp @@ -166,13 +166,6 @@ SinkFinalizeType PhysicalCreateIndex::Finalize(Pipeline &pipeline, Event &event, return SinkFinalizeType::READY; } - // Ensure that there are no other indexes with that name on this table. - const auto &indexes = storage.GetDataTableInfo()->GetIndexes(); - if (indexes.Contains(info->GetIndexName())) { - throw CatalogException("an index with that name already exists for this table: %s", - SQLIdentifier(info->GetIndexName())); - } - // PRIMARY KEY columns cannot be NULL. if (info->constraint_type == IndexConstraintType::PRIMARY) { auto &local_storage = LocalStorage::Get(context, storage.db); diff --git a/src/duckdb/src/execution/perfect_aggregate_hashtable.cpp b/src/duckdb/src/execution/perfect_aggregate_hashtable.cpp index 07ee46352..b3d85584f 100644 --- a/src/duckdb/src/execution/perfect_aggregate_hashtable.cpp +++ b/src/duckdb/src/execution/perfect_aggregate_hashtable.cpp @@ -266,9 +266,10 @@ static void ReconstructGroupVectorTemplated(uint32_t group_values[], Value &min, // if it is 0, the value is NULL data.WriteNull(); } else { - // otherwise we add the value (minus 1) to the min value + // otherwise we add the value (minus 1) to the min value - subtracting first, as the min value can be close + // to the maximum (e.g. the infinite timestamp) data.WriteValue(UnsafeNumericCast(UnsafeNumericCast(min_data) + - UnsafeNumericCast(group_index) - 1)); + (UnsafeNumericCast(group_index) - 1))); } } } diff --git a/src/duckdb/src/execution/physical_plan/plan_asof_join.cpp b/src/duckdb/src/execution/physical_plan/plan_asof_join.cpp index 0a3e82144..c22e2cf4f 100644 --- a/src/duckdb/src/execution/physical_plan/plan_asof_join.cpp +++ b/src/duckdb/src/execution/physical_plan/plan_asof_join.cpp @@ -40,8 +40,9 @@ static unique_ptr CreatePredicateFromConditions(const vector -PhysicalPlanGenerator::PlanAsOfLoopJoin(LogicalComparisonJoin &op, PhysicalOperator &probe, PhysicalOperator &build) { +optional_ptr PhysicalPlanGenerator::PlanAsOfLoopJoin(LogicalComparisonJoin &op, + PhysicalOperator &probe, PhysicalOperator &build, + const idx_t join_cardinality) { // 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, @@ -256,15 +257,14 @@ PhysicalPlanGenerator::PlanAsOfLoopJoin(LogicalComparisonJoin &op, PhysicalOpera auto window_types = probe.GetTypes(); window_types.emplace_back(pk_type); - const auto probe_cardinality = op.EstimateCardinality(context); - auto &window = Make(window_types, std::move(window_select), probe_cardinality); + auto &window = Make(window_types, std::move(window_select), join_cardinality); window.children.emplace_back(probe); auto &join = Make(join_op, build, window, std::move(join_op.conditions), join_op.join_type, - probe_cardinality); + join_cardinality); // Plan a projection of the compare column - auto &comp_proj = Make(std::move(comp_types), std::move(comp_list), probe_cardinality); + auto &comp_proj = Make(std::move(comp_types), std::move(comp_list), join_cardinality); comp_proj.children.emplace_back(join); // Plan an aggregation on the output of the join, grouping by key; @@ -275,7 +275,7 @@ PhysicalPlanGenerator::PlanAsOfLoopJoin(LogicalComparisonJoin &op, PhysicalOpera groups.emplace_back(std::move(pk_ref)); auto &aggr = - Make(context, aggr_types, std::move(aggregates), std::move(groups), probe_cardinality); + Make(context, aggr_types, std::move(aggregates), std::move(groups), join_cardinality); aggr.children.emplace_back(comp_proj); // Project away primary/grouping key @@ -287,7 +287,7 @@ PhysicalPlanGenerator::PlanAsOfLoopJoin(LogicalComparisonJoin &op, PhysicalOpera project_list.emplace_back(std::move(col_ref)); } - auto &proj = Make(op.types, std::move(project_list), probe_cardinality); + auto &proj = Make(op.types, std::move(project_list), join_cardinality); proj.children.emplace_back(aggr); return proj; } @@ -341,6 +341,8 @@ PhysicalOperator &PhysicalPlanGenerator::PlanAsOfJoin(LogicalComparisonJoin &op) D_ASSERT(op.children.size() == 2); idx_t lhs_cardinality = op.children[0]->EstimateCardinality(context); idx_t rhs_cardinality = op.children[1]->EstimateCardinality(context); + // Estimate the join cardinality before planning the children - planning consumes the bind data of their scans + const idx_t join_cardinality = op.EstimateCardinality(context); auto &left = CreatePlan(*op.children[0]); auto &right = CreatePlan(*op.children[1]); @@ -354,7 +356,7 @@ PhysicalOperator &PhysicalPlanGenerator::PlanAsOfJoin(LogicalComparisonJoin &op) if (!force_asof_join || has_predicate) { const idx_t asof_join_threshold = Settings::Get(context); if (has_predicate || (op.children[0]->has_estimated_cardinality && lhs_cardinality < asof_join_threshold)) { - auto result = PlanAsOfLoopJoin(op, left, right); + auto result = PlanAsOfLoopJoin(op, left, right, join_cardinality); if (result) { return *result; } diff --git a/src/duckdb/src/execution/physical_plan/plan_copy_to_file.cpp b/src/duckdb/src/execution/physical_plan/plan_copy_to_file.cpp index 53f3449ff..9f8475374 100644 --- a/src/duckdb/src/execution/physical_plan/plan_copy_to_file.cpp +++ b/src/duckdb/src/execution/physical_plan/plan_copy_to_file.cpp @@ -14,10 +14,8 @@ PhysicalOperator &PhysicalPlanGenerator::CreatePlan(LogicalCopyToFile &op) { PhysicalOperator &PhysicalPlanGenerator::CreatePlan(LogicalCopyToFile &op, PhysicalOperator &plan) { if (op.use_tmp_file) { - auto &fs = FileSystem::GetFileSystem(context); - auto path = StringUtil::GetFilePath(op.file_path); auto base = StringUtil::GetFileName(op.file_path); - op.file_path = fs.JoinPath(path, "tmp_" + base); + op.file_path = StringUtil::ReplaceFileName(op.file_path, "tmp_" + base); } bool preserve_insertion_order = PhysicalPlanGenerator::PreserveInsertionOrder(context, plan); bool supports_batch_index = PhysicalPlanGenerator::UseBatchIndex(context, plan); diff --git a/src/duckdb/src/execution/physical_plan/plan_create_index.cpp b/src/duckdb/src/execution/physical_plan/plan_create_index.cpp index 7263d83e2..90e9a3e62 100644 --- a/src/duckdb/src/execution/physical_plan/plan_create_index.cpp +++ b/src/duckdb/src/execution/physical_plan/plan_create_index.cpp @@ -95,14 +95,17 @@ static PhysicalOperator &AddSort(PhysicalPlanGenerator &plan, LogicalCreateIndex PhysicalOperator &PhysicalPlanGenerator::CreatePlan(LogicalCreateIndex &op) { // Early-out, if the index already exists. - auto &schema = op.table.schema; - auto entry = - schema.GetEntry(schema.GetCatalogTransaction(context), CatalogType::INDEX_ENTRY, op.info->GetIndexName()); - if (entry) { - if (op.info->on_conflict != OnCreateConflict::IGNORE_ON_CONFLICT) { - throw CatalogException("Index with name %s already exists!", op.info->GetIndexName()); + // ALTER TABLE ADD PRIMARY KEY / UNIQUE creates a constraint index, which has no catalog entry. + if (!op.alter_table_info) { + auto &schema = op.table.schema; + auto entry = + schema.GetEntry(schema.GetCatalogTransaction(context), CatalogType::INDEX_ENTRY, op.info->GetIndexName()); + if (entry) { + if (op.info->on_conflict != OnCreateConflict::IGNORE_ON_CONFLICT) { + throw CatalogException("Index with name %s already exists!", op.info->GetIndexName()); + } + return Make(op.types, op.estimated_cardinality); } - return Make(op.types, op.estimated_cardinality); } if (!op.table.IsDuckTable()) { diff --git a/src/duckdb/src/execution/radix_ht_adaptivity.cpp b/src/duckdb/src/execution/radix_ht_adaptivity.cpp new file mode 100644 index 000000000..4200c0285 --- /dev/null +++ b/src/duckdb/src/execution/radix_ht_adaptivity.cpp @@ -0,0 +1,107 @@ +#include "duckdb/execution/radix_ht_adaptivity.hpp" + +#include "duckdb/common/exception.hpp" +#include "duckdb/common/limits.hpp" +#include "duckdb/execution/radix_ht_sink_state.hpp" + +namespace duckdb { + +bool RadixHTAdaptivity::TryGrow(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate) { + if (gstate.number_of_threads <= RadixHTConfig::GROW_STRATEGY_THREAD_THRESHOLD || gstate.external) { + return false; + } + auto &ht = *lstate.ht; + const auto &adaptivity = ht.GetAdaptivityState(); + const auto materialized_count = ht.GetMaterializedCount(); + if (!adaptivity.HLLEnabled() || ht.Count() == materialized_count || + (gstate.spill_plan && gstate.StatePressureExceeded(ht))) { + return false; + } + + const auto hll_count = MinValue(adaptivity.GetHLLUpperBound(), materialized_count); + if (hll_count == 0) { + return false; + } + + if (ht.Capacity() > NumericLimits::Maximum() / (3 * sizeof(ht_entry_t))) { + return false; + } + const auto minimum_capacity = ht.Capacity() * 2; + auto next_capacity = MaxValue(GroupedAggregateHashTable::GetCapacityForCount(hll_count), minimum_capacity); + if (next_capacity > NumericLimits::Maximum() / sizeof(ht_entry_t)) { + return false; + } + const auto current_size = ht.GetSizeInBytes(); + const auto table_size = next_capacity * sizeof(ht_entry_t); + // Grow only when repeated tuple materialization outweighs the larger pointer table. + if (static_cast(materialized_count - hll_count) * static_cast(ht.GetLayout().GetRowWidth()) < + GROWTH_DUPLICATE_BYTES_FACTOR * static_cast(table_size)) { + return false; + } + + if (table_size > NumericLimits::Maximum() - current_size) { + return false; + } + // The old pointer table remains allocated until the new allocation succeeds. + const auto desired_size = current_size + table_size; + if (desired_size > gstate.GetThreadLimit() && desired_size <= gstate.memory_limit / gstate.number_of_threads) { + const annotated_lock_guard guard {gstate.lock}; + auto &memory_state = *gstate.temporary_memory_state; + const auto request = desired_size * gstate.number_of_threads; + const auto doubled_request = request <= NumericLimits::Maximum() / 2 ? request * 2 : request; + memory_state.SetRemainingSizeAndUpdateReservation(gstate.context, + MaxValue(memory_state.GetRemainingSize(), doubled_request)); + } + const auto thread_limit = gstate.GetThreadLimit(); + while (next_capacity >= minimum_capacity) { + if (current_size <= thread_limit && next_capacity <= (thread_limit - current_size) / sizeof(ht_entry_t)) { + break; + } + next_capacity /= 2; + } + if (next_capacity < minimum_capacity) { + return false; + } + + gstate.any_abandoned = true; + ht.Abandon(); + try { + ht.Resize(next_capacity); + } catch (const OutOfMemoryException &) { + // Other operators may consume the reservation before the allocation succeeds. + ht.EnableHLL(false); + return false; + } + ht.ResumeLookups(); + lstate.local_sink_capacity = next_capacity; + return true; +} + +void RadixHTAdaptivity::MaybeSkipLookups(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate) { + auto &ht = *lstate.ht; + const auto &adaptivity = ht.GetAdaptivityState(); + if (gstate.external || !adaptivity.HLLEnabled() || adaptivity.LookupsSkipped() || + gstate.number_of_threads <= RadixHTConfig::GROW_STRATEGY_THREAD_THRESHOLD || + adaptivity.GetSinkCount() < LOOKUP_SAMPLE_SIZE || ht.Capacity() != gstate.config.sink_capacity) { + return; + } + + const auto cycle_input = adaptivity.GetCycleInputCount(); + D_ASSERT(cycle_input >= ht.Count()); + const auto reduction = + cycle_input ? static_cast(cycle_input - ht.Count()) / static_cast(cycle_input) : 0; + if (reduction < MINIMUM_SMALL_TABLE_REDUCTION) { + ht.SkipLookups(); + } +} + +void RadixHTAdaptivity::MaybeResumeLookups(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate) { + auto &ht = *lstate.ht; + const auto &adaptivity = ht.GetAdaptivityState(); + if (!gstate.external && !lstate.spilling && adaptivity.HLLEnabled() && adaptivity.LookupsSkipped() && + adaptivity.GetSkippedInputCount() >= LOOKUP_SAMPLE_SIZE) { + ht.ResumeLookups(); + } +} + +} // namespace duckdb diff --git a/src/duckdb/src/execution/radix_partitioned_hashtable.cpp b/src/duckdb/src/execution/radix_partitioned_hashtable.cpp index f39849a21..926088701 100644 --- a/src/duckdb/src/execution/radix_partitioned_hashtable.cpp +++ b/src/duckdb/src/execution/radix_partitioned_hashtable.cpp @@ -1,4 +1,6 @@ #include "duckdb/execution/radix_partitioned_hashtable.hpp" +#include "duckdb/execution/radix_ht_sink_state.hpp" +#include "duckdb/execution/radix_ht_adaptivity.hpp" #include "duckdb/common/radix_partitioning.hpp" #include "duckdb/common/enums/debug_verification_mode.hpp" @@ -120,145 +122,6 @@ struct AggregatePartition : StateWithBlockableTasks { } }; -class RadixHTGlobalSinkState; - -//! How aggregate state spilling and native row combining are allowed to interleave -enum class SpillPhase { - //! Rows may still be combined natively, and the exported width may still grow beyond it - NATIVE_ALLOWED, - //! The exported width grew beyond the native width: every combine must export its rows - EXPORTED_ONLY, - //! Rows were combined natively: the exported width can no longer grow beyond the native width - NATIVE_COMBINE_STARTED -}; - -struct RadixHTConfig { -public: - explicit RadixHTConfig(RadixHTGlobalSinkState &sink); - - void Reset(); - void SetRadixBits(const idx_t &radix_bits_p); - bool SetRadixBitsToExternal(); - idx_t GetRadixBits() const; - idx_t GetMaximumSinkRadixBits() const; - -private: - void SetRadixBitsInternal(idx_t radix_bits_p, bool external); - idx_t InitialSinkRadixBits() const; - idx_t ExternalRadixBits(bool dynamic) const; - idx_t MaximumSinkRadixBits() const; - idx_t SinkCapacity() const; - -private: - //! The global sink state - RadixHTGlobalSinkState &sink; - -public: - //! Width of tuples - const idx_t row_width; - //! Capacity of HTs during the Sink - const idx_t sink_capacity; - -private: - //! Sink radix bits to initialize with - static constexpr idx_t MAXIMUM_INITIAL_SINK_RADIX_BITS = 4; - -public: - //! Maximum Sink radix bits (independent of threads) - static constexpr idx_t MAXIMUM_FINAL_SINK_RADIX_BITS = 8; - -private: - //! Current thread-global sink radix bits - atomic sink_radix_bits; - //! Maximum Sink radix bits (set based on number of threads, if not external) - const idx_t maximum_sink_radix_bits; - - //! Thresholds at which we reduce the sink radix bits - //! This needed to reduce cache misses when we have very wide rows - static constexpr idx_t ROW_WIDTH_THRESHOLD_ONE = 32; - static constexpr idx_t ROW_WIDTH_THRESHOLD_TWO = 64; - -public: - //! If we have this many or less threads, we grow the HT, otherwise we abandon - static constexpr idx_t GROW_STRATEGY_THREAD_THRESHOLD = 2; - //! If we fill this many blocks per partition, we trigger a repartition - static constexpr double BLOCK_FILL_FACTOR = 0.5; - //! By how many bits to repartition if a repartition is triggered - static constexpr idx_t REPARTITION_RADIX_BITS = 2; - //! Thread-limit divisor for state export and exported partition sizing - static constexpr idx_t AGGREGATE_STATE_SPILL_DIVISOR = 8; - //! Arena-only pressure that forces external aggregation - static constexpr idx_t AGGREGATE_STATE_PRESSURE_DIVISOR = 2; - //! Estimated memory amplification when importing exported states - static constexpr idx_t EXPORTED_STATE_MEMORY_MULTIPLIER = 2; -}; - -class RadixHTGlobalSinkState : public GlobalSinkState { -public: - RadixHTGlobalSinkState(ClientContext &context, const RadixPartitionedHashTable &radix_ht); - - //! Destroys aggregate states (if multi-scan) - ~RadixHTGlobalSinkState() override; - void Destroy(); - -public: - idx_t GetThreadLimit() const { - return temporary_memory_state->GetReservation() / number_of_threads / 10 * 8; - } - -public: - ClientContext &context; - //! Temporary memory state for managing this hash table's memory usage - unique_ptr temporary_memory_state; - atomic minimum_reservation; - - //! Whether we've called Finalize - bool finalized; - //! Whether we are doing an external aggregation - atomic external; - //! Threads that have called Sink - atomic active_threads; - //! Number of threads (from TaskScheduler) - const idx_t number_of_threads; - //! Memory limit (from BufferManager) - const idx_t memory_limit; - //! Block size (from BufferManager) - const idx_t block_alloc_size; - //! If any thread has called combine - atomic any_combined; - //! If any thread has called ht.Abandon() during Sink (meaning uncombined_data may have duplicates) - atomic any_abandoned; - - //! The radix HT - const RadixPartitionedHashTable &radix_ht; - //! Config for partitioning - RadixHTConfig config; - - //! Uncombined partitioned data that will be put into the AggregatePartitions - unique_ptr uncombined_data; - //! The spill metadata of the aggregate layout, set if the states can spill - unique_ptr spill_plan; - //! Synchronizes the transition to exported-only aggregation with concurrent combines - SpillPhase spill_phase DUCKDB_GUARDED_BY(lock); - //! Uncombined exported data, aligned one-to-one with the partitions of uncombined_data - vector> uncombined_exported_data; - //! Allocators used during the Sink/Finalize - vector> stored_allocators; - idx_t stored_allocators_size; - - //! Partitions that are finalized during GetData - vector> partitions; - //! For keeping track of progress - atomic finalize_done; - - //! Pin properties when scanning - TupleDataPinProperties scan_pin_properties; - //! Total count before combining - idx_t count_before_combining; - //! Maximum partition size if all unique - idx_t max_partition_size; -}; - RadixHTGlobalSinkState::RadixHTGlobalSinkState(ClientContext &context_p, const RadixPartitionedHashTable &radix_ht_p) : context(context_p), temporary_memory_state(TemporaryMemoryManager::Get(context).Register(context)), finalized(false), external(false), active_threads(0), @@ -421,39 +284,12 @@ idx_t RadixHTConfig::SinkCapacity() const { // Grow strategy, start off a bit bigger return 262144; } - // Start off tiny, we can adapt with DecideAdaptation later + // Start with a small local table and grow when repeated groups justify it return 32768; } -class RadixHTLocalSinkState : public LocalSinkState { -public: - RadixHTLocalSinkState(ClientContext &context, const RadixPartitionedHashTable &radix_ht); - void ResetForReuse(const RadixPartitionedHashTable &radix_ht, RadixHTGlobalSinkState &gstate); - -public: - //! Thread-local HT that is re-used after abandoning - unique_ptr ht; - //! Chunk with group columns - DataChunk group_chunk; - - //! After seeing this many tuples, we decide whether to adapt our strategy - //! This also serves as the maximum HT sink capacity - static constexpr idx_t ADAPTIVITY_THRESHOLD = 1048576; - //! Whether we have decided to adapt our strategy - bool adapted; - //! Whether this local state has already registered itself as active for the current iteration - bool registered; - //! Sink capacity for this thread - idx_t local_sink_capacity; - - //! Data that is abandoned ends up here (only if we're doing external aggregation) - unique_ptr abandoned_data; - //! Exported abandoned aggregate states, aligned one-to-one with the partitions of abandoned_data - vector> abandoned_exported_data; -}; - RadixHTLocalSinkState::RadixHTLocalSinkState(ClientContext &, const RadixPartitionedHashTable &radix_ht) - : adapted(false), registered(false), local_sink_capacity(DConstants::INVALID_INDEX) { + : registered(false), local_sink_capacity(DConstants::INVALID_INDEX) { // If there are no groups we create a fake group so everything has the same group group_chunk.InitializeEmpty(radix_ht.group_types); if (radix_ht.grouping_set.empty()) { @@ -461,27 +297,40 @@ RadixHTLocalSinkState::RadixHTLocalSinkState(ClientContext &, const RadixPartiti } } +void RadixHTLocalSinkState::PrepareForSpill(RadixHTGlobalSinkState &gstate) { + ht->EnableHLL(false); + gstate.any_abandoned = true; + ht->Abandon(); + if (!spilling) { + // Iteration reuse can retain a larger allocation than the logical capacity. + try { + ht->Resize(gstate.config.sink_capacity); + } catch (const OutOfMemoryException &) { + // Keep the existing table if the temporary replacement allocation cannot fit. + } + spilling = true; + } + local_sink_capacity = ht->Capacity(); +} + void RadixHTLocalSinkState::ResetForReuse(const RadixPartitionedHashTable &radix_ht, RadixHTGlobalSinkState &gstate) { group_chunk.Reset(); if (radix_ht.grouping_set.empty()) { group_chunk.data[0].Reference(Value::TINYINT(42), count_t(STANDARD_VECTOR_SIZE)); } registered = false; + spilling = false; abandoned_data.reset(); abandoned_exported_data.clear(); if (!ht) { - adapted = false; local_sink_capacity = DConstants::INVALID_INDEX; return; } - local_sink_capacity = MaxValue(gstate.config.sink_capacity, ht->Capacity()); ht->ResetForNewIteration(gstate.config.GetRadixBits()); + local_sink_capacity = MaxValue(gstate.config.sink_capacity, ht->Capacity()); if (gstate.number_of_threads > RadixHTConfig::GROW_STRATEGY_THREAD_THRESHOLD) { ht->EnableHLL(true); - adapted = false; - } else { - adapted = true; } } @@ -545,47 +394,6 @@ void RadixPartitionedHashTable::PopulateGroupChunk(DataChunk &group_chunk, DataC group_chunk.Verify(); } -void DecideAdaptation(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate) { - //! If the number of unique values is greater than this percentage, we skip lookups altogether - static constexpr double SKIP_LOOKUP_UNIQUE_PERCENTAGE_THRESHOLD = 0.95; - //! If the deduplication rate could be increased by this number, we increase our sink capacity - static constexpr double CAPACITY_INCREASE_DEDUPLICATION_RATE_THRESHOLD = 2.0; - - if (gstate.external) { - return; // Shouldn't adapt after this flag has been set - } - - auto &ht = *lstate.ht; - const auto sink_count = ht.GetSinkCount(); - D_ASSERT(sink_count >= RadixHTLocalSinkState::ADAPTIVITY_THRESHOLD); - - // Deduplicated count (affected by HT size) - const auto deduplicated_count = ht.GetMaterializedCount(); - // Estimated unique count (unaffected by HT size) - const auto hll_count = MinValue(ht.GetHLLUpperBound(), deduplicated_count); - - // Compute actual deduplicated percentage and potential deduplicated count (estimated by hll) - const auto deduplicated_percentage = static_cast(deduplicated_count) / static_cast(sink_count); - const auto hll_percentage = static_cast(hll_count) / static_cast(sink_count); - if (hll_percentage > SKIP_LOOKUP_UNIQUE_PERCENTAGE_THRESHOLD) { - // Almost everything is unique, skip lookups, just append, defer deduplication to GetData phase - ht.SkipLookups(); - return; - } - - const auto potential_increase_rate = deduplicated_percentage / hll_percentage; - if (potential_increase_rate > CAPACITY_INCREASE_DEDUPLICATION_RATE_THRESHOLD) { - // We could be deduplicating a lot better, increase HT capacity - D_ASSERT(IsPowerOfTwo(RadixHTLocalSinkState::ADAPTIVITY_THRESHOLD)); - const auto new_capacity = MinValue(GroupedAggregateHashTable::GetCapacityForCount(hll_count), - RadixHTLocalSinkState::ADAPTIVITY_THRESHOLD); - lstate.local_sink_capacity = MaxValue(gstate.config.sink_capacity, new_capacity); - gstate.any_abandoned = true; - ht.Abandon(); - ht.Resize(lstate.local_sink_capacity); - } -} - // Grow abandoned_data (frozen at the radix bits of when we first went external) up to the current radix bits, so it // never has fewer partitions than the data merged into it - Repartition/Combine only handle new >= old. void GrowAbandonedDataToRadixBits(ClientContext &context, RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate, @@ -712,10 +520,9 @@ void ExportAbandonedData(ClientContext &context, RadixHTGlobalSinkState &gstate, MemoryTag::HASH_TABLE, radix_bits, hash_col_idx, context); } -// Whether the aggregate arena alone approaches the thread memory limit -bool StatePressureExceeded(RadixHTGlobalSinkState &gstate, GroupedAggregateHashTable &ht) { +bool RadixHTGlobalSinkState::StatePressureExceeded(GroupedAggregateHashTable &ht) const { return ht.GetAggregateAllocator()->AllocationSize() > - gstate.GetThreadLimit() / RadixHTConfig::AGGREGATE_STATE_PRESSURE_DIVISOR; + GetThreadLimit() / RadixHTConfig::AGGREGATE_STATE_PRESSURE_DIVISOR; } // Whether this thread's aggregate states should be exported to spillable storage @@ -725,7 +532,7 @@ bool ShouldExportStates(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &l // every current state is inline, and exporting would only cost return false; } - if (StatePressureExceeded(gstate, ht) || !lstate.abandoned_exported_data.empty()) { + if (gstate.StatePressureExceeded(ht) || !lstate.abandoned_exported_data.empty()) { return true; } if (!gstate.external) { @@ -734,34 +541,36 @@ bool ShouldExportStates(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &l return arena_size >= gstate.GetThreadLimit() / RadixHTConfig::AGGREGATE_STATE_SPILL_DIVISOR; } +void UpdateSinkReservation(ClientContext &context, RadixHTGlobalSinkState &gstate, GroupedAggregateHashTable &ht, + idx_t total_size) { + if (total_size <= gstate.GetThreadLimit() || gstate.external) { + return; + } + const annotated_lock_guard guard {gstate.lock}; + if (total_size <= gstate.GetThreadLimit()) { + return; + } + auto &temporary_memory_state = *gstate.temporary_memory_state; + const auto aggregate_allocator_size = ht.GetAggregateAllocator()->AllocationSize(); + temporary_memory_state.SetMinimumReservation(aggregate_allocator_size * gstate.number_of_threads + + gstate.minimum_reservation); + const auto remaining_size = + MaxValue(gstate.number_of_threads * total_size, temporary_memory_state.GetRemainingSize()); + // Repeated pressure checks must not keep doubling an unsatisfied reservation. + const auto requested_size = remaining_size > gstate.memory_limit / 2 ? gstate.memory_limit : 2 * remaining_size; + temporary_memory_state.SetRemainingSizeAndUpdateReservation(context, requested_size); +} + void MaybeRepartition(ClientContext &context, RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate, const bool combine) { auto &config = gstate.config; auto &ht = *lstate.ht; - - // Check if we're approaching the memory limit - auto &temporary_memory_state = *gstate.temporary_memory_state; - const auto aggregate_allocator_size = ht.GetAggregateAllocator()->AllocationSize(); - const auto total_size = - aggregate_allocator_size + ht.GetPartitionedData().SizeInBytes() + ht.Capacity() * sizeof(ht_entry_t); - if (total_size > gstate.GetThreadLimit()) { - // We're over the thread memory limit - if (!gstate.external) { - // We haven't yet triggered out-of-core behavior, but maybe we don't have to, grab the lock and check again - const annotated_lock_guard guard {gstate.lock}; - if (total_size > gstate.GetThreadLimit()) { - // Out-of-core would be triggered below, update minimum reservation and try to increase the reservation - temporary_memory_state.SetMinimumReservation(aggregate_allocator_size * gstate.number_of_threads + - gstate.minimum_reservation); - auto remaining_size = - MaxValue(gstate.number_of_threads * total_size, temporary_memory_state.GetRemainingSize()); - temporary_memory_state.SetRemainingSizeAndUpdateReservation(context, 2 * remaining_size); - } - } - } + const auto total_size = ht.GetSizeInBytes(); + UpdateSinkReservation(context, gstate, ht, total_size); if (total_size > gstate.GetThreadLimit()) { if (gstate.config.SetRadixBitsToExternal()) { + lstate.PrepareForSpill(gstate); // We're approaching the memory limit, unpin the data const auto external_radix_bits = config.GetRadixBits(); if (!lstate.abandoned_data) { @@ -811,11 +620,7 @@ void RadixPartitionedHashTable::Sink(ExecutionContext &context, DataChunk &chunk lstate.local_sink_capacity = gstate.config.sink_capacity; lstate.ht = CreateHT(context.client, lstate.local_sink_capacity, gstate.config.GetRadixBits()); if (gstate.number_of_threads > RadixHTConfig::GROW_STRATEGY_THREAD_THRESHOLD) { - // Not using grow strategy, so we enable the HLL to potentially adapt later lstate.ht->EnableHLL(true); - } else { - // Using grow strategy, so won't ever adapt - lstate.adapted = true; } } if (!lstate.registered) { @@ -827,29 +632,36 @@ void RadixPartitionedHashTable::Sink(ExecutionContext &context, DataChunk &chunk PopulateGroupChunk(group_chunk, chunk); auto &ht = *lstate.ht; + if (gstate.external && !lstate.spilling) { + lstate.PrepareForSpill(gstate); + } ht.AddChunk(group_chunk, payload_input, filter); - // Decide whether we should adapt our strategy to the data - if (!lstate.adapted && lstate.ht->GetSinkCount() >= RadixHTLocalSinkState::ADAPTIVITY_THRESHOLD) { - DecideAdaptation(gstate, lstate); - ht.EnableHLL(false); // Can be disabled now (costs 5-10% performance in worst case, single column distinct) - lstate.adapted = true; + // Row width and aggregate state size can increase without filling the pointer table. + const auto total_size = ht.GetSizeInBytes(); + UpdateSinkReservation(context.client, gstate, ht, total_size); + const auto memory_pressure = + total_size > gstate.GetThreadLimit() || (gstate.spill_plan && gstate.StatePressureExceeded(ht)); + if (!memory_pressure && + ht.Count() + STANDARD_VECTOR_SIZE < GroupedAggregateHashTable::ResizeThreshold(lstate.local_sink_capacity)) { + return; } - if (ht.Count() + STANDARD_VECTOR_SIZE < GroupedAggregateHashTable::ResizeThreshold(lstate.local_sink_capacity) && - !(gstate.spill_plan && StatePressureExceeded(gstate, ht))) { - // We can fit another chunk, and the aggregate states are not under memory pressure either. - // The state check matters for few groups with large states: those never fill the hash - // table, but their arena must still be flushed and exported before it exhausts the limit. - return; + if (!memory_pressure) { + if (RadixHTAdaptivity::TryGrow(gstate, lstate)) { + return; + } + RadixHTAdaptivity::MaybeSkipLookups(gstate, lstate); } if (gstate.number_of_threads > RadixHTConfig::GROW_STRATEGY_THREAD_THRESHOLD || gstate.external) { - // 'Reset' the HT without taking its data, we can just keep appending to the same collection - // This only works because we never resize the HT + // Keep the materialized rows and clear only the local pointer table // We don't do this when running with 1 or 2 threads, it only makes sense when there's many threads gstate.any_abandoned = true; ht.Abandon(); + if (!memory_pressure) { + RadixHTAdaptivity::MaybeResumeLookups(gstate, lstate); + } } // Check if we need to repartition @@ -884,6 +696,7 @@ void RadixPartitionedHashTable::Sink(ExecutionContext &context, DataChunk &chunk } else { GrowAbandonedDataToRadixBits(context.client, gstate, lstate, ht.GetRadixBits()); } + lstate.PrepareForSpill(gstate); ht.AcquirePartitionedData()->Repartition(context.client, *lstate.abandoned_data); ExportAbandonedData(context.client, gstate, lstate); ht.GetAggregateAllocator()->FreeAll(); diff --git a/src/duckdb/src/function/aggregate_state_serialization.cpp b/src/duckdb/src/function/aggregate_state_serialization.cpp index 0dfd35964..0b6c6f4fa 100644 --- a/src/duckdb/src/function/aggregate_state_serialization.cpp +++ b/src/duckdb/src/function/aggregate_state_serialization.cpp @@ -270,10 +270,20 @@ static void DeserializeField(const LogicalType &type, const AggregateStateField RecursiveUnifiedVectorFormat child_data; Vector::RecursiveToUnifiedFormat(*physical_child, child_data); + const auto child_validity = logical_child.Validity(); for (idx_t i = 0; i < count; i++) { LinkedList linked_list; const auto entry = values[i]; if (entry.IsValid()) { + if (field.reject_null_elements) { + const auto &list_entry = entry.GetValue(); + for (idx_t child_idx = 0; child_idx < list_entry.length; child_idx++) { + if (!child_validity.IsValid(list_entry.offset + child_idx)) { + throw InvalidInputException( + "Invalid aggregate state - the list cannot contain NULL values"); + } + } + } // NULL inputs keep an empty linked list field.list_functions.AppendListEntry(allocator, linked_list, child_data, entry.GetValue()); } @@ -298,6 +308,9 @@ void AggregateStateSerialization::DeserializeStates(const BoundAggregateFunction } DeserializeField(layout.type, layout.field, input_vec, count, dest_buffer, layout.total_state_size, 0, allocator, ownership); + if (layout.validate_state) { + layout.validate_state(dest_buffer, count, layout.total_state_size); + } } void AggregateStateSerialization::SerializeStates(const AggregateStateLayout &layout, Vector &result, idx_t count, diff --git a/src/duckdb/src/function/cast/blob_cast.cpp b/src/duckdb/src/function/cast/blob_cast.cpp index 60a259076..65f90e17f 100644 --- a/src/duckdb/src/function/cast/blob_cast.cpp +++ b/src/duckdb/src/function/cast/blob_cast.cpp @@ -15,7 +15,7 @@ BoundCastInfo DefaultCasts::BlobCastSwitch(BindCastInput &input, const LogicalTy case LogicalTypeId::LEGACY_AGGREGATE_STATE: return DefaultCasts::ReinterpretCast; case LogicalTypeId::BIT: - return BoundCastInfo(&VectorCastHelpers::StringCast); + return BoundCastInfo(&VectorCastHelpers::TryCastStringLoop); case LogicalTypeId::GEOMETRY: return BlobToGeoCast(input, source, target); default: diff --git a/src/duckdb/src/function/cast/union/from_struct.cpp b/src/duckdb/src/function/cast/union/from_struct.cpp index ed8fbdf65..fa748e326 100644 --- a/src/duckdb/src/function/cast/union/from_struct.cpp +++ b/src/duckdb/src/function/cast/union/from_struct.cpp @@ -5,6 +5,7 @@ #include "duckdb/common/vector/struct_vector.hpp" #include "duckdb/function/cast/bound_cast_data.hpp" #include "duckdb/common/exception/conversion_exception.hpp" +#include "duckdb/common/operator/cast_operators.hpp" namespace duckdb { @@ -44,6 +45,21 @@ bool StructToUnionCast::AllowImplicitCastFromStruct(const LogicalType &source, c // Physical Cast execution +static string GetUnionInvalidReasonMessage(UnionInvalidReason reason) { + switch (reason) { + case UnionInvalidReason::TAG_OUT_OF_RANGE: + return "One or more of the tags do not point to a valid union member"; + case UnionInvalidReason::VALIDITY_OVERLAP: + return "One or more rows in the produced UNION have validity set for more than 1 member"; + case UnionInvalidReason::TAG_MISMATCH: + return "One or more rows in the produced UNION have tags that don't point to the valid member"; + case UnionInvalidReason::NULL_TAG: + return "One or more rows in the produced UNION have a NULL tag"; + default: + throw InternalException("Struct to union cast failed for unknown reason"); + } +} + bool StructToUnionCast::Cast(Vector &source, Vector &result, idx_t count, CastParameters ¶meters) { auto &cast_data = parameters.cast_data->Cast(); auto &lstate = parameters.local_state->Cast(); @@ -54,15 +70,16 @@ bool StructToUnionCast::Cast(Vector &source, Vector &result, idx_t count, CastPa auto &source_children = StructVector::GetEntries(source); auto &target_children = StructVector::GetEntries(result); + bool all_converted = true; for (idx_t i = 0; i < source_children.size(); i++) { auto &result_child_vector = target_children[i]; auto &source_child_vector = source_children[i]; CastParameters child_parameters(parameters, cast_data.child_cast_info[i].GetCastData(), lstate.local_states[i]); - auto converted = - cast_data.child_cast_info[i].Cast(source_child_vector, result_child_vector, count, child_parameters); - (void)converted; - D_ASSERT(converted); + if (!cast_data.child_cast_info[i].Cast(source_child_vector, result_child_vector, count, child_parameters)) { + // the failing values have been set to NULL by the (TRY_CAST) member cast + all_converted = false; + } // we flatten the child because we use FlatVector::SetNull below and we may get non-flat from source/cast result_child_vector.Flatten(); } @@ -87,26 +104,30 @@ bool StructToUnionCast::Cast(Vector &source, Vector &result, idx_t count, CastPa } } - auto check_tags = UnionVector::CheckUnionValidity(result, count); - switch (check_tags) { - case UnionInvalidReason::TAG_OUT_OF_RANGE: - throw ConversionException("One or more of the tags do not point to a valid union member"); - case UnionInvalidReason::VALIDITY_OVERLAP: - throw ConversionException("One or more rows in the produced UNION have validity set for more than 1 member"); - case UnionInvalidReason::TAG_MISMATCH: - throw ConversionException( - "One or more rows in the produced UNION have tags that don't point to the valid member"); - case UnionInvalidReason::NULL_TAG: - throw ConversionException("One or more rows in the produced UNION have a NULL tag"); - case UnionInvalidReason::VALID: - break; - default: - throw InternalException("Struct to union cast failed for unknown reason"); + const bool is_constant = result.GetVectorType() == VectorType::CONSTANT_VECTOR; + const idx_t row_count = is_constant ? 1 : count; + if (UnionVector::CheckUnionValidity(result, row_count) != UnionInvalidReason::VALID) { + // find the invalid rows - these raise an error, or become NULL for TRY_CAST + SelectionVector row_sel(1); + for (idx_t row = 0; row < row_count; row++) { + row_sel.set_index(0, row); + auto reason = UnionVector::CheckUnionValidity(result, 1, row_sel); + if (reason == UnionInvalidReason::VALID) { + continue; + } + HandleCastError::AssignError(GetUnionInvalidReasonMessage(reason), parameters); + if (is_constant) { + ConstantVector::SetNull(result, true); + } else { + FlatVector::SetNull(result, row, true); + } + all_converted = false; + } } FlatVector::SetSize(result, count_t(count)); result.Verify(); - return true; + return all_converted; } // Bind cast diff --git a/src/duckdb/src/function/function_binder.cpp b/src/duckdb/src/function/function_binder.cpp index bf702e8bf..35eab828e 100644 --- a/src/duckdb/src/function/function_binder.cpp +++ b/src/duckdb/src/function/function_binder.cpp @@ -818,6 +818,54 @@ unique_ptr FunctionBinder::BindScalarFunction(const ScalarFunctionCa return BindScalarFunction(func, std::move(arguments), error, is_operator, binder); } +//! Returns true if any argument is an untyped NULL, i.e. has type SQLNULL +static bool HasUntypedNullArgument(const vector> ®ular_args, + const vector>> &keyword_args) { + for (auto &child : regular_args) { + if (child->GetReturnType() == LogicalTypeId::SQLNULL) { + return true; + } + } + for (auto &child : keyword_args) { + if (child.second->GetReturnType() == LogicalTypeId::SQLNULL) { + return true; + } + } + return false; +} + +static bool FoldsToNull(ClientContext &context, const Expression &child) { + if (!child.IsFoldable()) { + return false; + } + Value result; + return ExpressionExecutor::TryEvaluateScalar(context, child, result) && result.IsNull(); +} + +//! Returns true if any argument is a constant expression that evaluates to NULL +static bool HasNullConstantArgument(ClientContext &context, const vector> ®ular_args, + const vector>> &keyword_args) { + for (auto &child : regular_args) { + if (FoldsToNull(context, *child)) { + return true; + } + } + for (auto &child : keyword_args) { + if (FoldsToNull(context, *child.second)) { + return true; + } + } + return false; +} + +static void PropagateReturnCollation(BoundSimpleFunction &bound_function, const FunctionProperties &props, + const vector> &children, const string &pushed_collation); + +//! The type of the NULL constant a function call is folded to - SQLNULL if the return type could not be determined +static LogicalType NullReturnType(const LogicalType &return_type) { + return return_type.IsComplete() ? return_type : LogicalType::SQLNULL; +} + unique_ptr FunctionBinder::BindScalarFunction(const ScalarFunctionCatalogEntry &func, vector>> arguments, ErrorData &error, bool is_operator, @@ -837,42 +885,21 @@ unique_ptr FunctionBinder::BindScalarFunction(const ScalarFunctionCa auto [regular_args, keyword_args] = SplitArguments(std::move(arguments)); // If any of the parameters are NULL, the function will just be replaced with a NULL constant. - // We try to give the NULL constant the correct type, but we have to do this without binding the function, - // because functions with DEFAULT_NULL_HANDLING should not have to deal with NULL inputs in their bind code. - // Some functions may have an invalid default return type, as they must be bound to infer the return type. - // In those cases, we default to SQLNULL. - const auto return_type_if_null = - bound_function.GetReturnType().IsComplete() ? bound_function.GetReturnType() : LogicalType::SQLNULL; + // Untyped NULLs never go through type resolution. For typed NULL constants, the types of the function are resolved + // if that is the only way to determine the return type - the bind callback is not invoked for these. if (bound_function.GetNullHandling() == FunctionNullHandling::DEFAULT_NULL_HANDLING) { - for (auto &child : regular_args) { - if (child->GetReturnType() == LogicalTypeId::SQLNULL) { - return make_uniq(Value(return_type_if_null)); - } - if (!child->IsFoldable()) { - continue; - } - Value result; - if (!ExpressionExecutor::TryEvaluateScalar(context, *child, result)) { - continue; - } - if (result.IsNull()) { - return make_uniq(Value(return_type_if_null)); - } + const auto &return_type = bound_function.GetReturnType(); + if (HasUntypedNullArgument(regular_args, keyword_args)) { + return make_uniq(Value(NullReturnType(return_type))); } - for (auto &child : keyword_args) { - if (child.second->GetReturnType() == LogicalTypeId::SQLNULL) { - return make_uniq(Value(return_type_if_null)); - } - if (!child.second->IsFoldable()) { - continue; - } - Value result; - if (!ExpressionExecutor::TryEvaluateScalar(context, *child.second, result)) { - continue; - } - if (result.IsNull()) { - return make_uniq(Value(return_type_if_null)); + if (HasNullConstantArgument(context, regular_args, keyword_args)) { + if (return_type.IsComplete()) { + return make_uniq(Value(return_type)); } + auto resolved = ResolveScalarFunctionTypes(std::move(selected_function), regular_args, keyword_args); + PropagateReturnCollation(resolved.bound_function, resolved.bound_function.GetProperties(), regular_args, + resolved.pushed_collation); + return make_uniq(Value(NullReturnType(resolved.bound_function.GetReturnType()))); } } return BindScalarFunction(std::move(selected_function), std::move(regular_args), std::move(keyword_args), @@ -1462,15 +1489,17 @@ static vector CaptureLogicalArguments(const BoundSimpleFunction &fu return result; } -pair> -FunctionBinder::ResolveFunction(shared_ptr function_p, vector> &arguments, - vector>> &named_arguments) { +ResolvedScalarFunctionTypes +FunctionBinder::ResolveScalarFunctionTypes(shared_ptr function_p, + vector> &arguments, + vector>> &named_arguments) { auto &function = *function_p; // Make a BoundScalarFunction out of the ScalarFunction, so we can store bind info and other properties in it. - BoundScalarFunction bound_function(std::move(function_p)); + ResolvedScalarFunctionTypes result {BoundScalarFunction(std::move(function_p)), {}, {}}; + auto &bound_function = result.bound_function; // Reorder named args and expand the variadic arguments - auto argument_names = ResolveArguments(function, bound_function, arguments, named_arguments); + result.argument_names = ResolveArguments(function, bound_function, arguments, named_arguments); // Attempt to resolve template types, before we call the "resolve_types" callback. ResolveTemplateTypes(bound_function, arguments); @@ -1478,17 +1507,27 @@ FunctionBinder::ResolveFunction(shared_ptr function_p, vec ResolveIncompleteArgumentTypes(bound_function, arguments); if (bound_function.HasResolveTypesCallback()) { - ResolveScalarFunctionTypesInput input(context, bound_function, arguments, argument_names); + ResolveScalarFunctionTypesInput input(context, bound_function, arguments, result.argument_names); bound_function.GetResolveTypesCallback()(input); - VerifyArgumentCount(bound_function, argument_names.size(), arguments.size()); + VerifyArgumentCount(bound_function, result.argument_names.size(), arguments.size()); } // The argument types are final - all templates must be resolved before we add casts for (auto &argument_type : bound_function.GetArguments()) { VerifyTemplateType(argument_type, bound_function.GetName()); } - auto pushed_collation = PushArgumentCollations(context, bound_function, bound_function.GetProperties(), arguments); + result.pushed_collation = + PushArgumentCollations(context, bound_function, bound_function.GetProperties(), arguments); CastToFunctionArguments(bound_function, arguments); + return result; +} + +pair> +FunctionBinder::ResolveFunction(shared_ptr function_p, vector> &arguments, + vector>> &named_arguments) { + auto resolved = ResolveScalarFunctionTypes(std::move(function_p), arguments, named_arguments); + auto &bound_function = resolved.bound_function; + auto &argument_names = resolved.argument_names; // The bind callback is called with arguments that are cast to the argument types of the bound function unique_ptr bind_info; @@ -1506,7 +1545,7 @@ FunctionBinder::ResolveFunction(shared_ptr function_p, vec bound_function.GetModifiedDatabasesCallback()(context, input); } - PropagateReturnCollation(bound_function, bound_function.GetProperties(), arguments, pushed_collation); + PropagateReturnCollation(bound_function, bound_function.GetProperties(), arguments, resolved.pushed_collation); bound_function.SetLogicalReturnType(bound_function.GetReturnType()); return {std::move(bound_function), std::move(bind_info)}; diff --git a/src/duckdb/src/function/scalar/operator/arithmetic.cpp b/src/duckdb/src/function/scalar/operator/arithmetic.cpp index a243f17d5..d3a2d693b 100644 --- a/src/duckdb/src/function/scalar/operator/arithmetic.cpp +++ b/src/duckdb/src/function/scalar/operator/arithmetic.cpp @@ -1471,6 +1471,24 @@ dtime_t InterpolateOperator::Operation(const dtime_t &lo, const double d, const return dtime_t(std::llround(static_cast(lo.value) * (1.0 - d) + static_cast(hi.value) * d)); } +//! TIMETZ values are interpolated by their time normalized to UTC - the result is the normalized time at offset +00 +template <> +dtime_tz_t InterpolateOperator::Operation(const dtime_tz_t &lo, const double d, const dtime_tz_t &hi) { + if (lo.sort_key() == hi.sort_key()) { + return lo; + } + // the sort key holds the normalized time (biased by the maximum offset) in its upper bits + const auto lo_micros = static_cast(dtime_tz_t::decode_micros(lo.sort_key())); + const auto hi_micros = static_cast(dtime_tz_t::decode_micros(hi.sort_key())); + auto micros = std::llround(lo_micros * (1.0 - d) + hi_micros * d); + micros -= int64_t(dtime_tz_t::MAX_OFFSET) * int64_t(dtime_tz_t::OFFSET_MICROS); + micros %= Interval::MICROS_PER_DAY; + if (micros < 0) { + micros += Interval::MICROS_PER_DAY; + } + return dtime_tz_t(dtime_t(micros), 0); +} + template <> timestamp_t InterpolateOperator::Operation(const timestamp_t &lo, const double d, const timestamp_t &hi) { return timestamp_t(std::llround(static_cast(lo.value) * (1.0 - d) + static_cast(hi.value) * d)); diff --git a/src/duckdb/src/function/scalar/strftime_format.cpp b/src/duckdb/src/function/scalar/strftime_format.cpp index 149004b2a..a96c08162 100644 --- a/src/duckdb/src/function/scalar/strftime_format.cpp +++ b/src/duckdb/src/function/scalar/strftime_format.cpp @@ -41,13 +41,19 @@ static idx_t StrfTimepecifierSize(StrTimeSpecifier specifier) { return 3; case StrTimeSpecifier::DAY_OF_YEAR_PADDED: return 3; - case StrTimeSpecifier::YEAR_ISO: - return 4; default: return 0; } } +//! Years within [0, 9999] are padded to 4 digits, other years are written in full (with their sign) +static idx_t StrfTimeYearLength(int32_t year) { + if (0 <= year && year <= 9999) { + return 4; + } + return UnsafeNumericCast(NumericHelper::SignedLength(year)); +} + static void StrfTimeSplitOffset(int offset, int &hh, int &mm, int &ss) { hh = offset / Interval::SECS_PER_HOUR; offset = offset % Interval::SECS_PER_HOUR; @@ -86,15 +92,10 @@ idx_t StrfTimeFormat::GetSpecifierLength(StrTimeSpecifier specifier, date_t date return Date::DAY_NAMES[Date::ExtractISODayOfTheWeek(date) % 7].GetSize(); case StrTimeSpecifier::FULL_MONTH_NAME: return Date::MONTH_NAMES[data[1] - 1].GetSize(); - case StrTimeSpecifier::YEAR_DECIMAL: { - auto year = data[0]; - // Be consistent with WriteStandardSpecifier - if (0 <= year && year <= 9999) { - return 4; - } else { - return UnsafeNumericCast(NumericHelper::SignedLength(year)); - } - } + case StrTimeSpecifier::YEAR_DECIMAL: + return StrfTimeYearLength(data[0]); + case StrTimeSpecifier::YEAR_ISO: + return StrfTimeYearLength(Date::ExtractISOYearNumber(date)); case StrTimeSpecifier::MONTH_DECIMAL: { idx_t len = 1; auto month = data[1]; @@ -219,6 +220,20 @@ char *StrfTimeFormat::WritePadded3(char *target, uint32_t value) const { } // write a value in the range of 0..999999... padded to the given number of digits +char *StrfTimeFormat::WriteYear(char *target, int32_t year) const { + if (year >= 0 && year <= 9999) { + return WritePadded(target, UnsafeNumericCast(year), 4); + } + auto abs_year = year < 0 ? -int64_t(year) : int64_t(year); + if (year < 0) { + *target = '-'; + target++; + } + auto len = NumericHelper::UnsignedLength(UnsafeNumericCast(abs_year)); + NumericHelper::FormatUnsigned(UnsafeNumericCast(abs_year), target + len); + return target + len; +} + char *StrfTimeFormat::WritePadded(char *target, uint32_t value, size_t padding) const { D_ASSERT(padding > 1); if (padding % 2) { @@ -292,7 +307,7 @@ char *StrfTimeFormat::WriteDateSpecifier(StrTimeSpecifier specifier, date_t date break; } case StrTimeSpecifier::YEAR_ISO: - target = WritePadded(target, UnsafeNumericCast(Date::ExtractISOYearNumber(date)), 4); + target = WriteYear(target, Date::ExtractISOYearNumber(date)); break; case StrTimeSpecifier::WEEKDAY_ISO: *target = char('0' + uint8_t(Date::ExtractISODayOfTheWeek(date))); @@ -326,19 +341,7 @@ char *StrfTimeFormat::WriteStandardSpecifier(StrTimeSpecifier specifier, int32_t target = WritePadded2(target, UnsafeNumericCast(AbsValue(data[0]) % 100)); break; case StrTimeSpecifier::YEAR_DECIMAL: - if (data[0] >= 0 && data[0] <= 9999) { - target = WritePadded(target, UnsafeNumericCast(data[0]), 4); - } else { - int32_t year = data[0]; - if (data[0] < 0) { - *target = '-'; - year = -year; - target++; - } - auto len = NumericHelper::UnsignedLength(UnsafeNumericCast(year)); - NumericHelper::FormatUnsigned(year, target + len); - target += len; - } + target = WriteYear(target, data[0]); break; case StrTimeSpecifier::HOUR_24_PADDED: { target = WritePadded2(target, UnsafeNumericCast(data[3])); diff --git a/src/duckdb/src/function/scalar/struct/struct_contains.cpp b/src/duckdb/src/function/scalar/struct/struct_contains.cpp index 5710721ca..26ed917df 100644 --- a/src/duckdb/src/function/scalar/struct/struct_contains.cpp +++ b/src/duckdb/src/function/scalar/struct/struct_contains.cpp @@ -6,17 +6,34 @@ #include "duckdb/execution/expression_executor.hpp" #include "duckdb/function/scalar/struct_functions.hpp" #include "duckdb/function/scalar/list/contains_or_position.hpp" +#include "duckdb/planner/expression/bound_function_expression.hpp" namespace duckdb { +struct StructSearchBindData : public FunctionData { + explicit StructSearchBindData(vector matching_members_p) : matching_members(std::move(matching_members_p)) { + } + + //! The indexes of the members that can match the value - the bind casts these to the type of the value + vector matching_members; + +public: + unique_ptr Copy() const override { + return make_uniq(matching_members); + } + bool Equals(const FunctionData &other_p) const override { + return matching_members == other_p.Cast().matching_members; + } +}; + +//! Searches the target in the candidates - the members at the given indexes of the struct template -static void TemplatedStructSearch(const Vector &input_vector, const vector &members, const Vector &target, - const idx_t count, Vector &result) { +static void TemplatedStructSearch(const Vector &input_vector, const vector> &candidates, + const vector &candidate_indexes, const Vector &target, const idx_t count, + Vector &result) { // If the return type is not a bool, return the position const auto return_pos = std::is_same::value; - const auto &target_type = target.GetType(); - UnifiedVectorFormat vector_format; input_vector.ToUnifiedFormat(vector_format); @@ -24,30 +41,23 @@ static void TemplatedStructSearch(const Vector &input_vector, const vector(target_format); + D_ASSERT(candidates.size() == candidate_indexes.size()); vector member_data_ptrs; vector member_vectors; - idx_t total_matches = 0; - for (const auto &member : members) { - if (member.GetType().InternalType() == target_type.InternalType()) { - UnifiedVectorFormat member_format; - member.ToUnifiedFormat(member_format); - member_data_ptrs.push_back(UnifiedVectorFormat::GetData(member_format)); - member_vectors.push_back(std::move(member_format)); - total_matches++; - } else { - member_data_ptrs.push_back(nullptr); - member_vectors.push_back(UnifiedVectorFormat()); - } + for (auto &candidate : candidates) { + UnifiedVectorFormat member_format; + candidate.get().ToUnifiedFormat(member_format); + member_data_ptrs.push_back(UnifiedVectorFormat::GetData(member_format)); + member_vectors.push_back(std::move(member_format)); } - if (total_matches == 0 && return_pos) { + if (candidates.empty() && return_pos) { // if there are no members that match the target type, we cannot return a position ConstantVector::SetNull(result, count_t(count)); return; } auto result_data = FlatVector::Writer(result, count); - const auto member_count = members.size(); for (idx_t row = 0; row < count; row++) { const auto &member_row_idx = vector_format.sel->get_index(row); @@ -74,12 +84,9 @@ static void TemplatedStructSearch(const Vector &input_vector, const vectorget_index(row); const auto col_valid = member_vector.validity.RowIsValid(member_data_idx); @@ -91,10 +98,11 @@ static void TemplatedStructSearch(const Vector &input_vector, const vector(member_idx + 1); + found_value = UnsafeNumericCast(candidate_indexes[candidate_idx] + 1); } else { found_value = RETURN_TYPE(true); } + break; } } @@ -111,65 +119,88 @@ static void TemplatedStructSearch(const Vector &input_vector, const vector -static void StructNestedOp(const Vector &input_vector, const vector &members, const Vector &target, - const idx_t count, Vector &result) { +static void StructNestedOp(const Vector &input_vector, const vector &members, + const vector &matching_members, const Vector &target, const idx_t count, + Vector &result) { const OrderModifiers order_modifiers(OrderType::ASCENDING, OrderByNullType::NULLS_LAST); // Set up sort keys for nested types. - const auto members_size = members.size(); vector member_sort_key_vectors; - for (idx_t i = 0; i < members_size; i++) { - Vector member_sort_key_vec(LogicalType::BLOB, count); - CreateSortKeyHelpers::CreateSortKeyWithValidity(members[i], member_sort_key_vec, order_modifiers); - - member_sort_key_vectors.push_back(std::move(member_sort_key_vec)); + member_sort_key_vectors.reserve(matching_members.size()); + for (auto member_idx : matching_members) { + member_sort_key_vectors.emplace_back(LogicalType::BLOB, count); + CreateSortKeyHelpers::CreateSortKeyWithValidity(members[member_idx], member_sort_key_vectors.back(), + order_modifiers); + } + vector> candidates; + for (auto &sort_key_vector : member_sort_key_vectors) { + candidates.push_back(sort_key_vector); } Vector target_sort_key_vec(LogicalType::BLOB, count); CreateSortKeyHelpers::CreateSortKeyWithValidity(target, target_sort_key_vec, order_modifiers); - TemplatedStructSearch(input_vector, member_sort_key_vectors, target_sort_key_vec, - count, result); + TemplatedStructSearch(input_vector, candidates, matching_members, + target_sort_key_vec, count, result); } template -static void StructSearchOp(const Vector &input_vector, const vector &members, const Vector &target, - const idx_t count, Vector &result) { +static void StructSearchOp(const Vector &input_vector, const vector &members, + const vector &matching_members, const Vector &target, const idx_t count, + Vector &result) { const auto &target_type = target.GetType().InternalType(); + vector> candidates; + for (auto member_idx : matching_members) { + candidates.push_back(members[member_idx]); + } switch (target_type) { case PhysicalType::BOOL: case PhysicalType::INT8: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::INT16: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::INT32: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::INT64: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::INT128: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::UINT8: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::UINT16: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::UINT32: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::UINT64: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::UINT128: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::FLOAT: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, target, + count, result); case PhysicalType::DOUBLE: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::VARCHAR: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::INTERVAL: - return TemplatedStructSearch(input_vector, members, target, count, result); + return TemplatedStructSearch(input_vector, candidates, matching_members, + target, count, result); case PhysicalType::STRUCT: case PhysicalType::LIST: case PhysicalType::ARRAY: - return StructNestedOp(input_vector, members, target, count, result); + return StructNestedOp(input_vector, members, matching_members, target, count, result); default: throw NotImplementedException("This function has not been implemented for logical type %s", TypeIdToString(target_type)); @@ -187,8 +218,10 @@ static void StructSearchFunction(DataChunk &args, ExpressionState &state, Vector const auto &input_vector = args.data[0]; const auto &members = StructVector::GetEntries(input_vector); const auto &target = args.data[1]; + auto &func_expr = state.expr.Cast(); + auto &info = func_expr.BindInfo()->Cast(); - StructSearchOp(input_vector, members, target, count, result); + StructSearchOp(input_vector, members, info.matching_members, target, count, result); } static void StructContainsResolveTypes(ResolveScalarFunctionTypesInput &input) { @@ -219,32 +252,59 @@ static void StructContainsResolveTypes(ResolveScalarFunctionTypesInput &input) { } bound_function.GetArguments()[0] = child_type; - // the value type must match one of the struct's children - LogicalType max_child_type = arguments[1]->GetReturnType(); - vector compatible_children; + // find the type that the value and all the children it can be compared with can be cast to + LogicalType target_type = arguments[1]->GetReturnType(); for (auto &child : struct_children) { - compatible_children.push_back( - LogicalType::TryGetMaxLogicalType(context, child.second, max_child_type, max_child_type)); + LogicalType max_type; + if (LogicalType::TryGetMaxLogicalType(context, child.second, target_type, max_type)) { + target_type = max_type; + } + } + bound_function.GetArguments()[1] = target_type; + // cast the children that can be compared with the value to that type - the others never match + vector new_child_types; + for (idx_t child_idx = 0; child_idx < struct_children.size(); child_idx++) { + auto &struct_child_type = struct_children[child_idx].second; + LogicalType max_type; + if (LogicalType::TryGetMaxLogicalType(context, struct_child_type, target_type, max_type) && + max_type == target_type) { + new_child_types.push_back(target_type); + } else { + new_child_types.push_back(struct_child_type); + } } - // cast all compatible children to the final max type child_list_t cast_children; - for (idx_t i = 0; i < struct_children.size(); i++) { - auto &child = struct_children[i]; - cast_children.push_back(make_pair(child.first, compatible_children[i] ? max_child_type : child.second)); - if (compatible_children[i]) { - bound_function.GetArguments()[1] = max_child_type; - } + for (idx_t i = 0; i < new_child_types.size(); i++) { + cast_children.push_back(make_pair(struct_children[i].first, new_child_types[i])); } // the input is an unnamed struct - represent it as a TUPLE bound_function.GetArguments()[0] = LogicalType::TUPLE(cast_children); } +static unique_ptr StructContainsBind(BindScalarFunctionInput &input) { + auto &arguments = input.GetArguments(); + vector matching_members; + auto &child_type = arguments[0]->GetReturnType(); + if (child_type.id() != LogicalTypeId::SQLNULL) { + // the members that can match the value were cast to the type of the value when resolving the types + auto &target_type = arguments[1]->GetReturnType(); + auto &struct_children = StructType::GetChildTypes(child_type); + for (idx_t child_idx = 0; child_idx < struct_children.size(); child_idx++) { + if (struct_children[child_idx].second == target_type) { + matching_members.push_back(child_idx); + } + } + } + return make_uniq(std::move(matching_members)); +} + ScalarFunction StructContainsFun::GetFunction() { ScalarFunction fun("struct_contains", {}, LogicalType::BOOLEAN, StructSearchFunction); fun.GetSignature().AddParameter("struct", LogicalTypeId::TUPLE).AddParameter("entry", LogicalType::ANY); fun.SetResolveTypesCallback(StructContainsResolveTypes); + fun.SetBindCallback(StructContainsBind); return fun; } @@ -252,6 +312,7 @@ ScalarFunction StructPositionFun::GetFunction() { ScalarFunction fun("struct_contains", {}, LogicalType::INTEGER, StructSearchFunction); fun.GetSignature().AddParameter("struct", LogicalTypeId::TUPLE).AddParameter("entry", LogicalType::ANY); fun.SetResolveTypesCallback(StructContainsResolveTypes); + fun.SetBindCallback(StructContainsBind); fun.SetNullHandling(FunctionNullHandling::SPECIAL_HANDLING); return fun; } diff --git a/src/duckdb/src/function/scalar/system/aggregate_export.cpp b/src/duckdb/src/function/scalar/system/aggregate_export.cpp index b36b9a219..584ec1bfb 100644 --- a/src/duckdb/src/function/scalar/system/aggregate_export.cpp +++ b/src/duckdb/src/function/scalar/system/aggregate_export.cpp @@ -136,9 +136,13 @@ void AggregateStateFinalize(DataChunk &input, ExpressionState &state_p, Vector & auto &layout = local_state.layout; auto count = input.size(); + // initialize the states - fields that are NULL in the input are not written by the deserialization + AggregateStateInput state_input(bind_data.aggr, bind_data.bind_data.get()); auto state_vec_writer = FlatVector::Writer(local_state.addresses, count); for (idx_t i = 0; i < count; i++) { - state_vec_writer.WriteValue(local_state.state_buffer.get() + i * layout.total_state_size); + data_ptr_t state_ptr = local_state.state_buffer.get() + i * layout.total_state_size; + bind_data.aggr.GetStateInitCallback()(state_input, &state_ptr, 1); + state_vec_writer.WriteValue(state_ptr); } AggregateStateSerialization::DeserializeStates(bind_data.aggr, layout, input.data[0], count, @@ -247,9 +251,25 @@ unique_ptr BindExportedAggregate(ClientContext &context FunctionBinder function_binder(context); auto best_function = function_binder.BindFunction(aggr_entry.name, aggr_entry.functions, argument_types, error); if (!best_function.IsValid()) { - throw InternalException("Could not re-bind exported aggregate %s: %s", function_name, error.Message()); + throw BinderException("Could not re-bind exported aggregate %s: %s", function_name, error.Message()); } const auto &aggr = aggr_entry.functions.GetFunctionByOffset(best_function.GetIndex()); + const auto &signature = aggr->GetSignature(); + for (idx_t arg_idx = 0; arg_idx < argument_types.size(); arg_idx++) { + if (argument_types[arg_idx].id() != LogicalTypeId::ANY) { + continue; + } + auto parameter = arg_idx < signature.GetPositionalParameterCount() + ? optional_ptr(signature.GetParameter(arg_idx)) + : signature.GetArgs(); + // ANY must describe a state-capable implementation, not a placeholder for type specialization. + const bool canonical_any = parameter && parameter->GetType().id() == LogicalTypeId::ANY && + AnyType::GetTargetType(parameter->GetType()) == LogicalType::ANY; + if (!canonical_any || !aggr->HasGetStateTypeCallback()) { + throw BinderException("Aggregate %s requires a concrete type for signature argument %llu", function_name, + arg_idx + 1); + } + } // FIXME: this is really hacky // but the aggregate state export needs a rework around how it handles more complex aggregates anyway @@ -275,10 +295,17 @@ unique_ptr BindExportedAggregate(ClientContext &context } } if (!signature_matches) { - throw InternalException("Type mismatch for exported aggregate %s: bound=[%s] requested=[%s]", function_name, - StringUtil::ToString(bound_args, ", "), StringUtil::ToString(argument_types, ", ")); + throw BinderException("Type mismatch for exported aggregate %s: bound=[%s] requested=[%s]", function_name, + StringUtil::ToString(bound_args, ", "), StringUtil::ToString(argument_types, ", ")); } + // The signature can retain ANY, but materialized result and state values must have complete types. + if (!bound_aggr.GetReturnType().IsComplete()) { + throw BinderException("Aggregate %s signature does not resolve a complete result type", function_name); + } + if (bound_aggr.HasGetStateTypeCallback() && !bound_aggr.GetStateType(bind_info.get()).type.IsComplete()) { + throw BinderException("Aggregate %s signature does not resolve a complete state type", function_name); + } const auto state_size = bound_aggr.GetStateSize(bind_info.get()); return make_uniq(bound_aggr, std::move(bind_info), state_size); } @@ -623,7 +650,12 @@ LogicalType ParseSignatureType(ClientContext &context, const Value &arg) { return TypeValue::GetType(arg); } if (arg.type().id() == LogicalTypeId::VARCHAR) { - return TransformStringToLogicalType(StringValue::Get(arg), context); + auto &name = StringValue::Get(arg); + // ANY describes a canonical aggregate argument, not an ordinary SQL value type. + if (StringUtil::CIEquals(name, "ANY")) { + return LogicalType::ANY; + } + return TransformStringToLogicalType(name, context); } throw BinderException("to_aggregate_state: the signature must be a list of types"); } @@ -769,6 +801,9 @@ unique_ptr BindToAggregateState(INPUT &input) { } } const auto buffer_struct = LogicalType::STRUCT(std::move(buffer_columns)); + if (!buffer_struct.IsComplete()) { + throw BinderException("to_aggregate_state: the ordered state buffer must have a complete type"); + } vector orders; auto order_value = input.GetConstant(4); ParseOrderBys(order_value, column_count, orders); @@ -823,7 +858,8 @@ unique_ptr ExportAggregateFunction::StateToSQL(const LogicalTy vector signature; vector> constant_arguments; for (idx_t i = 0; i < types.size(); i++) { - if (!TypeExpression::CanRepresent(types[i]) || TypeVisitor::Contains(types[i], [](const LogicalType &child) { + if ((types[i] != LogicalType::ANY && !TypeExpression::CanRepresent(types[i])) || + TypeVisitor::Contains(types[i], [](const LogicalType &child) { return child.id() == LogicalTypeId::VARCHAR && !StringType::GetCollation(child).empty(); })) { return nullptr; diff --git a/src/duckdb/src/function/table/arrow_conversion.cpp b/src/duckdb/src/function/table/arrow_conversion.cpp index b8de02f1f..899830562 100644 --- a/src/duckdb/src/function/table/arrow_conversion.cpp +++ b/src/duckdb/src/function/table/arrow_conversion.cpp @@ -65,9 +65,10 @@ static void GetValidityMask(ValidityMask &mask, ArrowArray &array, idx_t chunk_o //! just memcpy nullmask memcpy((void *)mask.GetData(), ArrowBufferData(array, 0) + bit_offset / 8, n_bitmask_bytes); } else { - //! need to re-align nullmask - vector temp_nullmask(n_bitmask_bytes + 1); - memcpy(temp_nullmask.data(), ArrowBufferData(array, 0) + bit_offset / 8, n_bitmask_bytes + 1); + //! need to re-align nullmask - only the bytes that hold the bits of the rows are guaranteed to exist + vector temp_nullmask(n_bitmask_bytes + 1, 0); + auto n_source_bytes = (bit_offset % 8 + size + 8 - 1) / 8; + memcpy(temp_nullmask.data(), ArrowBufferData(array, 0) + bit_offset / 8, n_source_bytes); ShiftRight(temp_nullmask.data(), NumericCast(n_bitmask_bytes + 1), NumericCast(bit_offset % 8ull)); //! why this has to be a right shift is a mystery to me memcpy((void *)mask.GetData(), data_ptr_cast(temp_nullmask.data()), n_bitmask_bytes); @@ -1426,7 +1427,8 @@ static void SetSelectionVector(SelectionVector &sel, data_ptr_t indices_p, const } static bool CanContainNull(const ArrowArray &array, const ValidityMask *parent_mask) { - if (array.null_count > 0) { + // a null count of -1 means that the number of nulls is unknown + if (array.null_count != 0) { return true; } if (!parent_mask) { diff --git a/src/duckdb/src/function/table/copy_csv.cpp b/src/duckdb/src/function/table/copy_csv.cpp index daec0394c..d469dff80 100644 --- a/src/duckdb/src/function/table/copy_csv.cpp +++ b/src/duckdb/src/function/table/copy_csv.cpp @@ -194,14 +194,14 @@ static void CSVListCopyOptions(ClientContext &context, CopyOptionsInput &input) copy_options["parallel"] = CopyOption(LogicalType::BOOLEAN, CopyOptionMode::READ_ONLY); copy_options["allow_quoted_nulls"] = CopyOption(LogicalType::BOOLEAN, CopyOptionMode::READ_ONLY); copy_options["store_rejects"] = CopyOption(LogicalType::BOOLEAN, CopyOptionMode::READ_ONLY); - copy_options["force_not_null"] = CopyOption(LogicalType::ANY, CopyOptionMode::READ_ONLY); + copy_options["force_not_null"] = CopyOption(LogicalType::LIST(LogicalType::VARCHAR), CopyOptionMode::READ_ONLY); copy_options["rejects_table"] = CopyOption(LogicalType::VARCHAR, CopyOptionMode::READ_ONLY); copy_options["rejects_scan"] = CopyOption(LogicalType::VARCHAR, CopyOptionMode::READ_ONLY); copy_options["rejects_limit"] = CopyOption(LogicalType::BIGINT, CopyOptionMode::READ_ONLY); copy_options["encoding"] = CopyOption(LogicalType::VARCHAR, CopyOptionMode::READ_ONLY); copy_options["thousands"] = CopyOption(LogicalType::VARCHAR, CopyOptionMode::READ_ONLY); - copy_options["force_quote"] = CopyOption(LogicalType::ANY, CopyOptionMode::WRITE_ONLY); + copy_options["force_quote"] = CopyOption(LogicalType::LIST(LogicalType::VARCHAR), CopyOptionMode::WRITE_ONLY); copy_options["prefix"] = CopyOption(LogicalType::VARCHAR, CopyOptionMode::WRITE_ONLY); copy_options["suffix"] = CopyOption(LogicalType::VARCHAR, CopyOptionMode::WRITE_ONLY); diff --git a/src/duckdb/src/function/table/system/logging_utils.cpp b/src/duckdb/src/function/table/system/logging_utils.cpp index a8be73e7f..198e83b5d 100644 --- a/src/duckdb/src/function/table/system/logging_utils.cpp +++ b/src/duckdb/src/function/table/system/logging_utils.cpp @@ -31,18 +31,24 @@ static void EnableLogging(ClientContext &context, TableFunctionInput &data, Data auto &log_manager = context.db->GetLogManager(); - // Apply the config generated from the input - log_manager.SetConfig(*context.db, bind_data.config); - - if (bind_data.log_types_to_set.empty()) { - log_manager.SetEnableLogging(true); - log_manager.SetLogMode(LogMode::LEVEL_ONLY); - } else { - log_manager.SetEnableStructuredLoggers(bind_data.log_types_to_set); - } + // Apply the config generated from the input - restoring the previous config if that fails part-way + auto previous_config = log_manager.GetConfig(); + try { + log_manager.SetConfig(*context.db, bind_data.config); + + if (bind_data.log_types_to_set.empty()) { + log_manager.SetEnableLogging(true); + log_manager.SetLogMode(LogMode::LEVEL_ONLY); + } else { + log_manager.SetEnableStructuredLoggers(bind_data.log_types_to_set); + } - if (!bind_data.storage_config.empty()) { - log_manager.UpdateLogStorageConfig(*context.db, bind_data.storage_config); + if (!bind_data.storage_config.empty()) { + log_manager.UpdateLogStorageConfig(*context.db, bind_data.storage_config); + } + } catch (...) { + log_manager.SetConfig(*context.db, previous_config); + throw; } } @@ -140,6 +146,10 @@ static unique_ptr BindEnableLogging(ClientContext &context, TableF for (const auto &log_type : result->log_types_to_set) { TryAutoloadLogTypeExtension(context, log_type); + // verify the log types before changing any of the logging settings + if (!context.db->GetLogManager().LookupLogType(log_type)) { + throw InvalidInputException("Unknown log type: '%s'", log_type); + } } return_types.emplace_back(LogicalType::BOOLEAN); diff --git a/src/duckdb/src/function/table/table_scan.cpp b/src/duckdb/src/function/table/table_scan.cpp index cce726d52..e67d1b70d 100644 --- a/src/duckdb/src/function/table/table_scan.cpp +++ b/src/duckdb/src/function/table/table_scan.cpp @@ -540,6 +540,8 @@ class DuckTableScanState : public TableScanGlobalState { l_state.job_rows_scanned += job_scan.RowsScanned(); job_scan.table_state.rows_scanned = 0; job_scan.local_state.rows_scanned = 0; + // a pooled state outlives the pipeline, it must not keep blocks pinned that a sink may still convert + job_scan.ReleasePins(); state_pool.Push(std::move(l_state.job->scan_state)); l_state.job.reset(); if (TryYieldControl(data_p)) { @@ -1107,9 +1109,9 @@ vector TableScanGetPartitionStats(ClientContext &context, G return storage.GetPartitionStats(context); } -BindInfo TableScanGetBindInfo(const optional_ptr bind_data_p) { +optional_ptr TableScanGetTableEntry(optional_ptr bind_data_p) { auto &bind_data = bind_data_p->Cast(); - return BindInfo(bind_data.table); + return &bind_data.table; } void TableScanDependency(LogicalDependencyList &entries, const FunctionData *bind_data_p) { @@ -1278,7 +1280,7 @@ TableFunction TableScanFunction::GetFunction() { scan_function.table_scan_progress = TableScanProgress; scan_function.get_partition_data = TableScanGetPartitionData; scan_function.get_partition_stats = TableScanGetPartitionStats; - scan_function.get_bind_info = TableScanGetBindInfo; + scan_function.get_table_entry = TableScanGetTableEntry; scan_function.projection_pushdown = true; scan_function.filter_pushdown = true; scan_function.filter_prune = true; diff --git a/src/duckdb/src/function/table/version/pragma_version.cpp b/src/duckdb/src/function/table/version/pragma_version.cpp index 76068116b..f6309c670 100644 --- a/src/duckdb/src/function/table/version/pragma_version.cpp +++ b/src/duckdb/src/function/table/version/pragma_version.cpp @@ -1,5 +1,5 @@ #ifndef DUCKDB_PATCH_VERSION -#define DUCKDB_PATCH_VERSION "0-alpha45672" +#define DUCKDB_PATCH_VERSION "0-alpha46057" #endif #ifndef DUCKDB_MINOR_VERSION #define DUCKDB_MINOR_VERSION 0 @@ -8,10 +8,10 @@ #define DUCKDB_MAJOR_VERSION 2 #endif #ifndef DUCKDB_VERSION -#define DUCKDB_VERSION "v2.0.0-alpha45672" +#define DUCKDB_VERSION "v2.0.0-alpha46057" #endif #ifndef DUCKDB_SOURCE_ID -#define DUCKDB_SOURCE_ID "f2f9329721" +#define DUCKDB_SOURCE_ID "b15358c371" #endif #include "duckdb/function/table/system_functions.hpp" #include "duckdb/main/database.hpp" diff --git a/src/duckdb/src/function/table_function.cpp b/src/duckdb/src/function/table_function.cpp index c46787428..64dd63ea7 100644 --- a/src/duckdb/src/function/table_function.cpp +++ b/src/duckdb/src/function/table_function.cpp @@ -24,12 +24,13 @@ BaseTableFunction::BaseTableFunction(table_function_t function_, table_function_ function(function_), in_out_function(nullptr), in_out_function_final(nullptr), statistics(nullptr), statistics_extended(nullptr), dependency(nullptr), cardinality(nullptr), get_metrics(nullptr), pushdown_complex_filter(nullptr), pushdown_expression(nullptr), to_string(nullptr), table_scan_progress(nullptr), - get_partition_data(nullptr), get_bind_info(nullptr), projection_expression_pushdown(nullptr), - get_multi_file_reader(nullptr), supports_pushdown_type(nullptr), supports_pushdown_extract(nullptr), - is_repeatable(nullptr), get_partition_info(nullptr), get_partition_stats(nullptr), get_virtual_columns(nullptr), - get_row_id_columns(nullptr), set_scan_order(nullptr), serialize(nullptr), deserialize(nullptr), - projection_pushdown(false), filter_pushdown(false), filter_prune(false), sampling_pushdown(false), - late_materialization(false), return_type(TableFunctionReturnType::TABLE_RETURNING_FUNCTION) { + get_partition_data(nullptr), get_bind_info(nullptr), get_table_entry(nullptr), + projection_expression_pushdown(nullptr), get_multi_file_reader(nullptr), supports_pushdown_type(nullptr), + supports_pushdown_extract(nullptr), is_repeatable(nullptr), get_partition_info(nullptr), + get_partition_stats(nullptr), get_virtual_columns(nullptr), get_row_id_columns(nullptr), set_scan_order(nullptr), + serialize(nullptr), deserialize(nullptr), projection_pushdown(false), filter_pushdown(false), filter_prune(false), + sampling_pushdown(false), late_materialization(false), + return_type(TableFunctionReturnType::TABLE_RETURNING_FUNCTION) { } TableFunction::TableFunction(Identifier name, const vector &arguments, table_function_t function_, @@ -99,6 +100,7 @@ BoundTableFunction::BoundTableFunction(shared_ptr function_ table_scan_progress = function.table_scan_progress; get_partition_data = function.get_partition_data; get_bind_info = function.get_bind_info; + get_table_entry = function.get_table_entry; projection_expression_pushdown = function.projection_expression_pushdown; get_multi_file_reader = function.get_multi_file_reader; supports_pushdown_type = function.supports_pushdown_type; @@ -200,6 +202,7 @@ bool BaseTableFunction::operator==(const BaseTableFunction &rhs) const { pushdown_complex_filter == rhs.pushdown_complex_filter && pushdown_expression == rhs.pushdown_expression && to_string == rhs.to_string && to_sql == rhs.to_sql && table_scan_progress == rhs.table_scan_progress && get_partition_data == rhs.get_partition_data && get_bind_info == rhs.get_bind_info && + get_table_entry == rhs.get_table_entry && projection_expression_pushdown == rhs.projection_expression_pushdown && get_multi_file_reader == rhs.get_multi_file_reader && supports_pushdown_type == rhs.supports_pushdown_type && is_repeatable == rhs.is_repeatable && get_partition_info == rhs.get_partition_info && diff --git a/src/duckdb/src/include/duckdb/common/adbc/wrappers.hpp b/src/duckdb/src/include/duckdb/common/adbc/wrappers.hpp index 20f6bbb11..ca71e4e24 100644 --- a/src/duckdb/src/include/duckdb/common/adbc/wrappers.hpp +++ b/src/duckdb/src/include/duckdb/common/adbc/wrappers.hpp @@ -9,6 +9,7 @@ #pragma once #include "duckdb.h" +#include "duckdb/common/atomic.hpp" #include "duckdb/common/mutex.hpp" #include "duckdb/common/string.hpp" #include "duckdb/common/unordered_map.hpp" @@ -23,6 +24,9 @@ namespace duckdb { struct DuckDBAdbcConnectionWrapper { duckdb_connection connection; unordered_map options; + //! A cancel and a max_execution_time timeout raise the same exception type, and errors_as_json rewrites the + //! message that would tell them apart. Cleared when a statement starts executing + atomic cancel_requested {false}; //! Register a stream wrapper so it can be materialized if another query runs on this connection. void RegisterStream(duckdb_adbc::DuckDBAdbcStreamWrapper *stream); diff --git a/src/duckdb/src/include/duckdb/common/arrow/arrow_format.hpp b/src/duckdb/src/include/duckdb/common/arrow/arrow_format.hpp index 11e0e76dd..1f5e2b097 100644 --- a/src/duckdb/src/include/duckdb/common/arrow/arrow_format.hpp +++ b/src/duckdb/src/include/duckdb/common/arrow/arrow_format.hpp @@ -88,6 +88,8 @@ class ArrowFormat : public ResultFormat { DUCKDB_API static unique_ptr UnpackUnit(unique_ptr unit); //! An export over the owner's buffers whose every node holds the owner, so a moved-out child outlives the rest DUCKDB_API static unique_ptr ShareArray(const ArrowArrayOwner &owner); + //! The count in a CHANGED_ROWS result's array, a single BIGINT row; 0 for an empty array + DUCKDB_API static idx_t ChangedRows(const ArrowArray &array); private: //! The rows an array holds, except at a batch boundary and at a producer's end of input diff --git a/src/duckdb/src/include/duckdb/common/enum_util.hpp b/src/duckdb/src/include/duckdb/common/enum_util.hpp index 37d592b91..3776a8a7c 100644 --- a/src/duckdb/src/include/duckdb/common/enum_util.hpp +++ b/src/duckdb/src/include/duckdb/common/enum_util.hpp @@ -236,6 +236,8 @@ enum class ExtensionRepositoryType : uint8_t; enum class ExtensionUpdateResultTag : uint8_t; +enum class ExternalFileCacheRequestSizing : uint8_t; + enum class ExternalResourceOperation : uint8_t; enum class ExtraDropInfoType : uint8_t; @@ -971,6 +973,9 @@ const char* EnumUtil::ToChars(ExtensionRepositoryType v template<> const char* EnumUtil::ToChars(ExtensionUpdateResultTag value); +template<> +const char* EnumUtil::ToChars(ExternalFileCacheRequestSizing value); + template<> const char* EnumUtil::ToChars(ExternalResourceOperation value); @@ -1920,6 +1925,9 @@ ExtensionRepositoryType EnumUtil::FromString(const char template<> ExtensionUpdateResultTag EnumUtil::FromString(const char *value); +template<> +ExternalFileCacheRequestSizing EnumUtil::FromString(const char *value); + template<> ExternalResourceOperation EnumUtil::FromString(const char *value); diff --git a/src/duckdb/src/include/duckdb/common/multi_file/multi_file_data.hpp b/src/duckdb/src/include/duckdb/common/multi_file/multi_file_data.hpp index abcc3e987..208249d4a 100644 --- a/src/duckdb/src/include/duckdb/common/multi_file/multi_file_data.hpp +++ b/src/duckdb/src/include/duckdb/common/multi_file/multi_file_data.hpp @@ -91,6 +91,14 @@ struct MultiFileColumnDefinition { return result; } + //! The definition of a column, with a child for every nested field an identifier can be attached to: the fields of + //! a STRUCT, the elements of a LIST or ARRAY, the "key_value" entries of a MAP, and the members of a UNION + DUCKDB_API static MultiFileColumnDefinition CreateNested(const Identifier &name, const LogicalType &type); + //! The definition of the nested field a path of child indexes addresses, starting at this column: a STRUCT field by + //! its index, the elements of a LIST or ARRAY by 0, the keys of a MAP by 0 and its values by 1, and a UNION member + //! by its index. Requires a definition from CreateNested; throws when the path addresses no nested field + DUCKDB_API MultiFileColumnDefinition &ResolveChildPath(const vector &child_path); + static vector ColumnsFromNamesAndTypes(const vector &names, const vector &types) { vector columns; diff --git a/src/duckdb/src/include/duckdb/common/multi_file/multi_file_function.hpp b/src/duckdb/src/include/duckdb/common/multi_file/multi_file_function.hpp index 40a636668..c8bd0884e 100644 --- a/src/duckdb/src/include/duckdb/common/multi_file/multi_file_function.hpp +++ b/src/duckdb/src/include/duckdb/common/multi_file/multi_file_function.hpp @@ -1296,9 +1296,9 @@ class MultiFileFunction : public TableFunction { } } - static BindInfo MultiFileGetBindInfo(const optional_ptr bind_data_p) { + static BindInfo MultiFileGetBindInfo(TableFunctionGetBindInfoInput &input) { BindInfo bind_info(ScanType::EXTERNAL); - auto &bind_data = bind_data_p->Cast(); + auto &bind_data = input.bind_data->Cast(); vector file_path; for (const auto &file : bind_data.file_list->GetDisplayFileList()) { diff --git a/src/duckdb/src/include/duckdb/common/multi_file/multi_file_reader.hpp b/src/duckdb/src/include/duckdb/common/multi_file/multi_file_reader.hpp index 13d3bb7cf..0c03bfd79 100644 --- a/src/duckdb/src/include/duckdb/common/multi_file/multi_file_reader.hpp +++ b/src/duckdb/src/include/duckdb/common/multi_file/multi_file_reader.hpp @@ -101,7 +101,7 @@ struct MultiFileReader { // Reserved field id used for the "_last_updated_sequence_number" field according to the iceberg spec static constexpr int32_t LAST_UPDATED_SEQUENCE_NUMBER_ID = 2147483539; //! The field of a file STRUCT that holds the path of the file - all other fields are open options - static constexpr const char *FILE_PATH_FIELD = "filename"; + static constexpr const char *FILE_PATH_FIELD = OpenFileInfo::PATH_FIELD; public: virtual ~MultiFileReader(); diff --git a/src/duckdb/src/include/duckdb/common/multi_file/table_function_multi_file.hpp b/src/duckdb/src/include/duckdb/common/multi_file/table_function_multi_file.hpp index cae66d571..f6fc9b487 100644 --- a/src/duckdb/src/include/duckdb/common/multi_file/table_function_multi_file.hpp +++ b/src/duckdb/src/include/duckdb/common/multi_file/table_function_multi_file.hpp @@ -18,11 +18,8 @@ namespace duckdb { // How a single-file table function takes part in a multi-file scan //===--------------------------------------------------------------------===// //! What the multi-file scan tells the bind of the single-file function that reads one of its files - see -//! TableFunctionBindInput::multi_file_input +//! TableFunctionBindInput::multi_file_input. The file itself is the input of the bind: see GetFile struct TableFunctionFileBindInput { - //! The file to read, with the options it is opened with (e.g. its size or encryption key) - the path the bind - //! receives as its input does not carry those - optional_ptr file; //! (Optional) The schema this bind is expected to produce, when the schema of the scan was already determined - //! the bind should read the file using this schema instead of determining a schema of its own optional_ptr> expected_names; @@ -53,14 +50,17 @@ struct TableFunctionFileBindInput { static const TableFunctionFileBindInput EMPTY; return input.multi_file_input ? *input.multi_file_input : EMPTY; } - //! The file a single-file function reads - the file of the multi-file scan when it is part of one, the path it - //! was given otherwise + //! The file a single-file function reads: its first input, which is the path of the file or a file struct + //! that also holds the options to open the file with (e.g. its size or encryption key) - see + //! OpenFileInfo::FromValue. A multi-file scan passes every file it reads this way static OpenFileInfo GetFile(const TableFunctionBindInput &input) { - auto &file_input = Get(input); - if (file_input.file) { - return *file_input.file; + auto &file = input.inputs[0]; + auto type = file.type().id(); + if (type != LogicalTypeId::VARCHAR && type != LogicalTypeId::STRUCT && type != LogicalTypeId::VARIANT) { + throw BinderException("%s reads a single file - given as a VARCHAR path or as a file struct, not as %s", + input.table_function.GetName(), file.type().ToString()); } - return OpenFileInfo(StringValue::Get(input.inputs[0])); + return OpenFileInfo::FromValue(file, input.table_function.GetName()); } }; @@ -153,15 +153,6 @@ class TableFunctionFileReaderOptions : public BaseFileReaderOptions { bool multi_file_scan = false; }; -//! What the bind of a wrapped single-file table function reports about the file it binds, beyond its names and types -struct TableFunctionFileBindInfo { - //! The columns of the file, one per bound column, e.g. to attach the field ids of the columns and their nested - //! fields. Left empty, the columns are derived from the bound names and types - vector columns; - //! The key-value metadata of the file, exposed as the metadata of the reader of the file - InsertionOrderPreservingMap metadata; -}; - //! Bind data of a multi-file function that wraps a single-file table function struct TableFunctionMultiFileData : public TableFunctionData { TableFunctionFileReaderOptions options; @@ -341,7 +332,8 @@ class TableFunctionFileReader : public BaseFileReader { }; //! A MultiFileReaderInterface that turns a table function reading a single file into a multi-file table function. -//! The wrapped function must accept a single VARCHAR file path as its only positional parameter - all of its named +//! The wrapped function must accept the file as its only positional parameter: as ANY, to receive the file together +//! with the options to open it with (see TableFunctionFileBindInput::GetFile), or as a VARCHAR path - all of its named //! parameters are forwarded to it, and all multi-file options (union_by_name, hive partitioning, filename, ...) are //! provided by the multi-file framework on top of it. class TableFunctionMultiFileWrapper : public MultiFileReaderInterface { diff --git a/src/duckdb/src/include/duckdb/common/open_file_info.hpp b/src/duckdb/src/include/duckdb/common/open_file_info.hpp index 4db343dbf..ea4b85c89 100644 --- a/src/duckdb/src/include/duckdb/common/open_file_info.hpp +++ b/src/duckdb/src/include/duckdb/common/open_file_info.hpp @@ -9,6 +9,7 @@ #pragma once #include "duckdb/common/common.hpp" +#include "duckdb/common/identifier.hpp" #include "duckdb/common/shared_ptr.hpp" #include "duckdb/common/types/value.hpp" @@ -46,7 +47,17 @@ struct OpenFileInfo { string path; shared_ptr extended_info; + //! The field of a file struct that holds the path of the file + static constexpr const char *PATH_FIELD = "filename"; + public: + //! The file a value specifies: a VARCHAR path, or a STRUCT holding the path in its "filename" field and the + //! options to open the file with in its other fields (a VARIANT holding either is unpacked). Errors name the + //! function that was given the file + DUCKDB_API static OpenFileInfo FromValue(const Value &input, const Identifier &function_name); + //! The inverse of FromValue: the path as a VARCHAR when there are no options, a file struct otherwise + DUCKDB_API Value ToValue() const; + bool operator<(const OpenFileInfo &rhs) const { return path < rhs.path; } diff --git a/src/duckdb/src/include/duckdb/common/operator/cast_operators.hpp b/src/duckdb/src/include/duckdb/common/operator/cast_operators.hpp index 8666cf928..111b4f9b1 100644 --- a/src/duckdb/src/include/duckdb/common/operator/cast_operators.hpp +++ b/src/duckdb/src/include/duckdb/common/operator/cast_operators.hpp @@ -913,14 +913,14 @@ struct CastFromBlob { template <> duckdb::string_t CastFromBlob::Operation(duckdb::string_t input, StringHeap &heap); -struct CastFromBlobToBit { - template - static inline string_t Operation(SRC input, StringHeap &heap) { +struct TryCastBlobToBit { + template + static inline bool Operation(SRC input, DST &result, Vector &result_vector, CastParameters ¶meters) { throw NotImplementedException("Cast from blob could not be performed!"); } }; template <> -string_t CastFromBlobToBit::Operation(string_t input, StringHeap &heap); +bool TryCastBlobToBit::Operation(string_t input, string_t &result, Vector &result_vector, CastParameters ¶meters); struct TryCastToBlob { template diff --git a/src/duckdb/src/include/duckdb/common/operator/interpolate.hpp b/src/duckdb/src/include/duckdb/common/operator/interpolate.hpp index 00e75039a..18d9b0f08 100644 --- a/src/duckdb/src/include/duckdb/common/operator/interpolate.hpp +++ b/src/duckdb/src/include/duckdb/common/operator/interpolate.hpp @@ -28,6 +28,8 @@ double InterpolateOperator::Operation(const double &lo, const double d, const do template <> dtime_t InterpolateOperator::Operation(const dtime_t &lo, const double d, const dtime_t &hi); template <> +dtime_tz_t InterpolateOperator::Operation(const dtime_tz_t &lo, const double d, const dtime_tz_t &hi); +template <> timestamp_t InterpolateOperator::Operation(const timestamp_t &lo, const double d, const timestamp_t &hi); template <> hugeint_t InterpolateOperator::Operation(const hugeint_t &lo, const double d, const hugeint_t &hi); diff --git a/src/duckdb/src/include/duckdb/common/optional_idx.hpp b/src/duckdb/src/include/duckdb/common/optional_idx.hpp index d2da740bb..48a6c165a 100644 --- a/src/duckdb/src/include/duckdb/common/optional_idx.hpp +++ b/src/duckdb/src/include/duckdb/common/optional_idx.hpp @@ -16,12 +16,11 @@ class optional_idx { static constexpr const idx_t INVALID_INDEX = idx_t(-1); public: - optional_idx() : index(INVALID_INDEX) { + constexpr optional_idx() : index(INVALID_INDEX) { } - optional_idx(idx_t index) : index(index) { // NOLINT: allow implicit conversion from idx_t - if (index == INVALID_INDEX) { - ThrowInvalidInitialization(); - } + // NOLINTNEXTLINE: allow implicit conversion from idx_t + constexpr optional_idx(idx_t index_p) + : index(index_p == INVALID_INDEX ? (ThrowInvalidInitialization(), index_p) : index_p) { } static optional_idx Invalid() { diff --git a/src/duckdb/src/include/duckdb/common/string_util.hpp b/src/duckdb/src/include/duckdb/common/string_util.hpp index c42d51bc5..037eada55 100644 --- a/src/duckdb/src/include/duckdb/common/string_util.hpp +++ b/src/duckdb/src/include/duckdb/common/string_util.hpp @@ -198,7 +198,7 @@ class StringUtil { //! Join multiple items of container with given size, transformed to string //! using function, into one string using the given separator template - static string Join(const C &input, S count, const string &separator, FUNC f) { + static string Join(const C &input, S count, const string &separator, const FUNC &f) { // The result std::string result; @@ -364,6 +364,9 @@ class StringUtil { DUCKDB_API static string GetFileExtension(const string &file_name); DUCKDB_API static string GetFileStem(const string &file_name); DUCKDB_API static string GetFilePath(const string &file_path); + //! Replace the file name of a path, keeping its directory part as written (also for files directly below a + //! scheme root, where splitting into GetFilePath and JoinPath would not round-trip: "opfs://a" -> "opfs://b") + DUCKDB_API static string ReplaceFileName(const string &file_path, const string &file_name); struct EnumStringLiteral { uint32_t number; diff --git a/src/duckdb/src/include/duckdb/common/types/column/column_data_collection.hpp b/src/duckdb/src/include/duckdb/common/types/column/column_data_collection.hpp index 137d93e2b..cf1c59792 100644 --- a/src/duckdb/src/include/duckdb/common/types/column/column_data_collection.hpp +++ b/src/duckdb/src/include/duckdb/common/types/column/column_data_collection.hpp @@ -23,6 +23,7 @@ class ColumnDataRowCollection; //! The ColumnDataCollection represents a set of (buffer-managed) data stored in columnar format //! It is efficient to read and scan +//! A collection with no columns still stores row counts. class ColumnDataCollection { public: //! Constructs an in-memory column data collection from an allocator diff --git a/src/duckdb/src/include/duckdb/common/types/geometry.hpp b/src/duckdb/src/include/duckdb/common/types/geometry.hpp index 1d673c17f..20c8d3cf4 100644 --- a/src/duckdb/src/include/duckdb/common/types/geometry.hpp +++ b/src/duckdb/src/include/duckdb/common/types/geometry.hpp @@ -154,51 +154,34 @@ class GeometryExtent { } void Extend(const VertexXY &vertex) { - x_min = MinValue(x_min, vertex.x); - x_max = MaxValue(x_max, vertex.x); - y_min = MinValue(y_min, vertex.y); - y_max = MaxValue(y_max, vertex.y); + ExtendAxis(x_min, x_max, vertex.x); + ExtendAxis(y_min, y_max, vertex.y); } void Extend(const VertexXYZ &vertex) { - x_min = MinValue(x_min, vertex.x); - x_max = MaxValue(x_max, vertex.x); - y_min = MinValue(y_min, vertex.y); - y_max = MaxValue(y_max, vertex.y); - z_min = MinValue(z_min, vertex.z); - z_max = MaxValue(z_max, vertex.z); + ExtendAxis(x_min, x_max, vertex.x); + ExtendAxis(y_min, y_max, vertex.y); + ExtendAxis(z_min, z_max, vertex.z); } void Extend(const VertexXYM &vertex) { - x_min = MinValue(x_min, vertex.x); - x_max = MaxValue(x_max, vertex.x); - y_min = MinValue(y_min, vertex.y); - y_max = MaxValue(y_max, vertex.y); - m_min = MinValue(m_min, vertex.m); - m_max = MaxValue(m_max, vertex.m); + ExtendAxis(x_min, x_max, vertex.x); + ExtendAxis(y_min, y_max, vertex.y); + ExtendAxis(m_min, m_max, vertex.m); } void Extend(const VertexXYZM &vertex) { - x_min = MinValue(x_min, vertex.x); - x_max = MaxValue(x_max, vertex.x); - y_min = MinValue(y_min, vertex.y); - y_max = MaxValue(y_max, vertex.y); - z_min = MinValue(z_min, vertex.z); - z_max = MaxValue(z_max, vertex.z); - m_min = MinValue(m_min, vertex.m); - m_max = MaxValue(m_max, vertex.m); + ExtendAxis(x_min, x_max, vertex.x); + ExtendAxis(y_min, y_max, vertex.y); + ExtendAxis(z_min, z_max, vertex.z); + ExtendAxis(m_min, m_max, vertex.m); } void Merge(const GeometryExtent &other) { - x_min = MinValue(x_min, other.x_min); - y_min = MinValue(y_min, other.y_min); - z_min = MinValue(z_min, other.z_min); - m_min = MinValue(m_min, other.m_min); - - x_max = MaxValue(x_max, other.x_max); - y_max = MaxValue(y_max, other.y_max); - z_max = MaxValue(z_max, other.z_max); - m_max = MaxValue(m_max, other.m_max); + MergeAxis(x_min, x_max, other.x_min, other.x_max); + MergeAxis(y_min, y_max, other.y_min, other.y_max); + MergeAxis(z_min, z_max, other.z_min, other.z_max); + MergeAxis(m_min, m_max, other.m_min, other.m_max); } bool IntersectsXY(const GeometryExtent &other) const { @@ -214,6 +197,29 @@ class GeometryExtent { return x_min <= other.x_min && x_max >= other.x_max && y_min <= other.y_min && y_max >= other.y_max; } +private: + //! A NaN coordinate cannot be placed on the axis, so it makes the range of the axis unknown + static void ExtendAxis(double &min, double &max, double value) { + if (std::isnan(value)) { + min = UNKNOWN_MIN; + max = UNKNOWN_MAX; + return; + } + min = MinValue(min, value); + max = MaxValue(max, value); + } + + static void MergeAxis(double &min, double &max, double other_min, double other_max) { + if (std::isnan(other_min) || std::isnan(other_max)) { + min = UNKNOWN_MIN; + max = UNKNOWN_MAX; + return; + } + min = MinValue(min, other_min); + max = MaxValue(max, other_max); + } + +public: double x_min; double y_min; double z_min; diff --git a/src/duckdb/src/include/duckdb/common/types/row/partitioned_tuple_data.hpp b/src/duckdb/src/include/duckdb/common/types/row/partitioned_tuple_data.hpp index 81400b3df..e72bd229a 100644 --- a/src/duckdb/src/include/duckdb/common/types/row/partitioned_tuple_data.hpp +++ b/src/duckdb/src/include/duckdb/common/types/row/partitioned_tuple_data.hpp @@ -135,6 +135,8 @@ class PartitionedTupleData { idx_t Count() const; //! Get the size (in bytes) of this PartitionedTupleData idx_t SizeInBytes() const; + //! Buffer-manager memory charged for row and heap blocks, plus allocator metadata + idx_t GetAllocatedSizeInBytes() const; //! Get the number of partitions of this PartitionedTupleData idx_t PartitionCount() const; //! Get the count and size of the largest partition diff --git a/src/duckdb/src/include/duckdb/common/types/row/tuple_data_allocator.hpp b/src/duckdb/src/include/duckdb/common/types/row/tuple_data_allocator.hpp index f648f0d18..7fc8eafcc 100644 --- a/src/duckdb/src/include/duckdb/common/types/row/tuple_data_allocator.hpp +++ b/src/duckdb/src/include/duckdb/common/types/row/tuple_data_allocator.hpp @@ -8,6 +8,7 @@ #pragma once +#include "duckdb/common/atomic.hpp" #include "duckdb/common/types/row/tuple_data_layout.hpp" #include "duckdb/common/types/row/tuple_data_states.hpp" #include "duckdb/common/arena_containers/arena_vector.hpp" @@ -78,6 +79,8 @@ class TupleDataAllocator { idx_t RowBlockCount() const; //! Number of heap blocks idx_t HeapBlockCount() const; + //! Cached allocation size of retained row and heap blocks + idx_t GetBlockAllocationSize() const; //! Sets the partition index of this tuple data allocator void SetPartitionIndex(idx_t index); //! Gets the partition index of this tuple data allocator @@ -122,11 +125,9 @@ class TupleDataAllocator { unsafe_vector> &parts, optional_ptr sort_key_payload_state = nullptr); //! Internal function for ReleaseOrStoreHandles - static void ReleaseOrStoreHandlesInternal(TupleDataSegment &segment, - unsafe_arena_vector &pinned_row_handles, - buffer_handle_map_t &handles, const ContinuousIdSet &block_ids, - unsafe_arena_vector &blocks, - TupleDataPinProperties properties); + void ReleaseOrStoreHandlesInternal(TupleDataSegment &segment, unsafe_arena_vector &pinned_row_handles, + buffer_handle_map_t &handles, const ContinuousIdSet &block_ids, + unsafe_arena_vector &blocks, TupleDataPinProperties properties); //! Create a row/heap block, store the handle in pin_state so the block stays pinned void CreateRowBlock(TupleDataSegment &segment, TupleDataPinState &pin_state); void CreateHeapBlock(TupleDataSegment &segment, TupleDataPinState &pin_state, idx_t size); @@ -157,6 +158,8 @@ class TupleDataAllocator { unsafe_arena_vector row_blocks; //! Blocks storing the variable-size data of the fixed-size rows (e.g., string, list) unsafe_arena_vector heap_blocks; + //! Blocks can be released by parallel scans of separate segments + atomic block_allocation_size {0}; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/common/types/row/tuple_data_collection.hpp b/src/duckdb/src/include/duckdb/common/types/row/tuple_data_collection.hpp index 5a5ea1785..5b5b468ae 100644 --- a/src/duckdb/src/include/duckdb/common/types/row/tuple_data_collection.hpp +++ b/src/duckdb/src/include/duckdb/common/types/row/tuple_data_collection.hpp @@ -82,6 +82,8 @@ class TupleDataCollection { idx_t ChunkCount() const; //! The size (in bytes) of the blocks held by this tuple data collection idx_t SizeInBytes() const; + //! Buffer-manager memory charged for row and heap blocks + idx_t GetBlockAllocationSize() const; //! Unpins all held pins void Unpin(); //! Sets the partition index of this tuple data collection diff --git a/src/duckdb/src/include/duckdb/common/types/value.hpp b/src/duckdb/src/include/duckdb/common/types/value.hpp index ce6d14793..7dcfca937 100644 --- a/src/duckdb/src/include/duckdb/common/types/value.hpp +++ b/src/duckdb/src/include/duckdb/common/types/value.hpp @@ -86,6 +86,8 @@ class Value { inline bool IsNull() const { return is_null; } + //! Whether the value is within the domain of its type (e.g. valid UTF-8, ENUM index within the dictionary) + DUCKDB_API bool IsValid() const; //! Create the lowest possible value of a given type (numeric only) DUCKDB_API static Value MinimumValue(const LogicalType &type); diff --git a/src/duckdb/src/include/duckdb/execution/aggregate_hashtable.hpp b/src/duckdb/src/include/duckdb/execution/aggregate_hashtable.hpp index 2ec91041e..696ee5b68 100644 --- a/src/duckdb/src/include/duckdb/execution/aggregate_hashtable.hpp +++ b/src/duckdb/src/include/duckdb/execution/aggregate_hashtable.hpp @@ -11,10 +11,10 @@ #include "duckdb/common/row_operations/row_matcher.hpp" #include "duckdb/common/types/row/partitioned_tuple_data.hpp" #include "duckdb/execution/base_aggregate_hashtable.hpp" +#include "duckdb/execution/aggregate_ht_adaptivity_state.hpp" #include "duckdb/execution/ht_entry.hpp" #include "duckdb/storage/arena_allocator.hpp" #include "duckdb/common/row_operations/row_operations.hpp" -#include "duckdb/common/types/hyperloglog.hpp" #include "duckdb/common/clustered_aggregate.hpp" #include "duckdb/common/atomic.hpp" @@ -145,12 +145,14 @@ class GroupedAggregateHashTable : public BaseAggregateHashTable { idx_t group_count, DataChunk &result) const; const PartitionedTupleData &GetPartitionedData() const; + //! Retained tuple blocks, allocator metadata, aggregate arena, and pointer table + idx_t GetSizeInBytes() const; unique_ptr AcquirePartitionedData(); void Abandon(); void Repartition(); shared_ptr GetAggregateAllocator(); - //! Resize the HT to the specified size. Must be larger than the current size. + //! Resize an empty HT, or grow an HT with no abandoned groups. void Resize(idx_t size); //! Resets the pointer table of the HT to all 0's void ClearPointerTable(); @@ -158,18 +160,18 @@ class GroupedAggregateHashTable : public BaseAggregateHashTable { void SetRadixBits(idx_t radix_bits); //! Get the radix bits for this HT idx_t GetRadixBits() const; - //! Get the total number of tuples sunk into this HT - idx_t GetSinkCount() const; //! Get the total number of tuples materialized currently in this HT idx_t GetMaterializedCount() const; - //! Skips lookups from here on out + //! Read-only adaptivity observations; mutations follow the HT's lifecycle. + const AggregateHTAdaptivityState &GetAdaptivityState() const { + return adaptivity; + } + //! Skip lookups until resuming at an empty pointer table void SkipLookups(); - //! Enable/disable HLL + //! Resume lookups after abandoning the previous pointer table. + void ResumeLookups(); + //! Enable/disable HLL; a fresh sketch must start before inserting groups void EnableHLL(bool enable); - //! Whether HLL is enabled - bool HLLEnabled() const; - //! Get HLL count - idx_t GetHLLUpperBound() const; //! Executes the filter(if any) and update the aggregates void Combine(GroupedAggregateHashTable &other); @@ -215,6 +217,12 @@ class GroupedAggregateHashTable : public BaseAggregateHashTable { //! The data of the HT unique_ptr partitioned_data; unique_ptr unpartitioned_data; + //! Cache tuple allocations while the materialized groups and their storage stay unchanged. + struct TupleSizeCache { + optional_idx count; + idx_t size = 0; + }; + mutable TupleSizeCache tuple_size_cache; //! Predicates for matching groups (always ExpressionType::COMPARE_EQUAL) vector predicates; @@ -231,14 +239,7 @@ class GroupedAggregateHashTable : public BaseAggregateHashTable { //! Bitmask for getting relevant bits from the hashes to determine the position hash_t bitmask; - //! How many tuples went into this HT (before de-duplication) - idx_t sink_count; - //! If true, we just append, skipping HT lookups - bool skip_lookups; - //! Whether to enable HLL counting the hashes - bool enable_hll; - //! The associated HLL - HyperLogLog hll; + AggregateHTAdaptivityState adaptivity; //! The active arena allocator used by the aggregates for their internal state shared_ptr aggregate_allocator; diff --git a/src/duckdb/src/include/duckdb/execution/aggregate_ht_adaptivity_state.hpp b/src/duckdb/src/include/duckdb/execution/aggregate_ht_adaptivity_state.hpp new file mode 100644 index 000000000..355bf18cc --- /dev/null +++ b/src/duckdb/src/include/duckdb/execution/aggregate_ht_adaptivity_state.hpp @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/execution/aggregate_ht_adaptivity_state.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/common/types/hyperloglog.hpp" +#include "duckdb/common/vector/vector_iterator.hpp" + +namespace duckdb { + +//! Observations and lookup mode owned by a grouped aggregate HT. +class AggregateHTAdaptivityState { +public: + idx_t GetSinkCount() const { + return sink_count; + } + //! Logical input since the current pointer table was cleared + idx_t GetCycleInputCount() const { + return sink_count - sink_count_at_abandon; + } + bool LookupsSkipped() const { + return skip_lookups; + } + //! Logical input since lookups were skipped + idx_t GetSkippedInputCount() const { + return skip_lookups ? sink_count - sink_count_at_skip : 0; + } + bool HLLEnabled() const { + return enable_hll; + } + idx_t GetHLLUpperBound() const { + D_ASSERT(enable_hll); + return LossyNumericCast((1 + HyperLogLogP<8>::GetErrorRate()) * static_cast(hll.Count())); + } + +private: + friend class GroupedAggregateHashTable; + + void AddInput(idx_t count) { + sink_count += count; + } + void BeginCycle() { + sink_count_at_abandon = sink_count; + } + void SkipLookups() { + skip_lookups = true; + sink_count_at_skip = sink_count; + } + void ResumeLookups() { + skip_lookups = false; + } + void EnableHLL(bool enable) { + enable_hll = enable; + } + void ObserveHashes(const Vector &hashes) { + D_ASSERT(enable_hll); + hll.Update(hashes); + } + void ObserveHashes(const VectorIterator &hashes, const SelectionVector &new_groups, idx_t count) { + D_ASSERT(enable_hll); + // Lookup hits were already observed when their groups were inserted. + for (idx_t i = 0; i < count; i++) { + hll.InsertElement(hashes[new_groups.get_index(i)].GetValue()); + } + } + void Reset() { + *this = AggregateHTAdaptivityState(); + } + +private: + //! How many tuples went into this HT (before de-duplication) + idx_t sink_count = 0; + //! Start of the current pointer-table fill cycle + idx_t sink_count_at_abandon = 0; + //! If true, we just append, skipping HT lookups + bool skip_lookups = false; + //! Start of the current append-only interval + idx_t sink_count_at_skip = 0; + //! Whether to enable HLL counting the hashes + bool enable_hll = false; + HyperLogLogP<8> hll; +}; + +} // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/execution/index/fixed_size_buffer.hpp b/src/duckdb/src/include/duckdb/execution/index/fixed_size_buffer.hpp index a78cb9e77..b246c7578 100644 --- a/src/duckdb/src/include/duckdb/execution/index/fixed_size_buffer.hpp +++ b/src/duckdb/src/include/duckdb/execution/index/fixed_size_buffer.hpp @@ -8,6 +8,7 @@ #pragma once +#include "duckdb/common/typedefs.hpp" #include "duckdb/storage/partial_block_manager.hpp" #include "duckdb/storage/buffer/block_handle.hpp" #include "duckdb/storage/buffer/buffer_handle.hpp" @@ -82,8 +83,10 @@ class FixedSizeBuffer { //! Load a buffer from disk, if not in memory. void LoadFromDisk(); - //! Returns the first free offset in a bitmask - uint32_t GetOffset(const idx_t bitmask_count, const idx_t available_segments); + //! Reserves a free slot in the buffer and returns the corresponding offset. + uint32_t AllocateSegment(const idx_t bitmask_count, const idx_t available_segments); + //! Frees the slot at the given offset in the current buffer. + void FreeSegment(const idx_t offset, const idx_t available_segments); //! Sets the allocation size, if dirty void SetAllocationSize(const idx_t available_segments, const idx_t segment_size, const idx_t bitmask_offset); diff --git a/src/duckdb/src/include/duckdb/execution/index/unbound_index.hpp b/src/duckdb/src/include/duckdb/execution/index/unbound_index.hpp index 362681994..c567fd9ad 100644 --- a/src/duckdb/src/include/duckdb/execution/index/unbound_index.hpp +++ b/src/duckdb/src/include/duckdb/execution/index/unbound_index.hpp @@ -61,7 +61,7 @@ struct BufferedIndexReplays { } }; -class UnboundIndex final : public Index { +class UnboundIndex : public Index { private: //! The CreateInfo of the index. unique_ptr create_info; diff --git a/src/duckdb/src/include/duckdb/execution/physical_plan_generator.hpp b/src/duckdb/src/include/duckdb/execution/physical_plan_generator.hpp index 6478ea265..efccfac14 100644 --- a/src/duckdb/src/include/duckdb/execution/physical_plan_generator.hpp +++ b/src/duckdb/src/include/duckdb/execution/physical_plan_generator.hpp @@ -206,7 +206,7 @@ class PhysicalPlanGenerator { bool PreserveInsertionOrder(PhysicalOperator &plan); bool UseBatchIndex(PhysicalOperator &plan); optional_ptr PlanAsOfLoopJoin(LogicalComparisonJoin &op, PhysicalOperator &probe, - PhysicalOperator &build); + PhysicalOperator &build, const idx_t join_cardinality); optional_ptr PlanAsOfInequalityJoin(LogicalComparisonJoin &op, PhysicalOperator &probe, PhysicalOperator &build, const idx_t lhs_cardinality, const idx_t rhs_cardinality); diff --git a/src/duckdb/src/include/duckdb/execution/radix_ht_adaptivity.hpp b/src/duckdb/src/include/duckdb/execution/radix_ht_adaptivity.hpp new file mode 100644 index 000000000..23ce5ef97 --- /dev/null +++ b/src/duckdb/src/include/duckdb/execution/radix_ht_adaptivity.hpp @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/execution/radix_ht_adaptivity.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/common/common.hpp" + +namespace duckdb { + +class RadixHTGlobalSinkState; +class RadixHTLocalSinkState; + +//! Policy for the local sink HT. Called at fill-cycle boundaries without memory pressure. +class RadixHTAdaptivity { +public: + //! Grow a profitable table within the memory budget, resuming lookups on success. + static bool TryGrow(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate); + //! Consider skipping lookups using the completed cycle, before abandonment. + static void MaybeSkipLookups(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate); + //! Periodically retry lookups at the existing capacity, after abandonment. + static void MaybeResumeLookups(RadixHTGlobalSinkState &gstate, RadixHTLocalSinkState &lstate); + + //! Minimum input before skipping lookups and between subsequent retries + static constexpr idx_t LOOKUP_SAMPLE_SIZE = 1048576; + +private: + static constexpr double GROWTH_DUPLICATE_BYTES_FACTOR = 4.0; + static constexpr double MINIMUM_SMALL_TABLE_REDUCTION = 0.05; +}; + +} // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/execution/radix_ht_sink_state.hpp b/src/duckdb/src/include/duckdb/execution/radix_ht_sink_state.hpp new file mode 100644 index 000000000..689bfd886 --- /dev/null +++ b/src/duckdb/src/include/duckdb/execution/radix_ht_sink_state.hpp @@ -0,0 +1,183 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/execution/radix_ht_sink_state.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/execution/radix_partitioned_hashtable.hpp" +#include "duckdb/execution/aggregate_hashtable.hpp" +#include "duckdb/execution/aggregate_state_spilling.hpp" +#include "duckdb/storage/temporary_memory_manager.hpp" + +namespace duckdb { + +class RadixHTGlobalSinkState; + +//! How aggregate state spilling and native row combining are allowed to interleave +enum class SpillPhase { + //! Rows may still be combined natively, and the exported width may still grow beyond it + NATIVE_ALLOWED, + //! The exported width grew beyond the native width: every combine must export its rows + EXPORTED_ONLY, + //! Rows were combined natively: the exported width can no longer grow beyond the native width + NATIVE_COMBINE_STARTED +}; + +struct RadixHTConfig { +public: + explicit RadixHTConfig(RadixHTGlobalSinkState &sink); + + void Reset(); + void SetRadixBits(const idx_t &radix_bits_p); + bool SetRadixBitsToExternal(); + idx_t GetRadixBits() const; + idx_t GetMaximumSinkRadixBits() const; + +private: + void SetRadixBitsInternal(idx_t radix_bits_p, bool external); + idx_t InitialSinkRadixBits() const; + idx_t ExternalRadixBits(bool dynamic) const; + idx_t MaximumSinkRadixBits() const; + idx_t SinkCapacity() const; + +private: + //! The global sink state + RadixHTGlobalSinkState &sink; + +public: + //! Width of tuples + const idx_t row_width; + //! Capacity of HTs during the Sink + const idx_t sink_capacity; + +private: + //! Sink radix bits to initialize with + static constexpr idx_t MAXIMUM_INITIAL_SINK_RADIX_BITS = 4; + +public: + //! Maximum Sink radix bits (independent of threads) + static constexpr idx_t MAXIMUM_FINAL_SINK_RADIX_BITS = 8; + +private: + //! Current thread-global sink radix bits + atomic sink_radix_bits; + //! Maximum Sink radix bits (set based on number of threads, if not external) + const idx_t maximum_sink_radix_bits; + + //! Thresholds at which we reduce the sink radix bits + //! This needed to reduce cache misses when we have very wide rows + static constexpr idx_t ROW_WIDTH_THRESHOLD_ONE = 32; + static constexpr idx_t ROW_WIDTH_THRESHOLD_TWO = 64; + +public: + //! If we have this many or less threads, we grow the HT, otherwise we abandon + static constexpr idx_t GROW_STRATEGY_THREAD_THRESHOLD = 2; + //! If we fill this many blocks per partition, we trigger a repartition + static constexpr double BLOCK_FILL_FACTOR = 0.5; + //! By how many bits to repartition if a repartition is triggered + static constexpr idx_t REPARTITION_RADIX_BITS = 2; + //! Thread-limit divisor for state export and exported partition sizing + static constexpr idx_t AGGREGATE_STATE_SPILL_DIVISOR = 8; + //! Arena-only pressure that forces external aggregation + static constexpr idx_t AGGREGATE_STATE_PRESSURE_DIVISOR = 2; + //! Estimated memory amplification when importing exported states + static constexpr idx_t EXPORTED_STATE_MEMORY_MULTIPLIER = 2; +}; + +class RadixHTGlobalSinkState : public GlobalSinkState { +public: + RadixHTGlobalSinkState(ClientContext &context, const RadixPartitionedHashTable &radix_ht); + + //! Destroys aggregate states (if multi-scan) + ~RadixHTGlobalSinkState() override; + void Destroy(); + +public: + idx_t GetThreadLimit() const { + return temporary_memory_state->GetReservation() / number_of_threads / 10 * 8; + } + //! Whether the aggregate arena alone approaches the thread memory limit + bool StatePressureExceeded(GroupedAggregateHashTable &ht) const; + +public: + ClientContext &context; + //! Temporary memory state for managing this hash table's memory usage + unique_ptr temporary_memory_state; + atomic minimum_reservation; + + //! Whether we've called Finalize + bool finalized; + //! Whether we are doing an external aggregation + atomic external; + //! Threads that have called Sink + atomic active_threads; + //! Number of threads (from TaskScheduler) + const idx_t number_of_threads; + //! Memory limit (from BufferManager) + const idx_t memory_limit; + //! Block size (from BufferManager) + const idx_t block_alloc_size; + //! If any thread has called combine + atomic any_combined; + //! If any thread has called ht.Abandon() during Sink (meaning uncombined_data may have duplicates) + atomic any_abandoned; + + //! The radix HT + const RadixPartitionedHashTable &radix_ht; + //! Config for partitioning + RadixHTConfig config; + + //! Uncombined partitioned data that will be put into the AggregatePartitions + unique_ptr uncombined_data; + //! The spill metadata of the aggregate layout, set if the states can spill + unique_ptr spill_plan; + //! Synchronizes the transition to exported-only aggregation with concurrent combines + SpillPhase spill_phase DUCKDB_GUARDED_BY(lock); + //! Uncombined exported data, aligned one-to-one with the partitions of uncombined_data + vector> uncombined_exported_data; + //! Allocators used during the Sink/Finalize + vector> stored_allocators; + idx_t stored_allocators_size; + + //! Partitions that are finalized during GetData + vector> partitions; + //! For keeping track of progress + atomic finalize_done; + + //! Pin properties when scanning + TupleDataPinProperties scan_pin_properties; + //! Total count before combining + idx_t count_before_combining; + //! Maximum partition size if all unique + idx_t max_partition_size; +}; + +class RadixHTLocalSinkState : public LocalSinkState { +public: + RadixHTLocalSinkState(ClientContext &context, const RadixPartitionedHashTable &radix_ht); + void ResetForReuse(const RadixPartitionedHashTable &radix_ht, RadixHTGlobalSinkState &gstate); + void PrepareForSpill(RadixHTGlobalSinkState &gstate); + +public: + //! Thread-local HT that is re-used after abandoning + unique_ptr ht; + //! Chunk with group columns + DataChunk group_chunk; + + //! Whether this local state has already registered itself as active for the current iteration + bool registered; + //! Whether this local table has entered spilling or state export + bool spilling = false; + //! Sink capacity for this thread + idx_t local_sink_capacity; + //! Data that is abandoned ends up here (only if we're doing external aggregation) + unique_ptr abandoned_data; + //! Exported abandoned aggregate states, aligned one-to-one with the partitions of abandoned_data + vector> abandoned_exported_data; +}; + +} // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/function/aggregate_function.hpp b/src/duckdb/src/include/duckdb/function/aggregate_function.hpp index 4f8a09b95..f8b4ef6e5 100644 --- a/src/duckdb/src/include/duckdb/function/aggregate_function.hpp +++ b/src/duckdb/src/include/duckdb/function/aggregate_function.hpp @@ -737,6 +737,10 @@ class AggregateFunction : public BaseAggregateFunction, public SimpleFunction { template static void WireStructStateType(AggregateFunction &result); + //! The state layout of a STATE with a STATE_TYPE - defined out-of-line (after BoundAggregateFunction is complete) + template + static AggregateStateLayout StructStateLayout(AggregateLayoutInput &input); + template static idx_t StateSize(AggregateStateInput &) { return sizeof(STATE); @@ -949,22 +953,7 @@ class BoundAggregateFunction : public BaseAggregateFunction, public BoundSimpleF template inline void AggregateFunction::WireStructStateType(AggregateFunction &result) { if constexpr (HasStructStateType::value) { - using ST = typename STATE::STATE_TYPE; - result.SetStructStateExport([](AggregateLayoutInput &input) { - auto &bound = input.function; - AggregateStateLayout layout; - if (bound.GetReturnType().IsAggregateState()) { - // the function has been modified for state export (see ExportAggregateFunction::SetStateExport) - - // its return type IS the state type already - layout.type = bound.GetReturnType(); - } else { - layout.type = AggregateFunction::BuildStateLogical(bound); - } - layout.total_state_size = AlignValue(sizeof(STATE)); - layout.field = BuildStateField(); - AggregateStateField::PopulateListFunctions(layout.type, layout.field); - return layout; - }); + result.SetStructStateExport(StructStateLayout); } else if constexpr (HasPrimitiveLogicalType::value) { result.SetStructStateExport([](AggregateLayoutInput &) { return AggregateStateLayout(PrimitiveToLogicalType(), AlignValue(sizeof(STATE))); @@ -972,6 +961,24 @@ inline void AggregateFunction::WireStructStateType(AggregateFunction &result) { } } +template +inline AggregateStateLayout AggregateFunction::StructStateLayout(AggregateLayoutInput &input) { + using ST = typename STATE::STATE_TYPE; + auto &bound = input.function; + AggregateStateLayout layout; + if (bound.GetReturnType().IsAggregateState()) { + // the function has been modified for state export (see ExportAggregateFunction::SetStateExport) - + // its return type IS the state type already + layout.type = bound.GetReturnType(); + } else { + layout.type = AggregateFunction::BuildStateLogical(bound); + } + layout.total_state_size = AlignValue(sizeof(STATE)); + layout.field = BuildStateField(); + AggregateStateField::PopulateListFunctions(layout.type, layout.field); + return layout; +} + // Defined here (after BoundAggregateFunction is complete) so the body can access the bound function's types. template inline LogicalType AggregateFunction::BuildStateLogical(const BoundAggregateFunction &bound_function) { diff --git a/src/duckdb/src/include/duckdb/function/aggregate_state_layout.hpp b/src/duckdb/src/include/duckdb/function/aggregate_state_layout.hpp index e7c5a137d..b30423af4 100644 --- a/src/duckdb/src/include/duckdb/function/aggregate_state_layout.hpp +++ b/src/duckdb/src/include/duckdb/function/aggregate_state_layout.hpp @@ -248,6 +248,8 @@ struct AggregateStateField { //! The segment functions used to read/write the linked list - only set when kind is LIST //! (populated by PopulateListFunctions, which requires the resolved logical type) ListSegmentFunctions list_functions; + //! For LIST: whether the linked list cannot contain NULL elements - importing a NULL element is an error + bool reject_null_elements = false; //! The alignment of this field when placed as a struct member, mirroring the C++ struct layout rules. //! For OPTIONAL_VALUE the alignment is that of the wrapped value - the trailing is_set bool does not affect it. @@ -488,6 +490,9 @@ AggregateStateField BuildStateField() { //! field.children=[{kind=STRUCT, field_offset=0, children=[struct fields]}]. //! - Non-optional struct: field.kind=STRUCT, field.field_offset=0, field.children=[struct fields]. //! total_state_size is the aligned stride between consecutive states in a packed buffer. +//! Checks `count` imported states, `stride` bytes apart - throws if a state is invalid +typedef void (*aggregate_validate_state_t)(const_data_ptr_t states, idx_t count, idx_t stride); + struct AggregateStateLayout { AggregateStateLayout() = default; AggregateStateLayout(LogicalType type_p, idx_t total_state_size_p, bool is_optional = false) @@ -518,6 +523,8 @@ struct AggregateStateLayout { idx_t total_state_size = 0; //! Constant values for arguments that must be re-bound with a specific constant rather than only the type unordered_map constant_parameters; + //! Checks the imported states for invariants the fields cannot express (e.g. between fields) + aggregate_validate_state_t validate_state = nullptr; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/function/function_binder.hpp b/src/duckdb/src/include/duckdb/function/function_binder.hpp index e604d0b75..413e6ea8a 100644 --- a/src/duckdb/src/include/duckdb/function/function_binder.hpp +++ b/src/duckdb/src/include/duckdb/function/function_binder.hpp @@ -28,6 +28,14 @@ struct SortedAggregateStateOrder { OrderByNullType null_order; }; +//! A scalar function with its argument and return types resolved, before the bind callback is invoked +struct ResolvedScalarFunctionTypes { + BoundScalarFunction bound_function; + vector argument_names; + //! The collation pushed into the arguments, to be propagated to the return type after binding + string pushed_collation; +}; + //! Overload selection: picks the cheapest matching overload out of a function set, given the argument types of a //! call. Free-standing so that callers without a ClientContext - which only limits the set of implicit casts //! considered, can select overloads too. @@ -332,6 +340,11 @@ class FunctionBinder { } private: + //! Resolves the argument / return types of the function and casts the children to them - everything that happens + //! before the bind callback is invoked + ResolvedScalarFunctionTypes + ResolveScalarFunctionTypes(shared_ptr function, vector> &children, + vector>> &keyword_args); //! Cast a set of expressions to the arguments of this function void CastToFunctionArguments(BoundSimpleFunction &function, vector> &children); diff --git a/src/duckdb/src/include/duckdb/function/scalar/strftime_format.hpp b/src/duckdb/src/include/duckdb/function/scalar/strftime_format.hpp index 9bf059d53..d4fcacd70 100644 --- a/src/duckdb/src/include/duckdb/function/scalar/strftime_format.hpp +++ b/src/duckdb/src/include/duckdb/function/scalar/strftime_format.hpp @@ -128,6 +128,8 @@ struct StrfTimeFormat : public StrTimeFormat { // NOLINT: work-around bug in cla char *WritePadded2(char *target, uint32_t value) const; char *WritePadded3(char *target, uint32_t value) const; char *WritePadded(char *target, uint32_t value, size_t padding) const; + //! Write a year - padded to 4 digits within [0, 9999], and in full (with its sign) otherwise + char *WriteYear(char *target, int32_t year) const; bool IsDateSpecifier(StrTimeSpecifier specifier); char *WriteDateSpecifier(StrTimeSpecifier specifier, date_t date, char *target) const; char *WriteStandardSpecifier(StrTimeSpecifier specifier, int32_t data[], const char *tz_name, size_t tz_len, diff --git a/src/duckdb/src/include/duckdb/function/table_function.hpp b/src/duckdb/src/include/duckdb/function/table_function.hpp index 748233b48..eea0000df 100644 --- a/src/duckdb/src/include/duckdb/function/table_function.hpp +++ b/src/duckdb/src/include/duckdb/function/table_function.hpp @@ -26,7 +26,6 @@ namespace duckdb { enum class TablePartitionInfo : uint8_t; struct PartitionStatistics; struct TableFunctionFileBindInput; -struct TableFunctionFileBindInfo; struct TableFunctionFileInitInput; //! Controls how a table function manages parallelism. @@ -133,10 +132,6 @@ struct TableFunctionBindInput { optional_ptr> input_plan; //! (Optional) Set when a multi-file scan binds this function to read one of its files optional_ptr multi_file_input; - //! (Optional) Set when binding a single file of a multi-file scan. The bind can describe the file it binds in more - //! detail than its names and types - e.g. attach the field ids of its columns, or the key-value metadata of the - //! file - which the multi-file reader then uses to read the file - optional_ptr file_info; }; struct TableFunctionInitInput { @@ -269,6 +264,17 @@ struct GetPartitionStatsInput { optional_ptr bind_data; }; +struct TableFunctionGetBindInfoInput { +public: + TableFunctionGetBindInfoInput(ClientContext &context, optional_ptr bind_data_p) + : context(context), bind_data(bind_data_p) { + } + +public: + ClientContext &context; + optional_ptr bind_data; +}; + struct TableFunctionGetMetricsInput { public: TableFunctionGetMetricsInput(ClientContext &context, optional_ptr bind_data_p, @@ -291,11 +297,19 @@ enum class ScanType : uint8_t { TABLE, PARQUET, EXTERNAL }; struct BindInfo { public: explicit BindInfo(ScanType type_p) : type(type_p) {}; - explicit BindInfo(TableCatalogEntry &table) : type(ScanType::TABLE), table(&table) {}; unordered_map options; ScanType type; - optional_ptr table; + + //! An identifier attached to a result column of the bound call, or to a field nested inside one (addressed as in + //! MultiFileColumnDefinition::ResolveChildPath) - e.g. a field id, by which a multi-file function wrapping the + //! function maps the columns of its files onto one another + struct ColumnIdentifier { + idx_t column_index; + vector child_path; + Value identifier; + }; + vector column_identifiers; void InsertOption(const string &name, Value value) { // NOLINT: work-around bug in clang-tidy if (options.find(name) != options.end()) { @@ -352,7 +366,9 @@ typedef OperatorFinalizeResultType (*table_in_out_function_final_t)(ExecutionCon typedef OperatorPartitionData (*table_function_get_partition_data_t)(ClientContext &context, TableFunctionGetPartitionInput &input); -typedef BindInfo (*table_function_get_bind_info_t)(const optional_ptr bind_data); +typedef BindInfo (*table_function_get_bind_info_t)(TableFunctionGetBindInfoInput &input); +//! The table a bound call scans, if it scans one - see LogicalGet::GetTable +typedef optional_ptr (*table_function_get_table_entry_t)(optional_ptr bind_data); typedef unique_ptr (*table_function_get_multi_file_reader_t)(const BoundTableFunction &); @@ -502,6 +518,8 @@ class BaseTableFunction { table_function_get_partition_data_t get_partition_data; //! (Optional) returns extra bind info table_function_get_bind_info_t get_bind_info; + //! (Optional) the table a bound call scans, e.g. for a scan of a catalog table + table_function_get_table_entry_t get_table_entry; //! (Optional) pushes down projection expressions like len() in "SELECT len(str)" or //! casts like "col as UINTEGER" in "SELECT col::UINTEGER" to scanner. //! Returns true if pushdown was successful diff --git a/src/duckdb/src/include/duckdb/main/capi_v2/extension_api_v2.hpp b/src/duckdb/src/include/duckdb/main/capi_v2/extension_api_v2.hpp index 24c2bac25..ad9d1ead1 100644 --- a/src/duckdb/src/include/duckdb/main/capi_v2/extension_api_v2.hpp +++ b/src/duckdb/src/include/duckdb/main/capi_v2/extension_api_v2.hpp @@ -1366,12 +1366,6 @@ typedef struct { (duckdb_v2_multi_file_function_handle function, duckdb_v2_str *reader_type, duckdb_v2_error_info_handle *err); DUCKDB_V2_ERROR(*duckdb_v2_multi_file_function_set_single_file_function) (duckdb_v2_multi_file_function_handle function, duckdb_v2_str *name, duckdb_v2_error_info_handle *err); - DUCKDB_V2_ERROR(*duckdb_v2_table_function_bind_add_file_metadata) - (duckdb_v2_table_function_bind_info_handle info, duckdb_v2_str *key, duckdb_v2_value_handle value, - duckdb_v2_error_info_handle *err); - DUCKDB_V2_ERROR(*duckdb_v2_table_function_bind_set_result_column_identifier) - (duckdb_v2_table_function_bind_info_handle info, idx_t column_index, const idx_t *child_path, - idx_t child_path_length, duckdb_v2_value_handle identifier, duckdb_v2_error_info_handle *err); DUCKDB_V2_ERROR(*duckdb_v2_table_function_claim_batch_get_bind_data) (duckdb_v2_table_function_claim_batch_info_handle info, void **data, duckdb_v2_error_info_handle *err); DUCKDB_V2_ERROR(*duckdb_v2_table_function_claim_batch_get_global_state) @@ -1385,6 +1379,19 @@ typedef struct { DUCKDB_V2_ERROR(*duckdb_v2_table_function_set_claim_batch_callback) (duckdb_v2_table_function_handle function, duckdb_v2_table_function_claim_batch_callback_fn callback, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_get_bind_data) + (duckdb_v2_table_function_get_bind_info_info_handle info, void **data, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_get_user_data) + (duckdb_v2_table_function_get_bind_info_info_handle info, void **data, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_set_column_identifier) + (duckdb_v2_table_function_get_bind_info_info_handle info, idx_t column_index, const idx_t *child_path, + idx_t child_path_length, duckdb_v2_value_handle identifier, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_set_option) + (duckdb_v2_table_function_get_bind_info_info_handle info, duckdb_v2_str *key, duckdb_v2_value_handle value, + duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_set_get_bind_info_callback) + (duckdb_v2_table_function_handle function, duckdb_v2_table_function_get_bind_info_callback_fn callback, + duckdb_v2_error_info_handle *err); } duckdb_ext_api_v2; //===--------------------------------------------------------------------===// @@ -2001,9 +2008,6 @@ inline duckdb_ext_api_v2 CreateAPIv2(void) { result.duckdb_v2_multi_file_function_set_reader_type = duckdb_v2_multi_file_function_set_reader_type; result.duckdb_v2_multi_file_function_set_single_file_function = duckdb_v2_multi_file_function_set_single_file_function; - result.duckdb_v2_table_function_bind_add_file_metadata = duckdb_v2_table_function_bind_add_file_metadata; - result.duckdb_v2_table_function_bind_set_result_column_identifier = - duckdb_v2_table_function_bind_set_result_column_identifier; result.duckdb_v2_table_function_claim_batch_get_bind_data = duckdb_v2_table_function_claim_batch_get_bind_data; result.duckdb_v2_table_function_claim_batch_get_global_state = duckdb_v2_table_function_claim_batch_get_global_state; @@ -2011,6 +2015,12 @@ inline duckdb_ext_api_v2 CreateAPIv2(void) { result.duckdb_v2_table_function_claim_batch_get_user_data = duckdb_v2_table_function_claim_batch_get_user_data; result.duckdb_v2_table_function_claim_batch_set_claimed = duckdb_v2_table_function_claim_batch_set_claimed; result.duckdb_v2_table_function_set_claim_batch_callback = duckdb_v2_table_function_set_claim_batch_callback; + result.duckdb_v2_table_function_get_bind_info_get_bind_data = duckdb_v2_table_function_get_bind_info_get_bind_data; + result.duckdb_v2_table_function_get_bind_info_get_user_data = duckdb_v2_table_function_get_bind_info_get_user_data; + result.duckdb_v2_table_function_get_bind_info_set_column_identifier = + duckdb_v2_table_function_get_bind_info_set_column_identifier; + result.duckdb_v2_table_function_get_bind_info_set_option = duckdb_v2_table_function_get_bind_info_set_option; + result.duckdb_v2_table_function_set_get_bind_info_callback = duckdb_v2_table_function_set_get_bind_info_callback; return result; } diff --git a/src/duckdb/src/include/duckdb/main/client_context.hpp b/src/duckdb/src/include/duckdb/main/client_context.hpp index 655062f52..9a086dfa2 100644 --- a/src/duckdb/src/include/duckdb/main/client_context.hpp +++ b/src/duckdb/src/include/duckdb/main/client_context.hpp @@ -44,6 +44,7 @@ class LogicalOperator; class PreparedStatement; class PreparedStatementData; class StatementIterator; +class StatementPreprocessor; class Relation; class BufferedFileWriter; class QueryProfiler; @@ -233,9 +234,11 @@ class ClientContext : public enable_shared_from_this { //! Preprocess a peel of parse-facing statements into engine-facing ones (PRAGMA reparse, //! MULTI_STATEMENT unpack, transaction wrapping), replacing `buffer` in place. Acquires the //! context lock internally when `lock` is null (callers that do not already hold it, e.g. the - //! shell). Drives StatementIterator's preprocessing. + //! shell). Drives StatementIterator's preprocessing. Pass a `preprocessor` to carry state, such as + //! an explicit BEGIN, across multiple calls for the same query. DUCKDB_API void PreprocessStatements(vector> &buffer, - optional_ptr lock = nullptr); + optional_ptr lock = nullptr, + optional_ptr preprocessor = nullptr); //! Extract the logical plan of a query DUCKDB_API unique_ptr ExtractPlan(const string &query); diff --git a/src/duckdb/src/include/duckdb/main/settings.hpp b/src/duckdb/src/include/duckdb/main/settings.hpp index c0be6c03a..a2817c0ad 100644 --- a/src/duckdb/src/include/duckdb/main/settings.hpp +++ b/src/duckdb/src/include/duckdb/main/settings.hpp @@ -1499,7 +1499,7 @@ struct ExternalFileCacheLocalMaxBlockSizeSetting { static constexpr bool IsDebug = false; static constexpr bool IsDeprecated = false; static constexpr const char *DefaultValue = "16384"; - static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_DEFAULT; + static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_ONLY; static constexpr idx_t SettingIndex = NEXT_SETTING_INDEX(); static void OnSet(SettingCallbackInfo &info, Value &input); }; @@ -1514,7 +1514,7 @@ struct ExternalFileCacheRemoteMaxBlockSizeSetting { static constexpr bool IsDebug = false; static constexpr bool IsDeprecated = false; static constexpr const char *DefaultValue = "2097152"; - static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_DEFAULT; + static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_ONLY; static constexpr idx_t SettingIndex = NEXT_SETTING_INDEX(); static void OnSet(SettingCallbackInfo &info, Value &input); }; @@ -1528,7 +1528,23 @@ struct ExternalFileCacheRemoteMinBlockSizeSetting { static constexpr bool IsDebug = false; static constexpr bool IsDeprecated = false; static constexpr const char *DefaultValue = "4096"; - static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_DEFAULT; + static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_ONLY; + static constexpr idx_t SettingIndex = NEXT_SETTING_INDEX(); + static void OnSet(SettingCallbackInfo &info, Value &input); +}; + +struct ExternalFileCacheRequestSizingSetting { + using RETURN_TYPE = ExternalFileCacheRequestSizing; + static constexpr const char *Name = "external_file_cache_request_sizing"; + static constexpr const char *Description = + "How reads of cached files are sized: AUTO lets each reader choose, GRID reads the aligned blocks of the cache " + "block size around each read, EXACT reads exactly the requested bytes. Reads of files that are not cached are " + "always exact."; + static constexpr const char *InputType = "VARCHAR"; + static constexpr bool IsDebug = false; + static constexpr bool IsDeprecated = false; + static constexpr const char *DefaultValue = "AUTO"; + static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_ONLY; static constexpr idx_t SettingIndex = NEXT_SETTING_INDEX(); static void OnSet(SettingCallbackInfo &info, Value &input); }; diff --git a/src/duckdb/src/include/duckdb/main/statement_iterator.hpp b/src/duckdb/src/include/duckdb/main/statement_iterator.hpp index 9e587a873..7111b76ce 100644 --- a/src/duckdb/src/include/duckdb/main/statement_iterator.hpp +++ b/src/duckdb/src/include/duckdb/main/statement_iterator.hpp @@ -18,6 +18,7 @@ namespace duckdb { class ClientContext; class ClientContextLock; class SQLStatement; +class StatementPreprocessor; //! Iterator over the engine-facing statements of a query. //! @@ -83,6 +84,9 @@ class StatementIterator { //! one-at-a-time across GetStatement calls before pulling + preprocessing the next peel. vector> buffer; idx_t buffer_cursor = 0; + //! Shared across all peels, so a peel is preprocessed knowing about an explicit BEGIN in an earlier + //! peel, even when the caller hasn't executed that BEGIN yet. + unique_ptr preprocessor; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/optimizer/type_pushdown.hpp b/src/duckdb/src/include/duckdb/optimizer/type_pushdown.hpp index 44a21605f..3d4d5eb07 100644 --- a/src/duckdb/src/include/duckdb/optimizer/type_pushdown.hpp +++ b/src/duckdb/src/include/duckdb/optimizer/type_pushdown.hpp @@ -120,9 +120,8 @@ unique_ptr PushdownOptimize(ClientContext &context, unique_ptr< if (expr == nullptr) { // Conflict for column continue; } - if (analysis.get.GetColumnIds()[column_index].IsVirtualColumn()) { - continue; - } + // Resolve() never yields a virtual column as a candidate + D_ASSERT(!analysis.get.GetColumnIds()[column_index].IsVirtualColumn()); TableFunctionProjectionExpressionInput input {analysis.get, *expr, column_index}; if (analysis.get.function.projection_expression_pushdown(context, input)) { analysis.get.has_pushed_projection = true; diff --git a/src/duckdb/src/include/duckdb/planner/binder.hpp b/src/duckdb/src/include/duckdb/planner/binder.hpp index 0ae9fddf0..4ff8a5153 100644 --- a/src/duckdb/src/include/duckdb/planner/binder.hpp +++ b/src/duckdb/src/include/duckdb/planner/binder.hpp @@ -610,7 +610,8 @@ class Binder : public enable_shared_from_this { BoundStatement BindCopyTo(CopyStatement &stmt, const CopyFunction &function, CopyToType copy_to_type); BoundStatement BindCopyFrom(CopyStatement &stmt, const CopyFunction &function); - void BindCopyOptions(CopyInfo &info); + void BindCopyFormat(CopyInfo &info); + void BindCopyOptions(CopyInfo &info, const CopyFunction &function); identifier_map_t GetFullCopyOptionsList(const CopyFunction &function, CopyOptionMode mode); void PrepareModifiers(OrderBinder &order_binder, QueryNode &statement, BoundQueryNode &result); diff --git a/src/duckdb/src/include/duckdb/planner/expression_binder/table_function_binder.hpp b/src/duckdb/src/include/duckdb/planner/expression_binder/table_function_binder.hpp index a86c2ed45..5fce9b6ee 100644 --- a/src/duckdb/src/include/duckdb/planner/expression_binder/table_function_binder.hpp +++ b/src/duckdb/src/include/duckdb/planner/expression_binder/table_function_binder.hpp @@ -15,8 +15,11 @@ namespace duckdb { //! The table function binder can bind standard table function parameters (i.e., non-table-in-out functions) class TableFunctionBinder : public ExpressionBinder { public: - TableFunctionBinder(Binder &binder, ClientContext &context, string table_function_name = string(), - string clause = "Table function"); + enum class IdentifierConversionPolicy : uint8_t { FOLLOW_SETTING, ALLOW }; + + TableFunctionBinder( + Binder &binder, ClientContext &context, string table_function_name = string(), string clause = "Table function", + IdentifierConversionPolicy identifier_conversion_policy = IdentifierConversionPolicy::FOLLOW_SETTING); public: void DisableSQLValueFunctions() { @@ -36,6 +39,7 @@ class TableFunctionBinder : public ExpressionBinder { private: string table_function_name; string clause; + IdentifierConversionPolicy identifier_conversion_policy; //! Whether sql_value_functions (GetSQLValueFunctionName) are considered when binding column refs bool accept_sql_value_functions = true; }; diff --git a/src/duckdb/src/include/duckdb/planner/logical_plan_sql_exporter.hpp b/src/duckdb/src/include/duckdb/planner/logical_plan_sql_exporter.hpp index 8e42fa673..742efa4ef 100644 --- a/src/duckdb/src/include/duckdb/planner/logical_plan_sql_exporter.hpp +++ b/src/duckdb/src/include/duckdb/planner/logical_plan_sql_exporter.hpp @@ -33,6 +33,7 @@ struct LogicalPlanSQLExportField { struct LogicalPlanSQLExportRelation { unique_ptr query; + //! A zero-column relation has a SQL placeholder column that is absent from fields. vector fields; }; diff --git a/src/duckdb/src/include/duckdb/planner/statement_preprocessor.hpp b/src/duckdb/src/include/duckdb/planner/statement_preprocessor.hpp index be8492f99..6ae253b03 100644 --- a/src/duckdb/src/include/duckdb/planner/statement_preprocessor.hpp +++ b/src/duckdb/src/include/duckdb/planner/statement_preprocessor.hpp @@ -30,6 +30,9 @@ class StatementPreprocessor { private: ClientContext &context; + //! Transaction state implied by explicit transaction statements seen so far. Kept across Preprocess calls, + //! because statements may all be preprocessed before any of them is executed. + CurrentTransactionState chained_transaction_state = NOT_IN_ACTIVE_TRANSACTION; private: //! Handles a pragma statement, determines whether the statement needs reparsing, if it does, it returns the diff --git a/src/duckdb/src/include/duckdb/storage/block_manager.hpp b/src/duckdb/src/include/duckdb/storage/block_manager.hpp index b36ebf1aa..14e17686a 100644 --- a/src/duckdb/src/include/duckdb/storage/block_manager.hpp +++ b/src/duckdb/src/include/duckdb/storage/block_manager.hpp @@ -117,6 +117,11 @@ class BlockManager { void UnregisterPersistentBlock(BlockHandle &block); //! UnregisterBlock, only accepts non-temporary block ids virtual void UnregisterBlock(block_id_t id); + //! Unregister the block id if no live handle is registered for it. Used when destroying a + //! handle: a newer handle may have been registered for the same block id in the meantime + //! (metadata blocks are re-registered in place) and must not be unregistered by a stale handle. + //! Returns true if the block id is no longer registered. + virtual bool UnregisterExpiredBlock(block_id_t id); //! Returns a reference to the metadata manager of this block manager. MetadataManager &GetMetadataManager(); diff --git a/src/duckdb/src/include/duckdb/storage/buffer/block_handle.hpp b/src/duckdb/src/include/duckdb/storage/buffer/block_handle.hpp index 54b5eba86..8941a0fac 100644 --- a/src/duckdb/src/include/duckdb/storage/buffer/block_handle.hpp +++ b/src/duckdb/src/include/duckdb/storage/buffer/block_handle.hpp @@ -152,6 +152,13 @@ class BlockMemory : public enable_shared_from_this { bool MustWriteToTemporaryFile() const { return destroy_buffer_upon == DestroyBufferUpon::BLOCK; } + //! Returns true, if the buffer can be loaded again after it was destroyed, i.e., it is backed + //! by its disk block or kept alive in a temporary file. False for buffers that are destroyed + //! upon eviction/unpin - including replaced persistent handles whose disk block may be + //! rewritten in place and must not be read through them again. + bool CanReload() const { + return destroy_buffer_upon == DestroyBufferUpon::BLOCK; + } //! Returns the memory usage. idx_t GetMemoryUsage() const { return memory_usage; diff --git a/src/duckdb/src/include/duckdb/storage/caching_mode.hpp b/src/duckdb/src/include/duckdb/storage/caching_mode.hpp index 7a53a18f4..3f9992511 100644 --- a/src/duckdb/src/include/duckdb/storage/caching_mode.hpp +++ b/src/duckdb/src/include/duckdb/storage/caching_mode.hpp @@ -28,4 +28,14 @@ enum class RequestSizing : uint8_t { BY_READER = 1, }; +//! How reads of cached files are sized, overriding the RequestSizing of their readers. +enum class ExternalFileCacheRequestSizing : uint8_t { + // Each reader chooses through its RequestSizing. + AUTO = 0, + // Every read covers the aligned blocks of the cache block size around it. + GRID = 1, + // Every read covers exactly the bytes it requests. + EXACT = 2, +}; + } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/storage/data_table.hpp b/src/duckdb/src/include/duckdb/storage/data_table.hpp index a118d9a4e..bc61266d5 100644 --- a/src/duckdb/src/include/duckdb/storage/data_table.hpp +++ b/src/duckdb/src/include/duckdb/storage/data_table.hpp @@ -251,10 +251,6 @@ class DataTable : public enable_shared_from_this { //! Scans the next chunk for the CREATE INDEX operator bool CreateIndexScan(TableScanState &state, DataChunk &result); - //! Returns true, if the index name is unique (i.e., no PK, UNIQUE, FK constraint has the same name) - //! FIXME: This is only necessary until we treat all indexes as catalog entries, allowing to alter constraints - bool IndexNameIsUnique(const string &name); - //! Initialize constraint verification state unique_ptr InitializeConstraintState(TableCatalogEntry &table, const vector> &bound_constraints); diff --git a/src/duckdb/src/include/duckdb/storage/single_file_block_manager.hpp b/src/duckdb/src/include/duckdb/storage/single_file_block_manager.hpp index ddb31fdf7..950d09c04 100644 --- a/src/duckdb/src/include/duckdb/storage/single_file_block_manager.hpp +++ b/src/duckdb/src/include/duckdb/storage/single_file_block_manager.hpp @@ -100,6 +100,8 @@ class SingleFileBlockManager : public BlockManager { void IncreaseBlockReferenceCount(block_id_t block_id) override; //! UnregisterBlock, only accepts non-temporary block ids void UnregisterBlock(block_id_t id) override; + //! Unregister the block id if no live handle is registered for it + bool UnregisterExpiredBlock(block_id_t id) override; //! Return the meta block id idx_t GetMetaBlock() override; //! Read the content of the block from disk @@ -193,6 +195,10 @@ class SingleFileBlockManager : public BlockManager { block_id_t GetFreeBlockIdInternal(FreeBlockType type); //! Adds a free block to the free_list, returns true if it was added to the regular free_list bool AddFreeBlock(unique_lock &lock, block_id_t block_id); + //! Move the block from free_blocks_in_use to the free list, if present + void ReleaseFreeBlockInUse(unique_lock &lock, block_id_t id); + //! Returns true, if the block is in the free list. Takes the lock (verification only). + bool BlockIsFreeListed(block_id_t block_id); private: AttachedDatabase &db; diff --git a/src/duckdb/src/include/duckdb/storage/table/scan_state.hpp b/src/duckdb/src/include/duckdb/storage/table/scan_state.hpp index 3347dc8d3..c60eabeb8 100644 --- a/src/duckdb/src/include/duckdb/storage/table/scan_state.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/scan_state.hpp @@ -148,6 +148,8 @@ struct ColumnScanState { void NextInternal(idx_t count); //! Returns the current row position in the segment idx_t GetPositionInSegment() const; + //! Releases the segment scan states (and the blocks they pin), including those of the child states + void ReleasePins(); }; enum class FetchType { @@ -324,6 +326,8 @@ class CollectionScanState { void InitializeColumnScans(); //! Processes the vector prepared by PrepareScanIO void ProcessPreparedScan(DuckTransaction &transaction, DataChunk &result); + //! Releases the row group and the blocks the column scans pin, the next assignment initializes them again + void ReleasePins(); private: //! Registers the remaining assignment's scan I/O, returning the async tasks that execute it @@ -395,6 +399,8 @@ class TableScanState { void InitializeColumnScans(); //! Rows scanned from persistent and transaction-local storage idx_t RowsScanned() const; + //! Releases what the scans of both storages pin, for a state that is kept around between assignments + void ReleasePins(); private: //! The column identifiers of the scan diff --git a/src/duckdb/src/include/duckdb/storage/table/table_index_list.hpp b/src/duckdb/src/include/duckdb/storage/table/table_index_list.hpp index 0174b0e5d..32509ec7a 100644 --- a/src/duckdb/src/include/duckdb/storage/table/table_index_list.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/table_index_list.hpp @@ -69,10 +69,6 @@ class TableIndexList { optional_idx active_checkpoint = optional_idx()); //! Removes an index entry from the list of index entries and release any storage the index owns. void RemoveIndex(idx_t index_oid); - //! Returns true, if the index name does not exist. - bool NameIsUnique(const string &name) const; - //! Returns true if an index with the given name exists. - bool Contains(const Identifier &name) const; //! Returns shared ownership of the stable logical index entry matching the name. shared_ptr FindEntry(const Identifier &name) const; //! Returns the entry matching the index OID. diff --git a/src/duckdb/src/include/duckdb_extension_v2.h b/src/duckdb/src/include/duckdb_extension_v2.h index 397cba13a..a50252398 100644 --- a/src/duckdb/src/include/duckdb_extension_v2.h +++ b/src/duckdb/src/include/duckdb_extension_v2.h @@ -1432,12 +1432,6 @@ typedef struct { (duckdb_v2_multi_file_function_handle function, duckdb_v2_str *reader_type, duckdb_v2_error_info_handle *err); DUCKDB_V2_ERROR(*duckdb_v2_multi_file_function_set_single_file_function) (duckdb_v2_multi_file_function_handle function, duckdb_v2_str *name, duckdb_v2_error_info_handle *err); - DUCKDB_V2_ERROR(*duckdb_v2_table_function_bind_add_file_metadata) - (duckdb_v2_table_function_bind_info_handle info, duckdb_v2_str *key, duckdb_v2_value_handle value, - duckdb_v2_error_info_handle *err); - DUCKDB_V2_ERROR(*duckdb_v2_table_function_bind_set_result_column_identifier) - (duckdb_v2_table_function_bind_info_handle info, idx_t column_index, const idx_t *child_path, - idx_t child_path_length, duckdb_v2_value_handle identifier, duckdb_v2_error_info_handle *err); DUCKDB_V2_ERROR(*duckdb_v2_table_function_claim_batch_get_bind_data) (duckdb_v2_table_function_claim_batch_info_handle info, void **data, duckdb_v2_error_info_handle *err); DUCKDB_V2_ERROR(*duckdb_v2_table_function_claim_batch_get_global_state) @@ -1451,6 +1445,19 @@ typedef struct { DUCKDB_V2_ERROR(*duckdb_v2_table_function_set_claim_batch_callback) (duckdb_v2_table_function_handle function, duckdb_v2_table_function_claim_batch_callback_fn callback, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_get_bind_data) + (duckdb_v2_table_function_get_bind_info_info_handle info, void **data, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_get_user_data) + (duckdb_v2_table_function_get_bind_info_info_handle info, void **data, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_set_column_identifier) + (duckdb_v2_table_function_get_bind_info_info_handle info, idx_t column_index, const idx_t *child_path, + idx_t child_path_length, duckdb_v2_value_handle identifier, duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_get_bind_info_set_option) + (duckdb_v2_table_function_get_bind_info_info_handle info, duckdb_v2_str *key, duckdb_v2_value_handle value, + duckdb_v2_error_info_handle *err); + DUCKDB_V2_ERROR(*duckdb_v2_table_function_set_get_bind_info_callback) + (duckdb_v2_table_function_handle function, duckdb_v2_table_function_get_bind_info_callback_fn callback, + duckdb_v2_error_info_handle *err); #endif // capigen:end appended } duckdb_ext_api_v2; @@ -2150,13 +2157,6 @@ typedef struct { duckdb_ext_api.duckdb_v2_multi_file_function_set_single_file_function #endif #if DUCKDB_V2_API_ALLOW_UNSTABLE -#define duckdb_v2_table_function_bind_add_file_metadata duckdb_ext_api.duckdb_v2_table_function_bind_add_file_metadata -#endif -#if DUCKDB_V2_API_ALLOW_UNSTABLE -#define duckdb_v2_table_function_bind_set_result_column_identifier \ - duckdb_ext_api.duckdb_v2_table_function_bind_set_result_column_identifier -#endif -#if DUCKDB_V2_API_ALLOW_UNSTABLE #define duckdb_v2_table_function_claim_batch_get_bind_data \ duckdb_ext_api.duckdb_v2_table_function_claim_batch_get_bind_data #endif @@ -2179,6 +2179,26 @@ typedef struct { #define duckdb_v2_table_function_set_claim_batch_callback \ duckdb_ext_api.duckdb_v2_table_function_set_claim_batch_callback #endif +#if DUCKDB_V2_API_ALLOW_UNSTABLE +#define duckdb_v2_table_function_get_bind_info_get_bind_data \ + duckdb_ext_api.duckdb_v2_table_function_get_bind_info_get_bind_data +#endif +#if DUCKDB_V2_API_ALLOW_UNSTABLE +#define duckdb_v2_table_function_get_bind_info_get_user_data \ + duckdb_ext_api.duckdb_v2_table_function_get_bind_info_get_user_data +#endif +#if DUCKDB_V2_API_ALLOW_UNSTABLE +#define duckdb_v2_table_function_get_bind_info_set_column_identifier \ + duckdb_ext_api.duckdb_v2_table_function_get_bind_info_set_column_identifier +#endif +#if DUCKDB_V2_API_ALLOW_UNSTABLE +#define duckdb_v2_table_function_get_bind_info_set_option \ + duckdb_ext_api.duckdb_v2_table_function_get_bind_info_set_option +#endif +#if DUCKDB_V2_API_ALLOW_UNSTABLE +#define duckdb_v2_table_function_set_get_bind_info_callback \ + duckdb_ext_api.duckdb_v2_table_function_set_get_bind_info_callback +#endif // capigen:end appended #endif // DUCKDB_BUILD_STATIC_EXTENSION diff --git a/src/duckdb/src/include/duckdb_v2.h b/src/duckdb/src/include/duckdb_v2.h index 834e5aded..b18ef619c 100644 --- a/src/duckdb/src/include/duckdb_v2.h +++ b/src/duckdb/src/include/duckdb_v2.h @@ -12175,6 +12175,16 @@ typedef struct _duckdb_v2_table_function_claim_batch_info { void *internal_ptr; } * duckdb_v2_table_function_claim_batch_info_handle; +/*! + * A borrowed opaque handle to the arguments supplied to a table function when the engine asks it to describe a bound + * call. The "get bind info" callback receives this handle, and can use it to describe the call in more detail than its + * columns: with identifiers of its columns (e.g. field ids), and with options - key-value facts about what the call + * reads. + */ +typedef struct _duckdb_v2_table_function_get_bind_info_info { + void *internal_ptr; +} * duckdb_v2_table_function_get_bind_info_info_handle; + /*! * An owned opaque handle to a multi-file table function being built. A multi-file function wraps an already registered * table function that reads a single file, and adds everything that is needed to read many files at once on top of it: @@ -12229,6 +12239,10 @@ typedef void (*duckdb_v2_table_function_claim_batch_callback_fn)(duckdb_v2_table duckdb_v2_context_handle context, duckdb_v2_error_info_handle *err); +typedef void (*duckdb_v2_table_function_get_bind_info_callback_fn)( + duckdb_v2_table_function_get_bind_info_info_handle info, duckdb_v2_context_handle context, + duckdb_v2_error_info_handle *err); + /* --- Functions for table --- */ /*! @@ -13569,13 +13583,69 @@ DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_claim_batch_set_claimed( #if DUCKDB_V2_API_ALLOW_UNSTABLE /*! - * Attaches an identifier to a result column, or to a field nested inside one. + * Sets the optional "get bind info" callback of the table function. + * + * The engine invokes the callback with the bind data of a bound call when it needs to know more about the call than its + * columns. E.g. a multi-file function registered with `duckdb_v2_multi_file_function_register()` asks the function it + * wraps for every file it binds: it maps the columns of the files onto the columns of its scan by the identifiers the + * callback reports, and exposes the options it reports as the metadata of the reader of the file. The callback may be + * invoked more than once for the same bind data, so it should only report what the bind already determined. * - * Identifiers are only consulted when the function reads a file as part of a multi-file function registered with - * `duckdb_v2_multi_file_function_register()`, and are ignored otherwise. A multi-file reader can then map the columns - * of every file onto the columns of the scan by identifier rather than by name - e.g. by the field ids of a file format - * that carries them, so that renamed columns and fields are still found. An identifier is an INTEGER field id or a - * VARCHAR name. + * history: + * - unstable: v2.0.0 + * + * @param function The function to set the callback of. + * @param callback The "get bind info" callback to set. + * @param err Optional. On failure, receives an opaque info handle the caller must destroy via + * `duckdb_v2_error_info_destroy()`. + * @return DUCKDB_V2_ERROR + */ +DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_set_get_bind_info_callback( + duckdb_v2_table_function_handle function, duckdb_v2_table_function_get_bind_info_callback_fn callback, + duckdb_v2_error_info_handle *err); +#endif + +#if DUCKDB_V2_API_ALLOW_UNSTABLE +/*! + * Retrieves the user data set via `duckdb_v2_table_function_set_user_data()`. + * + * history: + * - unstable: v2.0.0 + * + * @param info The get bind info handle. + * @param data Receives the user data pointer, or null if none was set. + * @param err Optional. On failure, receives an opaque info handle the caller must destroy via + * `duckdb_v2_error_info_destroy()`. + * @return DUCKDB_V2_ERROR + */ +DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_get_bind_info_get_user_data( + duckdb_v2_table_function_get_bind_info_info_handle info, void **data, duckdb_v2_error_info_handle *err); +#endif + +#if DUCKDB_V2_API_ALLOW_UNSTABLE +/*! + * Retrieves the bind data the function's bind callback set for the bound call being described. + * + * history: + * - unstable: v2.0.0 + * + * @param info The get bind info handle. + * @param data Receives the bind data pointer, or null if none was set. + * @param err Optional. On failure, receives an opaque info handle the caller must destroy via + * `duckdb_v2_error_info_destroy()`. + * @return DUCKDB_V2_ERROR + */ +DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_get_bind_info_get_bind_data( + duckdb_v2_table_function_get_bind_info_info_handle info, void **data, duckdb_v2_error_info_handle *err); +#endif + +#if DUCKDB_V2_API_ALLOW_UNSTABLE +/*! + * Attaches an identifier to a result column of the bound call, or to a field nested inside one. + * + * A multi-file reader can then map the columns of every file onto the columns of its scan by identifier rather than by + * name - e.g. by the field ids of a file format that carries them, so that renamed columns and fields are still found. + * An identifier is an INTEGER field id or a VARCHAR name. * * The nested field is addressed by a path of child indexes, starting at the result column at `column_index`: a STRUCT * field by its index, the elements of a LIST or ARRAY by `0`, the keys of a MAP by `0` and its values by `1`, and a @@ -13586,8 +13656,8 @@ DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_claim_batch_set_claimed( * history: * - unstable: v2.0.0 * - * @param info The bind info handle. - * @param column_index The index of the result column, in the order the columns were declared. + * @param info The get bind info handle. + * @param column_index The index of the result column, in the order the bind callback declared the columns. * @param child_path The path of child indexes addressing the nested field, or null for the column itself. * @param child_path_length The number of entries in the path, 0 for the column itself. * @param identifier The identifier, an INTEGER or VARCHAR value. Borrowed and copied. @@ -13595,33 +13665,32 @@ DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_claim_batch_set_claimed( * `duckdb_v2_error_info_destroy()`. * @return DUCKDB_V2_ERROR */ -DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_bind_set_result_column_identifier( - duckdb_v2_table_function_bind_info_handle info, idx_t column_index, const idx_t *child_path, +DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_get_bind_info_set_column_identifier( + duckdb_v2_table_function_get_bind_info_info_handle info, idx_t column_index, const idx_t *child_path, idx_t child_path_length, duckdb_v2_value_handle identifier, duckdb_v2_error_info_handle *err); #endif #if DUCKDB_V2_API_ALLOW_UNSTABLE /*! - * Adds an entry to the metadata of the file the function reads. + * Sets an option describing the bound call: a key-value fact about what the call reads, e.g. a property of the file it + * reads. Setting a key again replaces its value. * - * File metadata is only consulted when the function reads a file as part of a multi-file function registered with - * `duckdb_v2_multi_file_function_register()`, and is ignored otherwise: it is the key-value metadata the reader of the - * file exposes to the multi-file reader, e.g. to a custom multi-file reader of another extension. Entries keep the - * order in which they were added; adding a key again replaces its value. + * A multi-file function registered with `duckdb_v2_multi_file_function_register()` exposes the options of the function + * it wraps as the metadata of the reader of each file, e.g. to a custom multi-file reader of another extension. * * history: * - unstable: v2.0.0 * - * @param info The bind info handle. - * @param key The key of the entry. Borrowed and copied. - * @param value The value of the entry. Borrowed and copied. + * @param info The get bind info handle. + * @param key The key of the option. Borrowed and copied. + * @param value The value of the option. Borrowed and copied. * @param err Optional. On failure, receives an opaque info handle the caller must destroy via * `duckdb_v2_error_info_destroy()`. * @return DUCKDB_V2_ERROR */ -DUCKDB_C_API DUCKDB_V2_ERROR -duckdb_v2_table_function_bind_add_file_metadata(duckdb_v2_table_function_bind_info_handle info, duckdb_v2_str *key, - duckdb_v2_value_handle value, duckdb_v2_error_info_handle *err); +DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_table_function_get_bind_info_set_option( + duckdb_v2_table_function_get_bind_info_info_handle info, duckdb_v2_str *key, duckdb_v2_value_handle value, + duckdb_v2_error_info_handle *err); #endif #if DUCKDB_V2_API_ALLOW_UNSTABLE @@ -13696,12 +13765,19 @@ DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_multi_file_function_set_name(duckdb_v2_mu /*! * Sets the name of the table function that reads a single file, which the multi-file function wraps. * - * The single-file function must be registered before the multi-file function is, and must take the path of the file to - * read as its only positional VARCHAR parameter. It is bound once for every file that is read, and every named - * parameter it declares is also a named parameter of the multi-file function, forwarded to it as given. Everything that - * involves several files - globbing, lists of files, hive partitioning, the `filename` column, `union_by_name` and the - * like - is provided by the multi-file function on top of it. The name is borrowed and copied. A single-file function - * must be set before registration. + * The single-file function must be registered before the multi-file function is, and must take the file to read as its + * only positional parameter, of type ANY. It is bound once for every file that is read, and receives the file the way a + * multi-file function takes one: as the VARCHAR path of the file when there is nothing more to know about it, and + * otherwise as a STRUCT holding the path in its `filename` field and the options to open the file with in its other + * fields - e.g. the size of the file, as a file system reported it while globbing, or the options a caller passed along + * with the file (`read_x([{'filename': 'f.x', 'file_size': 42}])`). Opening the file with those options, set with + * `duckdb_v2_file_open_options_set_value()`, lets the file system skip work it would otherwise repeat, such as asking a + * remote store for the size of the file. A single-file function that takes a VARCHAR parameter instead is still + * accepted, and only receives the path. Every named parameter the single-file function declares is also a named + * parameter of the multi-file function, forwarded to it as given. Everything that involves several files - globbing, + * lists of files, hive partitioning, the `filename` column, `union_by_name` and the like - is provided by the + * multi-file function on top of it. The name is borrowed and copied. A single-file function must be set before + * registration. * * history: * - unstable: v2.0.0 @@ -13765,9 +13841,9 @@ DUCKDB_C_API DUCKDB_V2_ERROR duckdb_v2_multi_file_function_set_file_extension( * can then be called with the path of a single file, a glob pattern, or a list of either, and accepts the options every * multi-file function accepts (e.g. `filename`, `hive_partitioning`, `union_by_name`) next to the named parameters of * the single-file function. Registration requires a name and a single-file function, and fails when no table function - * by that name is registered or it does not take the path of a file as its only positional VARCHAR parameter. The - * caller still owns the handle after registration and must destroy it with `duckdb_v2_multi_file_function_destroy()`, - * which does not affect the registered function. + * by that name is registered or it does not take the file to read as its only positional parameter (see + * `duckdb_v2_multi_file_function_set_single_file_function()`). The caller still owns the handle after registration and + * must destroy it with `duckdb_v2_multi_file_function_destroy()`, which does not affect the registered function. * * history: * - unstable: v2.0.0 diff --git a/src/duckdb/src/main/capi/v1/arrow-c.cpp b/src/duckdb/src/main/capi/v1/arrow-c.cpp index 6de0e78b0..e07b834e4 100644 --- a/src/duckdb/src/main/capi/v1/arrow-c.cpp +++ b/src/duckdb/src/main/capi/v1/arrow-c.cpp @@ -38,22 +38,11 @@ idx_t ChangedRows(QueryResult &result) { if (types.size() != 1 || types[0].id() != duckdb::LogicalTypeId::BIGINT) { return 0; } - // A CHANGED_ROWS result is one BIGINT row, which the Arrow format wrapped in an array with a - // single int64 child auto &arrays = result.Collection(); if (arrays.empty()) { return 0; } - auto &array = arrays.front()->arrow_array; - if (array.length != 1 || array.n_children != 1 || !array.children || !array.children[0]) { - return 0; - } - auto &child = *array.children[0]; - if (child.n_buffers < 2 || !child.buffers || !child.buffers[1]) { - return 0; - } - auto counts = reinterpret_cast(child.buffers[1]); - return duckdb::NumericCast(counts[array.offset + child.offset]); + return ArrowFormat::ChangedRows(arrays.front()->arrow_array); } } // namespace diff --git a/src/duckdb/src/main/capi/v2/capi_v2_func_table.cpp b/src/duckdb/src/main/capi/v2/capi_v2_func_table.cpp index 9c27248b3..fae511ecb 100644 --- a/src/duckdb/src/main/capi/v2/capi_v2_func_table.cpp +++ b/src/duckdb/src/main/capi/v2/capi_v2_func_table.cpp @@ -2,6 +2,7 @@ #include "duckdb/main/capi_v2/capi_v2_function_internal.hpp" #include "duckdb/catalog/catalog.hpp" #include "duckdb/catalog/catalog_entry/table_function_catalog_entry.hpp" +#include "duckdb/common/multi_file/multi_file_data.hpp" #include "duckdb/common/multi_file/table_function_multi_file.hpp" #include "duckdb/execution/partition_info.hpp" #include "duckdb/function/partition_stats.hpp" @@ -28,6 +29,7 @@ class CV2TableFunctionData final : public FunctionData { bool cardinality_set = false; vector column_types; + vector column_names; auto Copy() const -> unique_ptr override { auto copy = make_uniq(); @@ -37,6 +39,7 @@ class CV2TableFunctionData final : public FunctionData { copy->cardinality_is_exact = cardinality_is_exact; copy->cardinality_set = cardinality_set; copy->column_types = column_types; + copy->column_names = column_names; return std::move(copy); } @@ -131,15 +134,6 @@ class CV2TableBindInfo { bool out_cardinality_is_exact = false; bool out_cardinality_set = false; OrderPreservationType out_order_preservation = OrderPreservationType::INSERTION_ORDER; - - //! The identifiers set on the result columns and their nested fields, by column and child path - struct ColumnIdentifier { - idx_t column_index; - vector child_path; - Value identifier; - }; - vector out_column_identifiers; - InsertionOrderPreservingMap out_file_metadata; }; static auto Convert(duckdb_v2_table_function_bind_info_handle info) -> CV2TableBindInfo * { @@ -337,87 +331,56 @@ class CV2TableFunctionInfo : public TableFunctionInfo { duckdb_v2_table_function_partition_data_callback_fn partition_data_cb = nullptr; duckdb_v2_table_function_partitioning_callback_fn partitioning_cb = nullptr; duckdb_v2_table_function_claim_batch_callback_fn claim_batch_cb = nullptr; + duckdb_v2_table_function_get_bind_info_callback_fn get_bind_info_cb = nullptr; shared_ptr user_data = nullptr; bool projection_pushdown = false; Identifier name; }; -//! The column definition of a column, with a child for every nested field an identifier can be attached to -static auto CV2TableCreateColumnDefinition(const Identifier &name, const LogicalType &type) - -> MultiFileColumnDefinition { - MultiFileColumnDefinition result(name, type); - switch (type.id()) { - case LogicalTypeId::STRUCT: - for (auto &child : StructType::GetChildTypes(type)) { - result.children.push_back(CV2TableCreateColumnDefinition(child.first, child.second)); - } - break; - case LogicalTypeId::LIST: - result.children.push_back(CV2TableCreateColumnDefinition(Identifier("list"), ListType::GetChildType(type))); - break; - case LogicalTypeId::ARRAY: - result.children.push_back(CV2TableCreateColumnDefinition(Identifier("list"), ArrayType::GetChildType(type))); - break; - case LogicalTypeId::MAP: { - // the multi-file reader expects the entries of a MAP as a "key_value" STRUCT of the keys and the values - MultiFileColumnDefinition key_value(Identifier("key_value"), ListType::GetChildType(type)); - key_value.children.push_back(CV2TableCreateColumnDefinition(Identifier("key"), MapType::KeyType(type))); - key_value.children.push_back(CV2TableCreateColumnDefinition(Identifier("value"), MapType::ValueType(type))); - result.children.push_back(std::move(key_value)); - break; - } - case LogicalTypeId::UNION: - for (idx_t i = 0; i < UnionType::GetMemberCount(type); i++) { - result.children.push_back( - CV2TableCreateColumnDefinition(UnionType::GetMemberName(type, i), UnionType::GetMemberType(type, i))); - } - break; - default: - break; +//! What the "get bind info" callback describes a bound call with +class CV2TableGetBindInfoInfo { +public: + explicit CV2TableGetBindInfoInfo(const CV2TableFunctionData &bind_data_p) : bind_data(bind_data_p) { } - return result; + + void *in_user_data = nullptr; + void *in_bind_data = nullptr; + //! The bound call - its result columns are the columns identifiers are attached to + const CV2TableFunctionData &bind_data; + + //! What the callback reports + BindInfo out_bind_info {ScanType::EXTERNAL}; +}; + +static auto Convert(duckdb_v2_table_function_get_bind_info_info_handle info) -> CV2TableGetBindInfoInfo * { + return reinterpret_cast(info); +} +static auto Convert(CV2TableGetBindInfoInfo *info) -> duckdb_v2_table_function_get_bind_info_info_handle { + return reinterpret_cast(info); } -//! Resolves a child path - as documented for table_function_bind_set_result_column_identifier - to the column -//! definition of the nested field it addresses -static auto CV2TableResolveChildPath(MultiFileColumnDefinition &column, const vector &child_path) - -> MultiFileColumnDefinition & { - reference current(column); - for (auto child_index : child_path) { - auto &definition = current.get(); - if (definition.type.id() == LogicalTypeId::MAP) { - // the keys and the values are the children of the "key_value" entry - if (child_index > 1) { - throw InvalidInputException("A MAP has two children: its keys (0) and its values (1), not %llu", - child_index); - } - current = definition.children[0].children[child_index]; - continue; - } - if (child_index >= definition.children.size()) { - throw InvalidInputException("Child index %llu is out of range for a field of type %s", child_index, - definition.type.ToString()); - } - current = definition.children[child_index]; +//! Asks the function to describe a bound call: e.g. the identifiers of its columns +static auto CV2TableGetBindInfo(TableFunctionGetBindInfoInput &input) -> BindInfo { + if (!input.bind_data) { + return BindInfo(ScanType::EXTERNAL); } - return current.get(); -} + const auto &bind_data = input.bind_data->Cast(); + const auto &info = *bind_data.info; -//! Describes the file the bind read to the multi-file reader: the identifiers of its columns, and its metadata -static void CV2TableDescribeFile(CV2TableBindInfo &args, TableFunctionFileBindInfo &file_info) { - if (!args.out_column_identifiers.empty()) { - vector columns; - for (idx_t i = 0; i < args.out_column_names.size(); i++) { - columns.push_back(CV2TableCreateColumnDefinition(args.out_column_names[i], args.out_column_types[i])); - } - for (auto &entry : args.out_column_identifiers) { - auto &definition = CV2TableResolveChildPath(columns[entry.column_index], entry.child_path); - definition.identifier = entry.identifier; - } - file_info.columns = std::move(columns); + CV2TableGetBindInfoInfo args(bind_data); + args.in_user_data = info.user_data ? info.user_data->GetData() : nullptr; + args.in_bind_data = bind_data.handle ? bind_data.handle->GetData() : nullptr; + + CV2ErrorInfo err = {}; + auto err_ptr = Convert(&err); + info.get_bind_info_cb(Convert(&args), Convert(&input.context), &err_ptr); + + if (err.HasError()) { + err.ThrowAsException(); } - file_info.metadata = std::move(args.out_file_metadata); + + return std::move(args.out_bind_info); } static auto CV2TableBind(ClientContext &context, TableFunctionBindInput &input, vector &return_types, @@ -452,11 +415,9 @@ static auto CV2TableBind(ClientContext &context, TableFunctionBindInput &input, // The binder hands each call site its own copy of the function, so this does not leak into other calls. input.table_function.order_preservation_type = args.out_order_preservation; - if (input.file_info) { - CV2TableDescribeFile(args, *input.file_info); - } result->column_types = args.out_column_types; + result->column_names = args.out_column_names; return_types = std::move(args.out_column_types); names = std::move(args.out_column_names); return std::move(result); @@ -850,6 +811,9 @@ class CV2TableFunction { function.get_partition_info = CV2TableGetPartitionInfo; } function.projection_pushdown = info.projection_pushdown; + if (info.get_bind_info_cb) { + function.get_bind_info = CV2TableGetBindInfo; + } info.name = name; function.function_info = make_shared_ptr(std::move(info)); @@ -950,14 +914,17 @@ class CV2MultiFileFunction { } for (auto &function : entry->Cast().functions.functions) { auto &signature = function->GetSignature(); - if (signature.GetRequiredParameterCount() == 1 && signature.GetParameter(0).AcceptsPosition() && - signature.GetParameter(0).GetType() == LogicalType::VARCHAR) { + if (signature.GetRequiredParameterCount() != 1 || !signature.GetParameter(0).AcceptsPosition()) { + continue; + } + auto &type = signature.GetParameter(0).GetType(); + if (type == LogicalType::VARCHAR || type.id() == LogicalTypeId::ANY) { return *function; } } throw InvalidInputException( - "Table function \"%s\" cannot read single files: it must take the path of the file to read as its only " - "positional VARCHAR parameter.", + "Table function \"%s\" cannot read single files: it must take the file to read as its only positional " + "parameter, of type ANY (or VARCHAR).", single_file_function); } @@ -1700,60 +1667,83 @@ DUCKDB_V2_ERROR duckdb_v2_table_function_claim_batch_set_claimed(duckdb_v2_table //---------------------------------------------------------------------------------------------------------------------- DUCKDB_V2_ERROR -duckdb_v2_table_function_bind_set_result_column_identifier(duckdb_v2_table_function_bind_info_handle info, - idx_t column_index, const idx_t *child_path, - idx_t child_path_length, duckdb_v2_value_handle identifier, - duckdb_v2_error_info_handle *err) { +duckdb_v2_table_function_set_get_bind_info_callback(duckdb_v2_table_function_handle function, + duckdb_v2_table_function_get_bind_info_callback_fn callback, + duckdb_v2_error_info_handle *err) { + DUCKDB_CHECK_ARG(function); + return WithErrorHandler(err, [&]() { Convert(function)->info.get_bind_info_cb = callback; }); +} + +DUCKDB_V2_ERROR +duckdb_v2_table_function_get_bind_info_get_user_data(duckdb_v2_table_function_get_bind_info_info_handle info, + void **data, duckdb_v2_error_info_handle *err) { + DUCKDB_CHECK_ARG(info); + DUCKDB_CHECK_ARG(data); + return WithErrorHandler(err, [&]() { *data = Convert(info)->in_user_data; }); +} + +DUCKDB_V2_ERROR +duckdb_v2_table_function_get_bind_info_get_bind_data(duckdb_v2_table_function_get_bind_info_info_handle info, + void **data, duckdb_v2_error_info_handle *err) { + DUCKDB_CHECK_ARG(info); + DUCKDB_CHECK_ARG(data); + return WithErrorHandler(err, [&]() { *data = Convert(info)->in_bind_data; }); +} + +DUCKDB_V2_ERROR +duckdb_v2_table_function_get_bind_info_set_column_identifier(duckdb_v2_table_function_get_bind_info_info_handle info, + idx_t column_index, const idx_t *child_path, + idx_t child_path_length, duckdb_v2_value_handle identifier, + duckdb_v2_error_info_handle *err) { DUCKDB_CHECK_ARG(info); DUCKDB_CHECK_ARG(identifier); return WithErrorHandler(err, [&]() { - auto &bind_info = *Convert(info); - if (column_index >= bind_info.out_column_types.size()) { + auto &args = *Convert(info); + auto &bind_data = args.bind_data; + if (column_index >= bind_data.column_types.size()) { throw duckdb::InvalidInputException( - "Index out of bounds in duckdb_v2_table_function_bind_set_result_column_identifier: result column %llu " - "has not been declared", + "Index out of bounds in duckdb_v2_table_function_get_bind_info_set_column_identifier: result column " + "%llu has not been declared", column_index); } if (child_path_length > 0 && !child_path) { - throw duckdb::InvalidInputException( - "duckdb_v2_table_function_bind_set_result_column_identifier: the child path cannot be null unless it " - "is empty"); + throw duckdb::InvalidInputException("duckdb_v2_table_function_get_bind_info_set_column_identifier: the " + "child path cannot be null unless it is empty"); } const auto &value = *Convert(identifier); const auto identifier_type = value.type().id(); if (value.IsNull() || (identifier_type != duckdb::LogicalTypeId::INTEGER && identifier_type != duckdb::LogicalTypeId::VARCHAR)) { - throw duckdb::InvalidInputException( - "duckdb_v2_table_function_bind_set_result_column_identifier: an identifier must be a non-NULL INTEGER " - "or VARCHAR, got %s", - value.ToString()); + throw duckdb::InvalidInputException("duckdb_v2_table_function_get_bind_info_set_column_identifier: an " + "identifier must be a non-NULL INTEGER or VARCHAR, got %s", + value.ToString()); } duckdb::vector path(child_path, child_path + child_path_length); // verify the path addresses a nested field of the column, so a mistake is reported where it is made - auto definition = CV2TableCreateColumnDefinition(bind_info.out_column_names[column_index], - bind_info.out_column_types[column_index]); - CV2TableResolveChildPath(definition, path); + auto definition = duckdb::MultiFileColumnDefinition::CreateNested(bind_data.column_names[column_index], + bind_data.column_types[column_index]); + definition.ResolveChildPath(path); - for (auto &entry : bind_info.out_column_identifiers) { + auto &identifiers = args.out_bind_info.column_identifiers; + for (auto &entry : identifiers) { if (entry.column_index == column_index && entry.child_path == path) { entry.identifier = value; return; } } - bind_info.out_column_identifiers.push_back({column_index, std::move(path), value}); + identifiers.push_back({column_index, std::move(path), value}); }); } -DUCKDB_V2_ERROR duckdb_v2_table_function_bind_add_file_metadata(duckdb_v2_table_function_bind_info_handle info, - duckdb_v2_str *key, duckdb_v2_value_handle value, - duckdb_v2_error_info_handle *err) { +DUCKDB_V2_ERROR +duckdb_v2_table_function_get_bind_info_set_option(duckdb_v2_table_function_get_bind_info_info_handle info, + duckdb_v2_str *key, duckdb_v2_value_handle value, + duckdb_v2_error_info_handle *err) { DUCKDB_CHECK_ARG(info); DUCKDB_CHECK_ARG(key); DUCKDB_CHECK_ARG(value); - return WithErrorHandler(err, [&]() { - auto &metadata = Convert(info)->out_file_metadata; - metadata[duckdb::string(Convert(*key))] = *Convert(value); - }); + return WithErrorHandler( + err, [&]() { Convert(info)->out_bind_info.options[duckdb::string(Convert(*key))] = *Convert(value); }); } //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/duckdb/src/main/capi/v2/capi_v2_result.cpp b/src/duckdb/src/main/capi/v2/capi_v2_result.cpp index d5608369c..932ef6b2d 100644 --- a/src/duckdb/src/main/capi/v2/capi_v2_result.cpp +++ b/src/duckdb/src/main/capi/v2/capi_v2_result.cpp @@ -39,14 +39,9 @@ void ReadChangedRows(const DataChunk &chunk, idx_t &rows_changed) { } void ReadChangedRows(const ArrowArrayWrapper &unit, idx_t &rows_changed) { - auto &array = unit.arrow_array; - if (array.length == 0) { - return; + if (unit.arrow_array.length > 0) { + rows_changed = ArrowFormat::ChangedRows(unit.arrow_array); } - D_ASSERT(array.n_children == 1); - auto &count = *array.children[0]; - auto values = static_cast(count.buffers[1]); - rows_changed = static_cast(values[array.offset + count.offset]); } } // anonymous namespace diff --git a/src/duckdb/src/main/client_context.cpp b/src/duckdb/src/main/client_context.cpp index 75107d2a3..657395edc 100644 --- a/src/duckdb/src/main/client_context.cpp +++ b/src/duckdb/src/main/client_context.cpp @@ -260,10 +260,19 @@ connection_t ClientContext::GetConnectionId() const { return connection_id; } -static bool IsExplainAnalyze(SQLStatement *statement) { +static bool IsExplainAnalyze(ClientContext &context, SQLStatement *statement) { if (!statement) { return false; } + if (statement->type == StatementType::EXECUTE_STATEMENT) { + // the profiler state set up when preparing is gone by the time the statement is executed + auto &prepared_statements = ClientData::Get(context).prepared_statements; + auto entry = prepared_statements.find(statement->Cast().name); + if (entry == prepared_statements.end()) { + return false; + } + return IsExplainAnalyze(context, entry->second->unbound_statement.get()); + } if (statement->type != StatementType::EXPLAIN_STATEMENT) { return false; } @@ -278,7 +287,7 @@ shared_ptr ClientContext::CreatePreparedStatementInternal auto result = make_shared_ptr(statement_type); auto &profiler = QueryProfiler::Get(*this); - profiler.StartQuery(statement->query, IsExplainAnalyze(statement.get())); + profiler.StartQuery(statement->query, IsExplainAnalyze(*this, statement.get())); Planner logical_planner(*this); if (parameters.statement_args) { auto ¶meter_values = *parameters.statement_args; @@ -409,8 +418,8 @@ StatementIterator ClientContext::IterateStatements(const string &query) { return StatementIterator(ParseIterator(*this, query)); } -void ClientContext::PreprocessStatements(vector> &buffer, - optional_ptr lock) { +void ClientContext::PreprocessStatements(vector> &buffer, optional_ptr lock, + optional_ptr preprocessor) { // Acquire our own lock if the caller doesn't hold one (e.g. the shell); own_lock keeps it alive // for the duration of the preprocess pass. unique_ptr own_lock; @@ -418,10 +427,14 @@ void ClientContext::PreprocessStatements(vector> &buffe own_lock = LockContext(); lock = own_lock.get(); } - StatementPreprocessor preprocessor(*this); + unique_ptr own_preprocessor; + if (!preprocessor) { + own_preprocessor = make_uniq(*this); + preprocessor = own_preprocessor.get(); + } const CurrentTransactionState transaction_state = transaction.HasActiveTransaction() ? IN_ACTIVE_TRANSACTION : NOT_IN_ACTIVE_TRANSACTION; - preprocessor.Preprocess(*lock, buffer, transaction_state); + preprocessor->Preprocess(*lock, buffer, transaction_state); } vector> ClientContext::ParseStatementsInternal(ClientContextLock &lock, const string &query) { diff --git a/src/duckdb/src/main/config.cpp b/src/duckdb/src/main/config.cpp index 0697a5455..2d5338f65 100644 --- a/src/duckdb/src/main/config.cpp +++ b/src/duckdb/src/main/config.cpp @@ -180,6 +180,7 @@ static const ConfigurationOption internal_options[] = { DUCKDB_SETTING_CALLBACK(ExternalFileCacheLocalMaxBlockSizeSetting), DUCKDB_SETTING_CALLBACK(ExternalFileCacheRemoteMaxBlockSizeSetting), DUCKDB_SETTING_CALLBACK(ExternalFileCacheRemoteMinBlockSizeSetting), + DUCKDB_SETTING_CALLBACK(ExternalFileCacheRequestSizingSetting), DUCKDB_SETTING(ExternalFileCacheSpillSetting), DUCKDB_SETTING_CALLBACK(ExternalThreadsSetting), DUCKDB_SETTING(FileSearchPathSetting), diff --git a/src/duckdb/src/main/connection.cpp b/src/duckdb/src/main/connection.cpp index f2268ac64..a41a3fd5b 100644 --- a/src/duckdb/src/main/connection.cpp +++ b/src/duckdb/src/main/connection.cpp @@ -172,12 +172,18 @@ vector> Connection::ExtractStatements(const string &que // Eager convenience over the lazy ClientContext::ExtractStatements iterator: drain the // engine-facing statements into a vector. auto &client_context = *context; - auto iterator = client_context.IterateStatements(query); vector> result; - while (iterator.Peek()) { - if (auto statement = iterator.GetStatement()) { - result.push_back(std::move(statement)); + try { + auto iterator = client_context.IterateStatements(query); + while (iterator.Peek()) { + if (auto statement = iterator.GetStatement()) { + result.push_back(std::move(statement)); + } } + } catch (std::exception &ex) { + ErrorData error(ex); + client_context.ProcessError(error, query); + error.Throw(); } return result; } diff --git a/src/duckdb/src/main/query_result.cpp b/src/duckdb/src/main/query_result.cpp index e765ecf70..81b769959 100644 --- a/src/duckdb/src/main/query_result.cpp +++ b/src/duckdb/src/main/query_result.cpp @@ -354,14 +354,26 @@ void QueryResult::CompleteInternal(ClientContextLock &lock) { } D_ASSERT(buffer); buffer->Decide(ResultLifetime::RETAINED); - QueryResultState state; - while (!IsTerminal(state = context->ExecuteTaskInternal(lock, *this))) { - if (state == QueryResultState::BLOCKED || state == QueryResultState::READY) { - context->WaitForTask(lock, *this); + try { + QueryResultState state; + while (!IsTerminal(state = context->ExecuteTaskInternal(lock, *this))) { + if (state == QueryResultState::BLOCKED || state == QueryResultState::READY) { + context->WaitForTask(lock, *this); + } } - } - if (state == QueryResultState::FINISHED) { - EndFinishedInternal(lock); + if (state == QueryResultState::FINISHED) { + EndFinishedInternal(lock); + } + } catch (...) { + // the caller holds the context lock - clean up with it here, as closing this result would lock it again + try { + if (context->IsActiveResult(lock, *this)) { + context->CleanupInternal(lock, this, true); + } + } catch (...) { // NOLINT + } + context.reset(); + throw; } context.reset(); } diff --git a/src/duckdb/src/main/settings/autogenerated_settings.cpp b/src/duckdb/src/main/settings/autogenerated_settings.cpp index 8d10be8f1..e44dca611 100644 --- a/src/duckdb/src/main/settings/autogenerated_settings.cpp +++ b/src/duckdb/src/main/settings/autogenerated_settings.cpp @@ -235,6 +235,16 @@ void ExplainOutputSetting::OnSet(SettingCallbackInfo &info, Value ¶meter) { EnumUtil::FromString(StringValue::Get(parameter)); } +//===----------------------------------------------------------------------===// +// External File Cache Request Sizing +//===----------------------------------------------------------------------===// +void ExternalFileCacheRequestSizingSetting::OnSet(SettingCallbackInfo &info, Value ¶meter) { + if (parameter.IsNull()) { + throw InvalidInputException("external_file_cache_request_sizing setting cannot be NULL"); + } + EnumUtil::FromString(StringValue::Get(parameter)); +} + //===----------------------------------------------------------------------===// // Fsync Mode //===----------------------------------------------------------------------===// diff --git a/src/duckdb/src/main/statement_iterator.cpp b/src/duckdb/src/main/statement_iterator.cpp index 36cf603da..0e20adbdd 100644 --- a/src/duckdb/src/main/statement_iterator.cpp +++ b/src/duckdb/src/main/statement_iterator.cpp @@ -4,6 +4,7 @@ #include "duckdb/main/query_profiler.hpp" #include "duckdb/parser/sql_statement.hpp" #include "duckdb/parser/statement/explain_statement.hpp" +#include "duckdb/planner/statement_preprocessor.hpp" namespace duckdb { @@ -15,7 +16,8 @@ static bool StatementIteratorIsExplainAnalyze(SQLStatement *statement) { } StatementIterator::StatementIterator(ParseIterator &&parse_iterator) - : source(std::move(parse_iterator)), context(source.GetClientContext()) { + : source(std::move(parse_iterator)), context(source.GetClientContext()), + preprocessor(make_uniq(context)) { } StatementIterator::~StatementIterator() = default; @@ -81,7 +83,7 @@ unique_ptr StatementIterator::GetStatementInternal(optional_ptr RegexpReplaceExtractRule::Apply(LogicalOperator &op, vect if (!compiled.ok() || !RegexpHasWholeTextAnchors(compiled.Regexp())) { return nullptr; } + if (group_index > compiled.NumberOfCapturingGroups()) { + // leave reporting the invalid back-reference to regexp_replace + return nullptr; + } string options_str = "k"; if (root.GetChildrenMutable().size() == 4) { diff --git a/src/duckdb/src/optimizer/statistics/expression/propagate_operator.cpp b/src/duckdb/src/optimizer/statistics/expression/propagate_operator.cpp index 94f99ff34..6eba5f59c 100644 --- a/src/duckdb/src/optimizer/statistics/expression/propagate_operator.cpp +++ b/src/duckdb/src/optimizer/statistics/expression/propagate_operator.cpp @@ -1,9 +1,33 @@ #include "duckdb/optimizer/statistics_propagator.hpp" +#include "duckdb/planner/expression/bound_cast_expression.hpp" #include "duckdb/planner/expression/bound_constant_expression.hpp" #include "duckdb/planner/expression/bound_operator_expression.hpp" +#include "duckdb/planner/expression_iterator.hpp" namespace duckdb { +//! Casting a variable-size value to VARIANT throws if the encoded row exceeds the maximum size, even for TRY_CAST +static bool ContainsThrowingVariantCast(const Expression &expr) { + if (BoundCastExpression::IsCast(expr)) { + auto &cast = expr.Cast(); + auto source_type = BoundCastExpression::SourceType(cast); + if (BoundCastExpression::TargetType(cast).id() == LogicalTypeId::VARIANT && + source_type.id() != LogicalTypeId::VARIANT && !TypeIsConstantSize(source_type.InternalType())) { + return true; + } + } + bool result = false; + ExpressionIterator::EnumerateChildren( + expr, [&](const Expression &child) { result = result || ContainsThrowingVariantCast(child); }); + return result; +} + +//! Folding a NULL check removes its child from the plan +static bool CanFoldNullCheck(const BoundOperatorExpression &expr) { + auto &child = *expr.GetChildren()[0]; + return !child.IsVolatile() && !ContainsThrowingVariantCast(child); +} + unique_ptr StatisticsPropagator::PropagateExpression(BoundOperatorExpression &expr, unique_ptr &expr_ptr) { bool all_have_stats = true; @@ -60,6 +84,9 @@ unique_ptr StatisticsPropagator::PropagateExpression(BoundOperat } return std::move(child_stats[0]); case ExpressionType::OPERATOR_IS_NULL: + if (!CanFoldNullCheck(expr)) { + return nullptr; + } if (!child_stats[0]->CanHaveNull()) { // child has no null values: x IS NULL will always be false expr_ptr = make_uniq(Value::BOOLEAN(false)); @@ -72,6 +99,9 @@ unique_ptr StatisticsPropagator::PropagateExpression(BoundOperat } return nullptr; case ExpressionType::OPERATOR_IS_NOT_NULL: + if (!CanFoldNullCheck(expr)) { + return nullptr; + } if (!child_stats[0]->CanHaveNull()) { // child has no null values: x IS NOT NULL will always be true expr_ptr = make_uniq(Value::BOOLEAN(true)); diff --git a/src/duckdb/src/optimizer/type_pushdown.cpp b/src/duckdb/src/optimizer/type_pushdown.cpp index 8ebbbb215..491f4402e 100644 --- a/src/duckdb/src/optimizer/type_pushdown.cpp +++ b/src/duckdb/src/optimizer/type_pushdown.cpp @@ -87,11 +87,19 @@ void FindGetsAndProjections(LogicalOperator &op, Analyses &analyses, Projections } } +// A column binding into a GET is an index into its column ids, not a column id +// itself. Virtual columns (e.g. "filename") have no entry in returned_types, so +// nothing can be pushed into them. +static bool IsRealGetColumn(const LogicalGet &get, ProjectionIndex column_index) { + const auto &column_ids = get.GetColumnIds(); + return column_index < column_ids.size() && !column_ids[column_index].IsVirtualColumn(); +} + optional Resolve(ColumnBinding binding, Analyses &analyses, const Projections &projections) { - if (IsVirtualColumn(binding.column_index)) { - return nullopt; - } if (const auto it = analyses.find(binding.table_index); it != analyses.end()) { + if (!IsRealGetColumn(it->second.get, binding.column_index)) { + return nullopt; + } return {{it->second, binding.column_index, nullptr, binding.column_index}}; } @@ -106,10 +114,10 @@ optional Resolve(ColumnBinding binding, Analyses &analyses, const Pr return nullopt; } const ColumnBinding get_binding = inner->Cast().Binding(); - if (IsVirtualColumn(get_binding.column_index)) { - return nullopt; - } if (const auto it = analyses.find(get_binding.table_index); it != analyses.end()) { + if (!IsRealGetColumn(it->second.get, get_binding.column_index)) { + return nullopt; + } return {{it->second, get_binding.column_index, &projection, binding.column_index}}; } return nullopt; diff --git a/src/duckdb/src/planner/binder/statement/bind_copy.cpp b/src/duckdb/src/planner/binder/statement/bind_copy.cpp index 0b29c4a55..c62f134b1 100644 --- a/src/duckdb/src/planner/binder/statement/bind_copy.cpp +++ b/src/duckdb/src/planner/binder/statement/bind_copy.cpp @@ -92,7 +92,7 @@ identifier_map_t Binder::GetFullCopyOptionsList(const CopyFunction & copy_options["row_group_size_bytes"] = CopyOption(LogicalType::ANY, CopyOptionMode::WRITE_ONLY); copy_options["row_groups_per_file"] = CopyOption(LogicalType::UBIGINT, CopyOptionMode::WRITE_ONLY); copy_options["file_size_bytes"] = CopyOption(LogicalType::ANY, CopyOptionMode::WRITE_ONLY); - copy_options["partition_by"] = CopyOption(LogicalType::ANY, CopyOptionMode::WRITE_ONLY); + copy_options["partition_by"] = CopyOption(LogicalType::LIST(LogicalType::VARCHAR), CopyOptionMode::WRITE_ONLY); copy_options["order_by"] = CopyOption(LogicalType::ANY, CopyOptionMode::WRITE_ONLY); copy_options["return_files"] = CopyOption(LogicalType::BOOLEAN, CopyOptionMode::WRITE_ONLY); copy_options["preserve_order"] = CopyOption(LogicalType::BOOLEAN, CopyOptionMode::WRITE_ONLY); @@ -703,8 +703,9 @@ BoundStatement Binder::BindCopyFrom(CopyStatement &stmt, const CopyFunction &fun return result; } -vector BindCopyOption(ClientContext &context, TableFunctionBinder &option_binder, const Identifier &name, - unique_ptr &expr) { +vector BindCopyOption(ClientContext &context, Binder &binder, const Identifier &name, + unique_ptr &expr, + TableFunctionBinder::IdentifierConversionPolicy conversion_policy) { vector result; if (!expr) { return result; @@ -719,20 +720,16 @@ vector BindCopyOption(ClientContext &context, TableFunctionBinder &option } } const bool is_partition_by = name == "partition_by"; + TableFunctionBinder option_binder(binder, context, "Copy", "Copy options", conversion_policy); if (is_partition_by) { - //! When binding the 'partition_by' option, we don't want to resolve a column reference to a SQLValueFunction - //! (like 'user') + // Partition columns such as 'user' must not resolve to SQL value functions. option_binder.DisableSQLValueFunctions(); } auto bound_expr = option_binder.Bind(expr); if (bound_expr->HasParameter()) { throw ParameterNotResolvedException(); } - if (is_partition_by) { - option_binder.EnableSQLValueFunctions(); - } - auto val = ExpressionExecutor::EvaluateScalar(context, *bound_expr, true); if (val.IsNull()) { throw BinderException("NULL is not supported as a valid option for COPY option \"" + name + "\""); @@ -763,10 +760,10 @@ string ExtractFormat(const string &file_path) { return format.substr(dot_pos + 1); } -void Binder::BindCopyOptions(CopyInfo &info) { - TableFunctionBinder option_binder(*this, context, "Copy", "Copy options"); +void Binder::BindCopyFormat(CopyInfo &info) { + const auto conversion_policy = TableFunctionBinder::IdentifierConversionPolicy::FOLLOW_SETTING; if (info.file_path_expression) { - auto inputs = BindCopyOption(context, option_binder, "filename", info.file_path_expression); + auto inputs = BindCopyOption(context, *this, "filename", info.file_path_expression, conversion_policy); if (inputs.size() != 1 || inputs[0].type().id() != LogicalTypeId::VARCHAR) { throw InternalException("Unsupported parameter type for filename: expected e.g. TARGET 'file.parquet'"); } @@ -777,6 +774,26 @@ void Binder::BindCopyOptions(CopyInfo &info) { info.file_path = inputs[0].ToString(); info.file_path_expression.reset(); } + auto format_entry = info.parsed_options.find("format"); + if (format_entry != info.parsed_options.end()) { + auto inputs = BindCopyOption(context, *this, "format", format_entry->second, conversion_policy); + if (inputs.size() != 1 || inputs[0].type().id() != LogicalTypeId::VARCHAR) { + throw ParserException("Unsupported parameter type for FORMAT: expected e.g. FORMAT 'csv', 'parquet'"); + } + info.format = StringUtil::Lower(inputs[0].ToString()); + info.is_format_auto_detected = false; + info.parsed_options.erase(format_entry); + } + if (info.is_format_auto_detected && info.format.empty()) { + info.format = ExtractFormat(info.file_path); + } +} + +void Binder::BindCopyOptions(CopyInfo &info, const CopyFunction &function) { + identifier_map_t copy_options; + if (function.copy_options) { + copy_options = GetFullCopyOptionsList(function, CopyOptionMode::READ_WRITE); + } unique_ptr partition_path; for (auto &[option_name, option_expr] : info.parsed_options) { if (option_name == "partition_path") { @@ -787,20 +804,13 @@ void Binder::BindCopyOptions(CopyInfo &info) { partition_path = std::move(option_expr); continue; } - auto inputs = BindCopyOption(context, option_binder, option_name, option_expr); - if (option_name == "format") { - // format specifier: interpret this option - if (inputs.size() != 1 || inputs[0].type().id() != LogicalTypeId::VARCHAR) { - throw ParserException("Unsupported parameter type for FORMAT: expected e.g. FORMAT 'csv', 'parquet'"); - } - info.format = StringUtil::Lower(inputs[0].ToString()); - info.is_format_auto_detected = false; - continue; - } - info.options[option_name] = std::move(inputs); - } - if (info.is_format_auto_detected && info.format.empty()) { - info.format = ExtractFormat(info.file_path); + // column list options accept bare column names + auto copy_option = copy_options.find(option_name); + const bool is_column_list = + copy_option != copy_options.end() && copy_option->second.type == LogicalType::LIST(LogicalType::VARCHAR); + auto conversion_policy = is_column_list ? TableFunctionBinder::IdentifierConversionPolicy::ALLOW + : TableFunctionBinder::IdentifierConversionPolicy::FOLLOW_SETTING; + info.options[option_name] = BindCopyOption(context, *this, option_name, option_expr, conversion_policy); } info.parsed_options.clear(); if (partition_path) { @@ -809,8 +819,7 @@ void Binder::BindCopyOptions(CopyInfo &info) { } BoundStatement Binder::Bind(CopyStatement &stmt, CopyToType copy_to_type) { - // bind the copy options - BindCopyOptions(*stmt.info); + BindCopyFormat(*stmt.info); if (stmt.info->is_from && stmt.info->parsed_options.find("partition_path") != stmt.info->parsed_options.end()) { throw InvalidInputException("Option partition_path is not supported for reading - only for writing"); } @@ -857,6 +866,8 @@ BoundStatement Binder::Bind(CopyStatement &stmt, CopyToType copy_to_type) { auto ©_function = entry->Cast(); auto &function = copy_function.function; + // bind the copy options + BindCopyOptions(*stmt.info, function); if (function.copy_options) { // list all copy options - then bind them and offer alternatives auto copy_mode = stmt.info->is_from ? CopyOptionMode::READ_ONLY : CopyOptionMode::WRITE_ONLY; @@ -882,7 +893,7 @@ BoundStatement Binder::Bind(CopyStatement &stmt, CopyToType copy_to_type) { stmt.info->is_from ? "reading" : "writing", stmt.info->is_from ? "writing" : "reading"); } - if (copy_option.type.id() != LogicalTypeId::ANY) { + if (copy_option.type.id() != LogicalTypeId::ANY && copy_option.type.id() != LogicalTypeId::LIST) { if (provided_values.empty()) { if (copy_option.type.id() == LogicalTypeId::BOOLEAN) { // boolean can be empty (e.g. "HEADER") diff --git a/src/duckdb/src/planner/binder/statement/bind_export.cpp b/src/duckdb/src/planner/binder/statement/bind_export.cpp index 521239723..659b3bdf1 100644 --- a/src/duckdb/src/planner/binder/statement/bind_export.cpp +++ b/src/duckdb/src/planner/binder/statement/bind_export.cpp @@ -155,13 +155,14 @@ BoundStatement Binder::Bind(ExportStatement &stmt) { result.types = {LogicalType::BOOLEAN}; result.names = {"Success"}; - // bind copy options - BindCopyOptions(*stmt.info); + BindCopyFormat(*stmt.info); // lookup the format in the catalog auto ©_function = Catalog::GetEntry( context, QualifiedName(Identifier::InvalidCatalog(), Identifier::DefaultSchema(), Identifier(stmt.info->format))); + // bind copy options + BindCopyOptions(*stmt.info, copy_function.function); if (!copy_function.function.copy_to_bind && !copy_function.function.plan) { throw NotImplementedException("COPY TO is not supported for FORMAT \"%s\"", stmt.info->format); } diff --git a/src/duckdb/src/planner/binder/tableref/bind_expressionlistref.cpp b/src/duckdb/src/planner/binder/tableref/bind_expressionlistref.cpp index d91ae8a6f..b26661ad4 100644 --- a/src/duckdb/src/planner/binder/tableref/bind_expressionlistref.cpp +++ b/src/duckdb/src/planner/binder/tableref/bind_expressionlistref.cpp @@ -31,7 +31,34 @@ static void WarnIfDecimalScaleIsReduced(ClientContext &context, const LogicalTyp warned_about_scale_reduction[column_idx] = true; } +static void VerifyExpressionListRef(const ExpressionListRef &expr) { + // the parser guarantees these, but a deserialized ExpressionListRef might not + if (expr.values.empty()) { + return; + } + auto column_count = expr.values[0].size(); + for (auto &expression_list : expr.values) { + if (expression_list.size() != column_count) { + throw BinderException("VALUES lists must all be the same length"); + } + } + if (!expr.expected_types.empty() && expr.expected_types.size() != column_count) { + throw BinderException("VALUES list has %d columns but %d expected types", column_count, + expr.expected_types.size()); + } + if (!expr.expected_names.empty() && expr.expected_names.size() != column_count) { + throw BinderException("VALUES list has %d columns but %d expected names", column_count, + expr.expected_names.size()); + } + for (auto &type : expr.expected_types) { + if (type.IsValid() && !type.IsComplete()) { + throw BinderException("VALUES list has an incomplete expected type %s", type.ToString()); + } + } +} + BoundStatement Binder::Bind(ExpressionListRef &expr) { + VerifyExpressionListRef(expr); BoundStatement result; result.types = expr.expected_types; result.names = expr.expected_names; diff --git a/src/duckdb/src/planner/expression_binder/table_function_binder.cpp b/src/duckdb/src/planner/expression_binder/table_function_binder.cpp index bf694dbaa..b338d14a1 100644 --- a/src/duckdb/src/planner/expression_binder/table_function_binder.cpp +++ b/src/duckdb/src/planner/expression_binder/table_function_binder.cpp @@ -11,9 +11,9 @@ namespace duckdb { TableFunctionBinder::TableFunctionBinder(Binder &binder, ClientContext &context, string table_function_name_p, - string clause_p) + string clause_p, IdentifierConversionPolicy identifier_conversion_policy_p) : ExpressionBinder(binder, context), table_function_name(std::move(table_function_name_p)), - clause(std::move(clause_p)) { + clause(std::move(clause_p)), identifier_conversion_policy(identifier_conversion_policy_p) { } BindResult TableFunctionBinder::BindLambdaReference(LambdaRefExpression &expr, idx_t depth) { @@ -78,7 +78,9 @@ BindResult TableFunctionBinder::BindColumnReference(unique_ptr result_name); } - auto setting = Settings::Get(context); + auto setting = identifier_conversion_policy == IdentifierConversionPolicy::ALLOW + ? TableFunctionIdentifierConversion::ENABLE_IMPLICIT_STRING + : Settings::Get(context); auto implicit_conversion_disabled = setting == TableFunctionIdentifierConversion::DISABLE_IMPLICIT_STRING; auto warn_implicit_conversion = setting == TableFunctionIdentifierConversion::DEFAULT; const auto msg = diff --git a/src/duckdb/src/planner/filter/table_filter_dynamic_function.cpp b/src/duckdb/src/planner/filter/table_filter_dynamic_function.cpp index a60b57e0b..953d9f372 100644 --- a/src/duckdb/src/planner/filter/table_filter_dynamic_function.cpp +++ b/src/duckdb/src/planner/filter/table_filter_dynamic_function.cpp @@ -46,6 +46,11 @@ bool DynamicFilterData::CompareValue(ExpressionType comparison_type, const Value FilterPropagateResult DynamicFilterData::CheckStatistics(const BaseStatistics &stats, ExpressionType comparison_type, const Value &constant) { + if (!stats.CanHaveNoNull()) { + return comparison_type == ExpressionType::COMPARE_DISTINCT_FROM ? FilterPropagateResult::FILTER_ALWAYS_TRUE + : FilterPropagateResult::FILTER_ALWAYS_FALSE; + } + switch (constant.type().InternalType()) { case PhysicalType::UINT8: case PhysicalType::UINT16: diff --git a/src/duckdb/src/planner/operator/logical_get.cpp b/src/duckdb/src/planner/operator/logical_get.cpp index cfa02fb53..c840296a1 100644 --- a/src/duckdb/src/planner/operator/logical_get.cpp +++ b/src/duckdb/src/planner/operator/logical_get.cpp @@ -77,10 +77,10 @@ LogicalGet::LogicalGet(TableIndex table_index, BoundTableFunction function, uniq } optional_ptr LogicalGet::GetTable() const { - if (!function.get_bind_info) { + if (!function.get_table_entry) { return nullptr; } - return function.get_bind_info(bind_data.get()).table; + return function.get_table_entry(bind_data.get()); } InsertionOrderPreservingMap LogicalGet::ParamsToString() const { @@ -310,7 +310,9 @@ idx_t LogicalGet::EstimateCardinality(ClientContext &context) { } idx_t LogicalGet::EstimateSourceCardinality(ClientContext &context) { - if (function.cardinality) { + // planning a LogicalGet consumes its bind data - the callbacks that require it must not be called, + // while callbacks that accept a null bind data (e.g. the range functions) return no estimate anyway + if (function.cardinality && bind_data) { auto node_stats = function.cardinality(context, bind_data.get()); if (node_stats && node_stats->has_estimated_cardinality) { return node_stats->estimated_cardinality; diff --git a/src/duckdb/src/planner/sql_export/logical_plan_sql_exporter.cpp b/src/duckdb/src/planner/sql_export/logical_plan_sql_exporter.cpp index 08652de6e..b089f4833 100644 --- a/src/duckdb/src/planner/sql_export/logical_plan_sql_exporter.cpp +++ b/src/duckdb/src/planner/sql_export/logical_plan_sql_exporter.cpp @@ -3,6 +3,7 @@ #include "duckdb/planner/logical_plan_sql_exporter.hpp" #include "duckdb/planner/sql_export/bound_expression_sql_exporter_internal.hpp" #include "duckdb/parser/expression/columnref_expression.hpp" +#include "duckdb/parser/expression/constant_expression.hpp" #include "duckdb/parser/query_node/select_node.hpp" #include "duckdb/planner/bound_expression_sql_exporter.hpp" #include "duckdb/planner/operator/logical_get.hpp" @@ -73,6 +74,19 @@ static optional ConstantSQLInput(const Expression &expression, LogicalOpe } } +static LogicalPlanSQLExportResult EnsureSQLProjection(LogicalPlanSQLExportResult result) { + if (result.HasError() || !result.GetValue().fields.empty() || + result.GetValue().query->type != QueryNodeType::SELECT_NODE) { + return result; + } + auto &select = result.GetValue().query->Cast(); + if (select.select_list.empty()) { + // SQL needs a SELECT expression even when the logical relation only carries row counts. + select.select_list.push_back(make_uniq(Literal::Null())); + } + return result; +} + static LogicalPlanSQLExportResult ApplyOutputNames(LogicalPlanSQLExportResult result, const vector &output_names) { if (result.HasError()) { @@ -82,6 +96,9 @@ static LogicalPlanSQLExportResult ApplyOutputNames(LogicalPlanSQLExportResult re return LogicalPlanSQLExportResult::Failure({LogicalPlanSQLExportHelpers::PlanUnsupportedFeature( LogicalPlanVerificationPath(), "output_names", "Output name count does not match the exported plan")}); } + if (output_names.empty()) { + return result; + } if (result.GetValue().query->type == QueryNodeType::SELECT_NODE) { auto &select = result.GetValue().query->Cast(); D_ASSERT(select.select_list.size() == output_names.size()); @@ -125,15 +142,15 @@ LogicalPlanSQLExportResult LogicalPlanSQLExportContext::Export(LogicalOperator & for (auto scope = source_scope; scope; scope = scope->parent) { for (auto &source : scope->sources) { if (&source.op.get() == &op) { - return LogicalPlanSQLExportResult::Success( - {CreateNamedSource(source.name, source.relation.fields), source.relation.fields}); + return EnsureSQLProjection(LogicalPlanSQLExportResult::Success( + {CreateNamedSource(source.name, source.relation.fields), source.relation.fields})); } } } ancestors.push_back(op); auto result = op.ToSQL(*this, path); ancestors.pop_back(); - return result; + return EnsureSQLProjection(std::move(result)); } void LogicalPlanSQLExportContext::PushNamedRelation(TableIndex index, const Identifier &name, bool is_recurring) { diff --git a/src/duckdb/src/planner/sql_export/sql_export_relations.cpp b/src/duckdb/src/planner/sql_export/sql_export_relations.cpp index c2f622261..2a32c15e2 100644 --- a/src/duckdb/src/planner/sql_export/sql_export_relations.cpp +++ b/src/duckdb/src/planner/sql_export/sql_export_relations.cpp @@ -323,7 +323,7 @@ LogicalPlanSQLExportResult LogicalSetOperation::ToSQL(LogicalPlanSQLExportContex // Retain the UNION boundary when optimization removed every other arm. LogicalEmptyResult empty(op.types, op.GetColumnBindings()); empty.ResolveOperatorTypes(); - auto exported = empty.ToSQL(export_context, path); + auto exported = export_context.Export(empty, path); if (exported.HasError()) { return exported; } diff --git a/src/duckdb/src/planner/sql_export/sql_export_values.cpp b/src/duckdb/src/planner/sql_export/sql_export_values.cpp index 87ac58933..11b067408 100644 --- a/src/duckdb/src/planner/sql_export/sql_export_values.cpp +++ b/src/duckdb/src/planner/sql_export/sql_export_values.cpp @@ -238,14 +238,18 @@ LogicalPlanSQLExportResult LogicalColumnDataGet::ToSQL(LogicalPlanSQLExportConte remaining -= chunk.size(); for (idx_t row = 0; row < chunk.size(); row++) { vector> exported_row; - for (idx_t column = 0; column < chunk.ColumnCount(); column++) { - auto value = - BoundExpressionSQLExporter::Export(BoundConstantExpression(chunk.GetValue(column, row)), {}); - if (value.HasError()) { - return LogicalPlanSQLExportResult::Failure({LogicalPlanSQLExportHelpers::PlanUnsupportedFeature( - path, "chunk_value", "The materialized value cannot be represented in SQL")}); + if (chunk.ColumnCount() == 0) { + exported_row.push_back(make_uniq(Literal::Null())); + } else { + for (idx_t column = 0; column < chunk.ColumnCount(); column++) { + auto value = + BoundExpressionSQLExporter::Export(BoundConstantExpression(chunk.GetValue(column, row)), {}); + if (value.HasError()) { + return LogicalPlanSQLExportResult::Failure({LogicalPlanSQLExportHelpers::PlanUnsupportedFeature( + path, "chunk_value", "The materialized value cannot be represented in SQL")}); + } + exported_row.push_back(std::move(value.GetValue())); } - exported_row.push_back(std::move(value.GetValue())); } values->values.push_back(std::move(exported_row)); } diff --git a/src/duckdb/src/planner/statement_preprocessor.cpp b/src/duckdb/src/planner/statement_preprocessor.cpp index 8f03cf908..f3268cce0 100644 --- a/src/duckdb/src/planner/statement_preprocessor.cpp +++ b/src/duckdb/src/planner/statement_preprocessor.cpp @@ -126,15 +126,26 @@ void StatementPreprocessor::Preprocess(ClientContextLock &lock, vectortype == StatementType::PRAGMA_STATEMENT || stmt->type == StatementType::MULTI_STATEMENT) { needs_preprocessing = true; - break; + needs_transaction = true; + } else if (stmt->type == StatementType::TRANSACTION_STATEMENT) { + needs_preprocessing = true; } } if (!needs_preprocessing) { return; } + if (!needs_transaction) { + // Transaction statements only update chained_transaction_state. Wrapping them in a transaction would start a + // throwaway autocommit transaction for a BEGIN (consuming a transaction id), and would make a ROLLBACK of an + // aborted transaction fail. + PreprocessInternal(lock, statements, transaction_context_state); + return; + } context.RunFunctionInTransactionInternal(lock, [&] { PreprocessInternal(lock, statements, transaction_context_state); }); @@ -142,7 +153,6 @@ void StatementPreprocessor::Preprocess(ClientContextLock &lock, vector> &statements, const CurrentTransactionState transaction_context_state) { - CurrentTransactionState chained_transaction_state = NOT_IN_ACTIVE_TRANSACTION; vector> new_statements; for (idx_t i = 0; i < statements.size(); i++) { auto query = statements[i]->query; diff --git a/src/duckdb/src/storage/buffer/block_handle.cpp b/src/duckdb/src/storage/buffer/block_handle.cpp index f185a2338..844871e02 100644 --- a/src/duckdb/src/storage/buffer/block_handle.cpp +++ b/src/duckdb/src/storage/buffer/block_handle.cpp @@ -224,15 +224,17 @@ BufferHandle BlockHandle::Load(QueryContext context, unique_ptr reus return BufferHandle(shared_from_this(), memory.GetBuffer()); } + if (!memory.CanReload()) { + // the buffer was destroyed upon unpin/evict, or the handle was replaced and its disk block + // may be rewritten in place - the caller must re-resolve the block id + return BufferHandle(); + } + if (BlockId() < MAXIMUM_BLOCK) { auto block = AllocateBlock(block_manager, std::move(reusable_buffer), block_id); block_manager.Read(context, *block); memory.GetBuffer() = std::move(block); } else { - if (!memory.MustWriteToTemporaryFile()) { - // The buffer was destroyed upon unpin/evict, so there is no temporary buffer to read. - return BufferHandle(); - } auto &buffer_manager = memory.GetBufferManager(); auto &buffer = memory.GetBuffer(); buffer = buffer_manager.ReadTemporaryBuffer(context, memory.GetMemoryTag(), *this, std::move(reusable_buffer)); diff --git a/src/duckdb/src/storage/buffer/block_manager.cpp b/src/duckdb/src/storage/buffer/block_manager.cpp index 44abcf0a5..e20b25bd3 100644 --- a/src/duckdb/src/storage/buffer/block_manager.cpp +++ b/src/duckdb/src/storage/buffer/block_manager.cpp @@ -132,13 +132,29 @@ void BlockManager::UnregisterBlock(block_id_t id) { blocks.erase(id); } +bool BlockManager::UnregisterExpiredBlock(block_id_t id) { + D_ASSERT(id < MAXIMUM_BLOCK); + lock_guard lock(blocks_lock); + auto entry = blocks.find(id); + if (entry == blocks.end()) { + // the block id was already unregistered + return true; + } + if (!entry->second.expired()) { + // a newer handle was registered for this block id - the registration is not ours to remove + return false; + } + blocks.erase(entry); + return true; +} + void BlockManager::UnregisterPersistentBlock(BlockHandle &block) { if (in_destruction) { return; } auto id = block.BlockId(); D_ASSERT(id < MAXIMUM_BLOCK); - UnregisterBlock(id); + UnregisterExpiredBlock(id); } MetadataManager &BlockManager::GetMetadataManager() { diff --git a/src/duckdb/src/storage/data_table.cpp b/src/duckdb/src/storage/data_table.cpp index 45fd7bc38..2d068c373 100644 --- a/src/duckdb/src/storage/data_table.cpp +++ b/src/duckdb/src/storage/data_table.cpp @@ -435,10 +435,6 @@ void DataTable::CleanupAppend(VisibilityBound lowest_visibility_bound, idx_t sta row_groups->CleanupAppend(lowest_visibility_bound, start, count); } -bool DataTable::IndexNameIsUnique(const string &name) { - return info->indexes.NameIsUnique(name); -} - Identifier DataTableInfo::GetSchemaName() { return schema_path.back(); } diff --git a/src/duckdb/src/storage/external_file_cache/caching_file_system.cpp b/src/duckdb/src/storage/external_file_cache/caching_file_system.cpp index 628109e4d..15d9d4c59 100644 --- a/src/duckdb/src/storage/external_file_cache/caching_file_system.cpp +++ b/src/duckdb/src/storage/external_file_cache/caching_file_system.cpp @@ -7,6 +7,7 @@ #include "duckdb/common/enums/memory_tag.hpp" #include "duckdb/common/file_system.hpp" #include "duckdb/main/client_context.hpp" +#include "duckdb/main/settings.hpp" #include "duckdb/parallel/task_executor.hpp" #include "duckdb/parallel/task_scheduler.hpp" #include "duckdb/storage/buffer/block_handle.hpp" @@ -353,13 +354,24 @@ FileBufferHandleGroup CachingFileHandle::Read(const idx_t nr_bytes, const idx_t const idx_t max_block_size = external_file_cache.GetCacheMaxBlockSize(current_cached_file->path); const idx_t min_block_size = MinValue(external_file_cache.GetCacheMinBlockSize(current_cached_file->path), max_block_size); + auto request_sizing = flags.GetRequestSizing(); + switch (Settings::Get(caching_file_system.db)) { + case ExternalFileCacheRequestSizing::GRID: + request_sizing = RequestSizing::BY_CACHE; + break; + case ExternalFileCacheRequestSizing::EXACT: + request_sizing = RequestSizing::BY_READER; + break; + case ExternalFileCacheRequestSizing::AUTO: + break; + } idx_t fetch_location = location; idx_t fetch_end = location + nr_bytes; if (file_size <= max_block_size) { // fetch a file that fits in one block whole, so later reads of it hit the cache fetch_location = 0; fetch_end = file_size; - } else if (flags.GetRequestSizing() == RequestSizing::BY_CACHE) { + } else if (request_sizing == RequestSizing::BY_CACHE) { fetch_location = location - location % max_block_size; fetch_end = AlignValue(fetch_end, max_block_size); } else { diff --git a/src/duckdb/src/storage/index_entry.cpp b/src/duckdb/src/storage/index_entry.cpp index d94bbf923..f370520d2 100644 --- a/src/duckdb/src/storage/index_entry.cpp +++ b/src/duckdb/src/storage/index_entry.cpp @@ -362,6 +362,10 @@ void IndexEntry::VerifyAppend(const shared_ptr &delete_entry, DataCh } } bound_index.VerifyAppend(chunk, index_append_info, manager); + if (auto delta = deltas.Find(IndexDeltaType::ADDED_DATA_DURING_CHECKPOINT)) { + // rows committed while a checkpoint runs are only in this delta + delta->VerifyAppend(chunk, index_append_info, manager); + } } void IndexEntry::VerifyForeignKey(const shared_ptr &delete_entry, DataChunk &chunk, diff --git a/src/duckdb/src/storage/metadata/metadata_manager.cpp b/src/duckdb/src/storage/metadata/metadata_manager.cpp index dfa05660c..b465283b2 100644 --- a/src/duckdb/src/storage/metadata/metadata_manager.cpp +++ b/src/duckdb/src/storage/metadata/metadata_manager.cpp @@ -101,30 +101,38 @@ MetadataHandle MetadataManager::Pin(const MetadataPointer &pointer) { MetadataHandle MetadataManager::Pin(const QueryContext &context, const MetadataPointer &pointer) { D_ASSERT(pointer.index < METADATA_BLOCK_COUNT); - shared_ptr block_handle; - { - lock_guard guard(block_lock); - auto entry = blocks.find(UnsafeNumericCast(pointer.block_index)); - if (entry == blocks.end()) { - throw InternalException("Trying to pin block %llu - but the block did not exist", pointer.block_index); - } - auto &block = entry->second; + while (true) { + shared_ptr block_handle; + { + lock_guard guard(block_lock); + auto entry = blocks.find(UnsafeNumericCast(pointer.block_index)); + if (entry == blocks.end()) { + throw InternalException("Trying to pin block %llu - but the block did not exist", pointer.block_index); + } + auto &block = entry->second; #ifdef DEBUG - for (auto &free_block : block.free_blocks) { - if (free_block == pointer.index) { - throw InternalException("Pinning block %d.%d but it is marked as a free block", block.block_id, - free_block); + for (auto &free_block : block.free_blocks) { + if (free_block == pointer.index) { + throw InternalException("Pinning block %d.%d but it is marked as a free block", block.block_id, + free_block); + } } - } #endif - block_handle = block.block; - } + block_handle = block.block; + } - MetadataHandle handle; - handle.pointer.block_index = pointer.block_index; - handle.pointer.index = pointer.index; - handle.handle = buffer_manager.Pin(block_handle); - return handle; + auto buffer_handle = buffer_manager.Pin(block_handle); + if (!buffer_handle.IsValid()) { + // the handle was replaced by ConvertToTransient while we were pinning it and can no + // longer be loaded from disk - retry with the current handle + continue; + } + MetadataHandle handle; + handle.pointer.block_index = pointer.block_index; + handle.pointer.index = pointer.index; + handle.handle = std::move(buffer_handle); + return handle; + } } void MetadataManager::ConvertToTransient(unique_lock &block_lock, MetadataBlock &metadata_block) { @@ -141,6 +149,13 @@ void MetadataManager::ConvertToTransient(unique_lock &block_lock, Metadat // copy the data to the transient block memcpy(new_buffer.GetDataMutable(), old_buffer.Ptr(), block_manager.GetBlockSize()); + // the disk block now belongs to the transient block and is rewritten in place by a later + // checkpoint - the old handle must not reload it from disk after its buffer is evicted + { + auto old_block_lock = old_block->GetMemory().GetLock(); + old_block->GetMemory().SetDestroyBufferUpon(DestroyBufferUpon::EVICTION); + } + // unregister the old block block_manager.UnregisterBlock(metadata_block.block_id); diff --git a/src/duckdb/src/storage/serialization/serialize_query_node.cpp b/src/duckdb/src/storage/serialization/serialize_query_node.cpp index b933c654d..31be0071c 100644 --- a/src/duckdb/src/storage/serialization/serialize_query_node.cpp +++ b/src/duckdb/src/storage/serialization/serialize_query_node.cpp @@ -6,6 +6,7 @@ #include "duckdb/common/serializer/serializer.hpp" #include "duckdb/common/serializer/deserializer.hpp" #include "duckdb/parser/query_node/list.hpp" +#include "duckdb/parser/tableref/emptytableref.hpp" #include "duckdb/parser/query_node/update_query_node.hpp" #include "duckdb/parser/query_node/delete_query_node.hpp" #include "duckdb/parser/query_node/insert_query_node.hpp" @@ -218,6 +219,9 @@ unique_ptr SelectNode::Deserialize(Deserializer &deserializer) { deserializer.ReadPropertyWithDefault>(206, "having", result->having); deserializer.ReadPropertyWithDefault>(207, "sample", result->sample); deserializer.ReadPropertyWithDefault>(208, "qualify", result->qualify); + if (!result->from_table) { + result->from_table = make_uniq(); + } return std::move(result); } diff --git a/src/duckdb/src/storage/serialization/serialize_tableref.cpp b/src/duckdb/src/storage/serialization/serialize_tableref.cpp index 15ba6006b..be0bbc9fd 100644 --- a/src/duckdb/src/storage/serialization/serialize_tableref.cpp +++ b/src/duckdb/src/storage/serialization/serialize_tableref.cpp @@ -114,6 +114,9 @@ unique_ptr ColumnDataRef::Deserialize(Deserializer &deserializer) { auto expected_names = deserializer.ReadPropertyWithDefault>(200, "expected_names"); auto collection = deserializer.ReadPropertyWithDefault>(202, "collection"); auto result = duckdb::unique_ptr(new ColumnDataRef(std::move(collection), std::move(expected_names))); + if (!result->collection) { + throw SerializationException("ColumnDataRef is missing its collection"); + } return std::move(result); } diff --git a/src/duckdb/src/storage/single_file_block_manager.cpp b/src/duckdb/src/storage/single_file_block_manager.cpp index 0f2b8ea9a..07cf07028 100644 --- a/src/duckdb/src/storage/single_file_block_manager.cpp +++ b/src/duckdb/src/storage/single_file_block_manager.cpp @@ -1197,10 +1197,15 @@ void SingleFileBlockManager::ReadBlock(Block &block, bool skip_block_header) con void SingleFileBlockManager::Read(QueryContext context, Block &block) { D_ASSERT(block.id >= 0); - D_ASSERT(std::find(free_list.begin(), free_list.end(), block.id) == free_list.end()); + D_ASSERT(!BlockIsFreeListed(block.id)); ReadAndChecksum(context, block, GetBlockLocation(block.id)); } +bool SingleFileBlockManager::BlockIsFreeListed(block_id_t block_id) { + lock_guard lock(single_file_block_lock); + return free_list.find(block_id) != free_list.end(); +} + void SingleFileBlockManager::ReadBlocks(QueryContext context, FileBuffer &buffer, block_id_t start_block, idx_t block_count) { D_ASSERT(start_block >= 0); @@ -1375,12 +1380,9 @@ void SingleFileBlockManager::WriteHeader(QueryContext context, DatabaseHeader he // no blocks in the free list header.free_list = DConstants::INVALID_INDEX; } - lock.unlock(); - metadata_manager.Flush(context); - - lock.lock(); header.block_count = NumericCast(max_block); lock.unlock(); + metadata_manager.Flush(context); header.storage_compatibility = options.storage_version; @@ -1435,11 +1437,11 @@ void SingleFileBlockManager::FileSync() { handle->Sync(); } -void SingleFileBlockManager::UnregisterBlock(block_id_t id) { - // perform the actual unregistration - BlockManager::UnregisterBlock(id); - // check if it is part of the newly free list - lock_guard lock(single_file_block_lock); +void SingleFileBlockManager::ReleaseFreeBlockInUse(unique_lock &lock, block_id_t id) { + if (!lock.owns_lock()) { + throw InternalException("ReleaseFreeBlockInUse must be called while holding the lock"); + } + // check if the block is part of the newly free list auto entry = free_blocks_in_use.find(id); if (entry != free_blocks_in_use.end()) { // it is! move it to the regular free list so the block can be re-used @@ -1448,6 +1450,27 @@ void SingleFileBlockManager::UnregisterBlock(block_id_t id) { } } +void SingleFileBlockManager::UnregisterBlock(block_id_t id) { + unique_lock lock(single_file_block_lock); + // perform the actual unregistration + BlockManager::UnregisterBlock(id); + ReleaseFreeBlockInUse(lock, id); +} + +bool SingleFileBlockManager::UnregisterExpiredBlock(block_id_t id) { + // hold the lock across the live-handle check and the free-list transition: otherwise a + // re-registration and retirement of the block id can interleave between them, and the + // free-list transition would run although a live handle exists + // lock order: single_file_block_lock before blocks_lock, as in AddFreeBlock + unique_lock lock(single_file_block_lock); + if (!BlockManager::UnregisterExpiredBlock(id)) { + // a newer handle is registered for this block id - the block id is still in use + return false; + } + ReleaseFreeBlockInUse(lock, id); + return true; +} + void SingleFileBlockManager::TrimFreeBlockRange(block_id_t start, block_id_t end) { auto block_count = NumericCast(end + 1 - start); auto offset = BLOCK_START + (NumericCast(start) * GetBlockAllocSize()); diff --git a/src/duckdb/src/storage/table/scan_state.cpp b/src/duckdb/src/storage/table/scan_state.cpp index a879ff67c..1edf103b8 100644 --- a/src/duckdb/src/storage/table/scan_state.cpp +++ b/src/duckdb/src/storage/table/scan_state.cpp @@ -190,6 +190,16 @@ void ColumnScanState::Next(idx_t count) { } } +void ColumnScanState::ReleasePins() { + current = nullptr; + initialized = false; + scan_state.reset(); + previous_states.clear(); + for (auto &child_state : child_states) { + child_state.ReleasePins(); + } +} + const vector &CollectionScanState::GetColumnIds() { return parent.GetColumnIds(); } @@ -369,6 +379,20 @@ void TableScanState::InitializeColumnScans() { local_state.InitializeColumnScans(); } +void CollectionScanState::ReleasePins() { + SetRowGroup(nullptr); + prepared_vector.Reset(); + column_scans_pending = false; + for (auto &column_scan : column_scans) { + column_scan.ReleasePins(); + } +} + +void TableScanState::ReleasePins() { + table_state.ReleasePins(); + local_state.ReleasePins(); +} + idx_t CollectionScanState::RemainingAssignmentRows() const { const idx_t current_row = vector_index * STANDARD_VECTOR_SIZE; return current_row < max_row_group_row ? max_row_group_row - current_row : 0; diff --git a/src/duckdb/src/storage/table/variant/variant_shredding.cpp b/src/duckdb/src/storage/table/variant/variant_shredding.cpp index 61b1094bb..51a5b6a26 100644 --- a/src/duckdb/src/storage/table/variant/variant_shredding.cpp +++ b/src/duckdb/src/storage/table/variant/variant_shredding.cpp @@ -439,12 +439,17 @@ bool VariantShreddingStats::GetShreddedTypeInternal(const VariantColumnStatsData bool fully_consistent = !force_partial && max_count == total_value_count; if (type_index == static_cast(VariantLogicalType::OBJECT)) { child_list_t child_types; + case_insensitive_string_set_t field_names; for (auto &entry : column.field_stats) { auto &child_column = GetColumnStats(entry.second); if (entry.first.empty()) { //! Do not include empty field names in the shredded type! continue; } + if (!field_names.emplace(entry.first).second) { + //! STRUCT field names are case-insensitive, but VARIANT object keys are case-sensitive. + return false; + } LogicalType child_type; if (GetShreddedTypeInternal(child_column, child_type, total_value_count, force_partial)) { child_types.emplace_back(entry.first, child_type); diff --git a/src/duckdb/src/storage/table_index_list.cpp b/src/duckdb/src/storage/table_index_list.cpp index 3e5cb1e61..ea2d43c1a 100644 --- a/src/duckdb/src/storage/table_index_list.cpp +++ b/src/duckdb/src/storage/table_index_list.cpp @@ -327,29 +327,6 @@ bool TableIndexList::HasBufferedReplays() const { return false; } -bool TableIndexList::NameIsUnique(const string &name) const { - annotated_lock_guard lock(index_entries_lock); - // Only covers PK, FK, and UNIQUE indexes. - // is_unique also covers primary-key indexes. - for (const auto &entry : index_entries) { - auto index_info = entry->GetStorageInfo(); - if ((index_info.is_unique || index_info.is_foreign) && entry->GetName() == name) { - return false; - } - } - return true; -} - -bool TableIndexList::Contains(const Identifier &name) const { - annotated_lock_guard lock(index_entries_lock); - for (const auto &entry : index_entries) { - if (entry->GetName() == name) { - return true; - } - } - return false; -} - shared_ptr TableIndexList::FindEntry(const Identifier &name) const { annotated_lock_guard lock(index_entries_lock); for (const auto &entry : index_entries) { diff --git a/src/duckdb/ub_src_execution.cpp b/src/duckdb/ub_src_execution.cpp index eb44a6519..8159c07dc 100644 --- a/src/duckdb/ub_src_execution.cpp +++ b/src/duckdb/ub_src_execution.cpp @@ -24,6 +24,8 @@ #include "src/execution/physical_plan_generator.cpp" +#include "src/execution/radix_ht_adaptivity.cpp" + #include "src/execution/radix_partitioned_hashtable.cpp" #include "src/execution/row_id_deduplicator.cpp"