From 6dac213f446a882a73b95001053bf71879befaf5 Mon Sep 17 00:00:00 2001 From: DuckLabs GitHub Bot Date: Thu, 15 Oct 2026 20:27:08 +0000 Subject: [PATCH] Update vendored DuckDB sources to 2bb5316a8b5 --- .../core_functions/scalar/date/date_part.cpp | 29 +- .../scalar/list/list_reduce.cpp | 2 +- src/duckdb/extension/icu/icu-datepart.cpp | 4 +- .../catalog_entry/duck_schema_entry.cpp | 7 +- .../catalog_entry/duck_table_entry.cpp | 15 +- .../catalog_entry/table_catalog_entry.cpp | 20 +- src/duckdb/src/catalog/catalog_set.cpp | 24 +- .../src/catalog/default/default_views.cpp | 8 +- src/duckdb/src/catalog/dependency_manager.cpp | 6 +- src/duckdb/src/catalog/duck_catalog.cpp | 17 +- src/duckdb/src/common/enum_util.cpp | 62 +- .../src/common/enums/optimizer_type.cpp | 1 + src/duckdb/src/execution/index/art/art.cpp | 107 +- .../src/execution/index/bound_index.cpp | 6 +- .../execution/index/fixed_size_allocator.cpp | 94 +- .../src/execution/index/fixed_size_buffer.cpp | 108 +- .../src/execution/index/unbound_index.cpp | 20 +- .../operator/order/physical_top_n.cpp | 1 + .../operator/persistent/physical_insert.cpp | 1 + .../operator/scan/physical_table_scan.cpp | 1 + .../operator/schema/physical_create_index.cpp | 16 +- .../execution/radix_partitioned_hashtable.cpp | 1 + .../src/function/scalar/string/like.cpp | 5 +- src/duckdb/src/function/table/read_duckdb.cpp | 2 +- .../table/system/duckdb_constraints.cpp | 10 + src/duckdb/src/function/table/table_scan.cpp | 1 + .../function/table/version/pragma_version.cpp | 6 +- .../catalog_entry/duck_schema_entry.hpp | 1 + .../catalog_entry/duck_table_entry.hpp | 4 +- .../include/duckdb/catalog/catalog_set.hpp | 5 +- .../duckdb/catalog/dependency_manager.hpp | 2 +- .../include/duckdb/catalog/duck_catalog.hpp | 3 +- .../src/include/duckdb/common/enum_util.hpp | 22 +- .../common/enums/constraint_check_mode.hpp | 17 + .../enums/database_modification_type.hpp | 3 + .../duckdb/common/enums/optimizer_type.hpp | 3 +- .../duckdb/execution/index/art/art.hpp | 32 +- .../execution/index/art/art_operator.hpp | 38 +- .../duckdb/execution/index/bound_index.hpp | 13 +- .../execution/index/fixed_size_allocator.hpp | 8 +- .../execution/index/fixed_size_buffer.hpp | 26 +- .../duckdb/execution/index/unbound_index.hpp | 7 +- .../operator/schema/physical_create_index.hpp | 1 + .../src/include/duckdb/main/settings.hpp | 13 + .../optimizer/remove_redundant_order_keys.hpp | 20 + .../optimizer/statistics_propagator.hpp | 6 +- .../duckdb/optimizer/window_self_join.hpp | 3 + .../parser/constraints/unique_constraint.hpp | 13 +- .../duckdb/parser/parsed_data/alter_info.hpp | 2 + .../duckdb/parser/parsed_data/create_info.hpp | 2 + .../duckdb/parser/parsed_data/drop_info.hpp | 2 + .../peg/ast/column_constraint_entry.hpp | 2 +- .../peg/transformer/peg_transformer.hpp | 18 +- .../src/include/duckdb/planner/binder.hpp | 2 +- .../expression_binder/index_binder.hpp | 1 + .../storage/checkpoint/checkpoint_options.hpp | 5 +- .../storage/checkpoint/table_data_writer.hpp | 12 +- .../storage/checkpoint/table_index_writer.hpp | 64 ++ .../duckdb/storage/checkpoint_manager.hpp | 13 +- .../src/include/duckdb/storage/data_table.hpp | 6 +- .../src/include/duckdb/storage/index.hpp | 20 + .../duckdb/storage/index_storage_info.hpp | 2 + .../include/duckdb/storage/storage_info.hpp | 3 - .../duckdb/storage/storage_manager.hpp | 10 +- .../duckdb/storage/table/append_state.hpp | 2 - .../duckdb/storage/table/data_table_info.hpp | 5 - .../duckdb/storage/table/delete_state.hpp | 1 - .../storage/table/in_memory_checkpoint.hpp | 3 +- .../duckdb/storage/table/index_entry.hpp | 29 +- .../duckdb/storage/table/row_group.hpp | 35 +- .../storage/table/row_group_collection.hpp | 26 +- .../storage/table/row_version_manager.hpp | 6 +- .../duckdb/storage/table/scan_state.hpp | 13 - .../duckdb/storage/table/table_index_list.hpp | 28 +- .../duckdb/storage/table/table_statistics.hpp | 1 - .../duckdb/storage/table/update_segment.hpp | 17 +- .../duckdb/transaction/duck_transaction.hpp | 21 +- .../transaction/duck_transaction_manager.hpp | 2 +- .../duckdb/transaction/update_info.hpp | 45 +- src/duckdb/src/main/config.cpp | 1 + src/duckdb/src/main/query_profiler.cpp | 1 + .../src/optimizer/filter_statistics.cpp | 2 +- src/duckdb/src/optimizer/optimizer.cpp | 10 +- .../optimizer/pushdown/pushdown_aggregate.cpp | 2 +- .../optimizer/pushdown/pushdown_window.cpp | 4 +- .../optimizer/remove_redundant_order_keys.cpp | 361 +++++++ .../rule/list_comprehension_rewrite.cpp | 28 +- .../operator/propagate_aggregate.cpp | 5 + .../src/optimizer/statistics_propagator.cpp | 4 +- src/duckdb/src/optimizer/window_self_join.cpp | 39 +- .../parser/constraints/unique_constraint.cpp | 25 +- .../src/parser/parsed_data/alter_info.cpp | 17 + .../src/parser/parsed_data/create_info.cpp | 17 + .../src/parser/parsed_data/drop_info.cpp | 8 + .../transformer/transform_create_table.cpp | 38 +- .../peg/transformer/transform_generated.cpp | 24 +- .../transform_generated_trampoline.cpp | 24 +- .../planner/binder/statement/bind_alter.cpp | 4 +- .../planner/binder/statement/bind_create.cpp | 4 +- .../binder/statement/bind_create_table.cpp | 46 +- .../planner/binder/statement/bind_drop.cpp | 4 +- .../planner/binder/statement/bind_insert.cpp | 20 +- .../expression_binder/index_binder.cpp | 1 + .../storage/checkpoint/table_data_writer.cpp | 40 +- .../storage/checkpoint/table_index_writer.cpp | 32 + src/duckdb/src/storage/checkpoint_manager.cpp | 88 +- src/duckdb/src/storage/data_table.cpp | 49 +- src/duckdb/src/storage/index.cpp | 6 +- src/duckdb/src/storage/index_entry.cpp | 84 +- src/duckdb/src/storage/local_storage.cpp | 3 +- .../src/storage/metadata/metadata_reader.cpp | 2 + .../src/storage/metadata/metadata_writer.cpp | 2 + .../serialization/serialize_constraint.cpp | 4 +- src/duckdb/src/storage/storage_manager.cpp | 32 +- .../storage/table/in_memory_checkpoint.cpp | 46 +- src/duckdb/src/storage/table/row_group.cpp | 114 +-- .../storage/table/row_group_collection.cpp | 153 ++- .../src/storage/table/row_version_manager.cpp | 32 +- .../src/storage/table/table_statistics.cpp | 9 +- .../src/storage/table/update_segment.cpp | 960 ++++++++++-------- src/duckdb/src/storage/table_index_list.cpp | 75 +- src/duckdb/src/storage/wal_replay.cpp | 15 +- src/duckdb/src/storage/write_ahead_log.cpp | 10 +- .../src/transaction/duck_transaction.cpp | 81 +- .../transaction/duck_transaction_manager.cpp | 52 +- .../src/transaction/wal_write_state.cpp | 3 +- src/duckdb/ub_src_optimizer.cpp | 2 + src/duckdb/ub_src_storage_checkpoint.cpp | 2 + 128 files changed, 2477 insertions(+), 1320 deletions(-) create mode 100644 src/duckdb/src/include/duckdb/common/enums/constraint_check_mode.hpp create mode 100644 src/duckdb/src/include/duckdb/optimizer/remove_redundant_order_keys.hpp create mode 100644 src/duckdb/src/include/duckdb/storage/checkpoint/table_index_writer.hpp create mode 100644 src/duckdb/src/optimizer/remove_redundant_order_keys.cpp create mode 100644 src/duckdb/src/storage/checkpoint/table_index_writer.cpp diff --git a/src/duckdb/extension/core_functions/scalar/date/date_part.cpp b/src/duckdb/extension/core_functions/scalar/date/date_part.cpp index 7d3e6fb3d..de8e5aa30 100644 --- a/src/duckdb/extension/core_functions/scalar/date/date_part.cpp +++ b/src/duckdb/extension/core_functions/scalar/date/date_part.cpp @@ -437,8 +437,32 @@ struct DatePart { return YearWeekFromParts(yyyy, ww); } + // ISO years >= 1 use yyyy * 100 + week. Year 0 and negative years subtract the week, + // so the function decreases as the week number increases. + static int32_t MinISOYear(date_t input) { + return Date::ExtractISOYearNumber(input); + } + static int32_t MinISOYear(timestamp_t input) { + return Date::ExtractISOYearNumber(Timestamp::GetDate(input)); + } + template + static int32_t MinISOYear(U) { + return 0; + } + template static unique_ptr PropagateStatistics(ClientContext &context, FunctionStatisticsInput &input) { + // Endpoint bounds do not contain the values in between once the range reaches the + // subtracted-week encoding. Positive ISO years are non-decreasing in the date. + auto &nstats = input.child_stats[0]; + if (!NumericStats::HasMinMax(nstats)) { + return nullptr; + } + auto min = NumericStats::GetMin(nstats); + auto max = NumericStats::GetMax(nstats); + if (min > max || !min.IsFinite() || !max.IsFinite() || MinISOYear(min) < 1) { + return nullptr; + } return PropagateDatePartStatistics(input.child_stats); } }; @@ -2646,9 +2670,8 @@ ScalarFunctionSet HoursFun::GetFunctions() { } ScalarFunctionSet YearWeekFun::GetFunctions() { - auto set = GetDatePartFunction(); - set.SetUnaryArgProperties(ArgProperties().NonDecreasing()); - return set; + // Not monotone on the full date domain: see YearWeekOperator::YearWeekFromParts. + return GetDatePartFunction(); } ScalarFunctionSet DayOfMonthFun::GetFunctions() { diff --git a/src/duckdb/extension/core_functions/scalar/list/list_reduce.cpp b/src/duckdb/extension/core_functions/scalar/list/list_reduce.cpp index e25fb64d0..0001a9e17 100644 --- a/src/duckdb/extension/core_functions/scalar/list/list_reduce.cpp +++ b/src/duckdb/extension/core_functions/scalar/list/list_reduce.cpp @@ -179,7 +179,7 @@ bool ExecuteReduce(const idx_t loops, ReduceExecuteInfo &execute_info, LambdaFun vector slices; const idx_t initial_offset = info.has_initial ? 1 : 0; for (idx_t i = 0; i < info.column_infos.size() - initial_offset; i++) { - if (info.column_infos[i].vector.get().GetVectorType() == VectorType::CONSTANT_VECTOR) { + if (info.column_infos[initial_offset + i].vector.get().GetVectorType() == VectorType::CONSTANT_VECTOR) { // only reference constant vectors input_chunk.data[accumulator_offset + 1 + i].Reference(info.column_infos[initial_offset + i].vector); } else { diff --git a/src/duckdb/extension/icu/icu-datepart.cpp b/src/duckdb/extension/icu/icu-datepart.cpp index 7c89576a8..47fbefad3 100644 --- a/src/duckdb/extension/icu/icu-datepart.cpp +++ b/src/duckdb/extension/icu/icu-datepart.cpp @@ -702,8 +702,8 @@ void RegisterICUDatePartFunctions(ExtensionLoader &loader) { ICUDatePart::AddUnaryPartCodeFunctions("julian", loader, LogicalType::DOUBLE); // register combinations - ICUDatePart::AddUnaryPartCodeFunctions("yearweek", loader, LogicalType::BIGINT, - ArgProperties().NonDecreasing()); // ISO year and week + // yearweek subtracts the week for ISO year <= 0, so it is not monotone across that range + ICUDatePart::AddUnaryPartCodeFunctions("yearweek", loader, LogicalType::BIGINT); // register various aliases ICUDatePart::AddUnaryPartCodeFunctions("dayofmonth", loader); 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 9aa86ad34..7e2b00a5c 100644 --- a/src/duckdb/src/catalog/catalog_entry/duck_schema_entry.cpp +++ b/src/duckdb/src/catalog/catalog_entry/duck_schema_entry.cpp @@ -361,8 +361,13 @@ void DuckSchemaEntry::Scan(ClientContext &context, CatalogType type, } void DuckSchemaEntry::Scan(CatalogType type, const std::function &callback) { + Scan(type, VisibilityBound::AllCommitted(), callback); +} + +void DuckSchemaEntry::Scan(CatalogType type, VisibilityBound bound, + const std::function &callback) { auto &set = GetCatalogSet(type); - set.Scan(callback); + set.Scan(bound, callback); } void DuckSchemaEntry::DropEntry(ClientContext &context, DropInfo &info) { 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 78f304fcc..e3bd405bb 100644 --- a/src/duckdb/src/catalog/catalog_entry/duck_table_entry.cpp +++ b/src/duckdb/src/catalog/catalog_entry/duck_table_entry.cpp @@ -186,7 +186,7 @@ DuckTableEntry::DuckTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, Bou auto column_indexes = unique.GetLogicalIndexes(columns); if (info.indexes.empty()) { auto index_info = GetIndexInfo(constraint_type, false, info.base, i); - storage->AddIndex(columns, column_indexes, constraint_type, std::move(index_info)); + storage->AddIndex(columns, column_indexes, constraint_type, std::move(index_info), unique.check_mode); continue; } @@ -197,7 +197,8 @@ DuckTableEntry::DuckTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, Bou index_storage_info.name = name_info.name; } - storage->AddIndex(columns, column_indexes, constraint_type, std::move(index_storage_info)); + storage->AddIndex(columns, column_indexes, constraint_type, std::move(index_storage_info), + unique.check_mode); continue; } @@ -215,7 +216,8 @@ DuckTableEntry::DuckTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, Bou if (info.indexes.empty()) { auto constraint_type = IndexConstraintType::FOREIGN; auto index_info = GetIndexInfo(constraint_type, false, info.base, i); - storage->AddIndex(columns, column_indexes, constraint_type, std::move(index_info)); + storage->AddIndex(columns, column_indexes, constraint_type, std::move(index_info), + ConstraintCheckMode::DEFAULT); continue; } @@ -226,7 +228,8 @@ DuckTableEntry::DuckTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, Bou index_storage_info.name = name_info.name; } - storage->AddIndex(columns, column_indexes, IndexConstraintType::FOREIGN, std::move(index_storage_info)); + storage->AddIndex(columns, column_indexes, IndexConstraintType::FOREIGN, std::move(index_storage_info), + ConstraintCheckMode::DEFAULT); } } } @@ -1560,8 +1563,8 @@ optional_ptr DuckTableEntry::GetTrigger(CatalogTransaction transac return triggers->GetEntry(transaction, name); } -void DuckTableEntry::ScanTriggersNonTransactional(const std::function &callback) { - triggers->Scan(callback); +void DuckTableEntry::ScanTriggers(VisibilityBound bound, const std::function &callback) { + triggers->Scan(bound, callback); } bool DuckTableEntry::DropTrigger(CatalogTransaction transaction, const Identifier &name, bool cascade) { diff --git a/src/duckdb/src/catalog/catalog_entry/table_catalog_entry.cpp b/src/duckdb/src/catalog/catalog_entry/table_catalog_entry.cpp index 0cc8ca917..ee58c9f46 100644 --- a/src/duckdb/src/catalog/catalog_entry/table_catalog_entry.cpp +++ b/src/duckdb/src/catalog/catalog_entry/table_catalog_entry.cpp @@ -119,8 +119,8 @@ string TableCatalogEntry::ColumnsToSQL(const ColumnList &columns, const vector> unique_columns; - logical_index_map_t> pk_columns; + logical_index_map_t> unique_columns; + logical_index_map_t> pk_columns; identifier_set_t multi_key_pks; vector extra_constraints; for (auto &constraint : constraints) { @@ -132,9 +132,9 @@ string TableCatalogEntry::ColumnsToSQL(const ColumnList &columns, const vector entry(current); - while (entry.get().HasChild()) { - if (IsCommitted(entry.get().timestamp.load())) { - // this entry is committed: use it - break; - } - entry = entry.get().Child(); - } - return entry.get(); -} - SimilarCatalogEntry CatalogSet::SimilarEntry(CatalogTransaction transaction, const Identifier &name) { unique_lock lock(catalog_lock); CreateDefaultEntries(transaction, lock); @@ -787,14 +775,14 @@ void CatalogSet::ScanWithPrefix(CatalogTransaction transaction, const std::funct } } -void CatalogSet::Scan(const std::function &callback) { +void CatalogSet::Scan(VisibilityBound bound, const std::function &callback) { // lock the catalog set lock_guard lock(catalog_lock); + CatalogTransaction transaction(catalog.GetDatabase(), MAX_TRANSACTION_ID, bound); for (auto &kv : map.Entries()) { - auto &entry = *kv.second; - auto &committed_entry = GetCommittedEntry(entry); - if (!committed_entry.deleted) { - callback(committed_entry); + auto &entry = GetEntryForTransaction(transaction, *kv.second); + if (!entry.deleted) { + callback(entry); } } } @@ -807,7 +795,7 @@ void CatalogSet::SetDefaultGenerator(unique_ptr defaults_p) { void CatalogSet::Verify(Catalog &catalog_p) { D_ASSERT(&catalog_p == &catalog); vector> entries; - Scan([&](CatalogEntry &entry) { entries.push_back(entry); }); + Scan(VisibilityBound::AllCommitted(), [&](CatalogEntry &entry) { entries.push_back(entry); }); for (auto &entry : entries) { entry.get().Verify(catalog_p); } diff --git a/src/duckdb/src/catalog/default/default_views.cpp b/src/duckdb/src/catalog/default/default_views.cpp index 84fb055a0..252813621 100644 --- a/src/duckdb/src/catalog/default/default_views.cpp +++ b/src/duckdb/src/catalog/default/default_views.cpp @@ -75,7 +75,9 @@ static const DefaultView internal_views[] = { {"pg_catalog", "pg_constraint", "SELECT table_oid*1000000+constraint_index oid, constraint_text conname, schema_oid connamespace, CASE " "constraint_type WHEN 'CHECK' then 'c' WHEN 'UNIQUE' then 'u' WHEN 'PRIMARY KEY' THEN 'p' WHEN 'FOREIGN KEY' THEN " - "'f' ELSE 'x' END contype, false condeferrable, false condeferred, true convalidated, table_oid conrelid, 0 " + "'f' ELSE 'x' END contype, constraint_check_mode = 'DEFERRED' condeferrable, constraint_check_mode = 'DEFERRED' " + "condeferred, true convalidated, table_oid " + "conrelid, 0 " "contypid, 0 conindid, 0 conparentid, 0 confrelid, NULL confupdtype, NULL confdeltype, NULL confmatchtype, true " "conislocal, 0 coninhcount, false connoinherit, constraint_column_indexes conkey, NULL confkey, NULL conpfeqop, " "NULL conppeqop, NULL conffeqop, NULL conexclop, expression conbin FROM duckdb_constraints()"}, @@ -197,7 +199,9 @@ static const DefaultView internal_views[] = { {"information_schema", "table_constraints", "SELECT database_name constraint_catalog, schema_name constraint_schema, constraint_name, database_name " "table_catalog, schema_name table_schema, table_name, CASE constraint_type WHEN 'NOT NULL' THEN 'CHECK' ELSE " - "constraint_type END constraint_type, 'NO' is_deferrable, 'NO' initially_deferred, 'YES' enforced, 'YES' " + "constraint_type END constraint_type, CASE WHEN constraint_check_mode = 'DEFERRED' THEN 'YES' ELSE 'NO' END " + "is_deferrable, CASE WHEN " + "constraint_check_mode = 'DEFERRED' THEN 'YES' ELSE 'NO' END initially_deferred, 'YES' enforced, 'YES' " "nulls_distinct FROM duckdb_constraints() WHERE constraint_type = 'PRIMARY KEY' OR constraint_type = 'FOREIGN " "KEY' OR constraint_type = 'UNIQUE' OR constraint_type = 'CHECK' OR constraint_type = 'NOT NULL';"}, {"information_schema", "constraint_column_usage", diff --git a/src/duckdb/src/catalog/dependency_manager.cpp b/src/duckdb/src/catalog/dependency_manager.cpp index 73086195e..f7eccea6b 100644 --- a/src/duckdb/src/catalog/dependency_manager.cpp +++ b/src/duckdb/src/catalog/dependency_manager.cpp @@ -675,9 +675,9 @@ void DependencyManager::ReorderEntries(catalog_entry_vector_t &entries, ClientCo ReorderEntries(entries, transaction, false); } -void DependencyManager::ReorderEntries(catalog_entry_vector_t &entries) { - // Read all committed entries. A checkpoint writes internal entries too - CatalogTransaction transaction(catalog.GetDatabase(), MAX_COMMIT_ID, VisibilityBound::Before(MAX_COMMIT_ID)); +void DependencyManager::ReorderEntries(catalog_entry_vector_t &entries, VisibilityBound bound) { + // Read the entries committed below the checkpoint's bound. A checkpoint writes internal entries too + CatalogTransaction transaction(catalog.GetDatabase(), MAX_TRANSACTION_ID, bound); ReorderEntries(entries, transaction, true); } diff --git a/src/duckdb/src/catalog/duck_catalog.cpp b/src/duckdb/src/catalog/duck_catalog.cpp index 77a6365af..05698d1cf 100644 --- a/src/duckdb/src/catalog/duck_catalog.cpp +++ b/src/duckdb/src/catalog/duck_catalog.cpp @@ -169,11 +169,12 @@ static void ScanNestedSchemas(CatalogTransaction transaction, SchemaCatalogEntry }); } -static void ScanNestedSchemas(SchemaCatalogEntry &schema, const std::function &callback) { - schema.Scan(CatalogType::SCHEMA_ENTRY, [&](CatalogEntry &entry) { - auto &nested = entry.Cast(); +static void ScanNestedSchemas(VisibilityBound bound, DuckSchemaEntry &schema, + const std::function &callback) { + schema.Scan(CatalogType::SCHEMA_ENTRY, bound, [&](CatalogEntry &entry) { + auto &nested = entry.Cast(); callback(nested); - ScanNestedSchemas(nested, callback); + ScanNestedSchemas(bound, nested, callback); }); } @@ -188,11 +189,11 @@ void DuckCatalog::ScanSchemas(ClientContext &context, std::function callback) { - schemas->Scan([&](CatalogEntry &entry) { - auto &schema = entry.Cast(); +void DuckCatalog::ScanSchemas(VisibilityBound bound, std::function callback) { + schemas->Scan(bound, [&](CatalogEntry &entry) { + auto &schema = entry.Cast(); callback(schema); - ScanNestedSchemas(schema, callback); + ScanNestedSchemas(bound, schema, callback); }); } diff --git a/src/duckdb/src/common/enum_util.cpp b/src/duckdb/src/common/enum_util.cpp index 254b89f0f..4ac68bf19 100644 --- a/src/duckdb/src/common/enum_util.cpp +++ b/src/duckdb/src/common/enum_util.cpp @@ -26,6 +26,7 @@ #include "duckdb/common/enums/checkpoint_abort.hpp" #include "duckdb/common/enums/checkpoint_on_detach.hpp" #include "duckdb/common/enums/compression_type.hpp" +#include "duckdb/common/enums/constraint_check_mode.hpp" #include "duckdb/common/enums/copy_overwrite_mode.hpp" #include "duckdb/common/enums/cte_materialize.hpp" #include "duckdb/common/enums/date_part_specifier.hpp" @@ -186,7 +187,6 @@ #include "duckdb/parallel/scan_read_ahead.hpp" #include "duckdb/parallel/task.hpp" #include "duckdb/parser/constraint.hpp" -#include "duckdb/parser/constraints/unique_constraint.hpp" #include "duckdb/parser/expression/lambda_expression.hpp" #include "duckdb/parser/expression/parameter_expression.hpp" #include "duckdb/parser/expression/star_expression.hpp" @@ -230,6 +230,7 @@ #include "duckdb/storage/caching_mode.hpp" #include "duckdb/storage/compression/bitpacking.hpp" #include "duckdb/storage/external_file_cache/external_file_cache_block_state.hpp" +#include "duckdb/storage/index.hpp" #include "duckdb/storage/magic_bytes.hpp" #include "duckdb/storage/statistics/base_statistics.hpp" #include "duckdb/storage/statistics/variant_stats.hpp" @@ -320,6 +321,24 @@ ARTSearchResult EnumUtil::FromString(const char *value) { return static_cast(StringUtil::StringToEnum(GetARTSearchResultValues(), 2, "ARTSearchResult", value)); } +const StringUtil::EnumStringLiteral *GetARTSerializationFormatValues() { + static constexpr StringUtil::EnumStringLiteral values[] { + { static_cast(ARTSerializationFormat::V1_0_0), "V1_0_0" }, + { static_cast(ARTSerializationFormat::CURRENT), "CURRENT" } + }; + return values; +} + +template<> +const char* EnumUtil::ToChars(ARTSerializationFormat value) { + return StringUtil::EnumToString(GetARTSerializationFormatValues(), 2, "ARTSerializationFormat", static_cast(value)); +} + +template<> +ARTSerializationFormat EnumUtil::FromString(const char *value) { + return static_cast(StringUtil::StringToEnum(GetARTSerializationFormatValues(), 2, "ARTSerializationFormat", value)); +} + const StringUtil::EnumStringLiteral *GetAccessModeValues() { static constexpr StringUtil::EnumStringLiteral values[] { { static_cast(AccessMode::UNDEFINED), "UNDEFINED" }, @@ -1463,23 +1482,23 @@ ConflictManagerMode EnumUtil::FromString(const char *value) return static_cast(StringUtil::StringToEnum(GetConflictManagerModeValues(), 2, "ConflictManagerMode", value)); } -const StringUtil::EnumStringLiteral *GetConstraintTimingValues() { +const StringUtil::EnumStringLiteral *GetConstraintCheckModeValues() { static constexpr StringUtil::EnumStringLiteral values[] { - { static_cast(ConstraintTiming::DEFAULT), "DEFAULT" }, - { static_cast(ConstraintTiming::IMMEDIATE), "IMMEDIATE" }, - { static_cast(ConstraintTiming::DEFERRED), "DEFERRED" } + { static_cast(ConstraintCheckMode::DEFAULT), "DEFAULT" }, + { static_cast(ConstraintCheckMode::IMMEDIATE), "IMMEDIATE" }, + { static_cast(ConstraintCheckMode::DEFERRED), "DEFERRED" } }; return values; } template<> -const char* EnumUtil::ToChars(ConstraintTiming value) { - return StringUtil::EnumToString(GetConstraintTimingValues(), 3, "ConstraintTiming", static_cast(value)); +const char* EnumUtil::ToChars(ConstraintCheckMode value) { + return StringUtil::EnumToString(GetConstraintCheckModeValues(), 3, "ConstraintCheckMode", static_cast(value)); } template<> -ConstraintTiming EnumUtil::FromString(const char *value) { - return static_cast(StringUtil::StringToEnum(GetConstraintTimingValues(), 3, "ConstraintTiming", value)); +ConstraintCheckMode EnumUtil::FromString(const char *value) { + return static_cast(StringUtil::StringToEnum(GetConstraintCheckModeValues(), 3, "ConstraintCheckMode", value)); } const StringUtil::EnumStringLiteral *GetConstraintTypeValues() { @@ -3086,6 +3105,24 @@ IndexBindState EnumUtil::FromString(const char *value) { return static_cast(StringUtil::StringToEnum(GetIndexBindStateValues(), 4, "IndexBindState", value)); } +const StringUtil::EnumStringLiteral *GetIndexCheckpointModeValues() { + static constexpr StringUtil::EnumStringLiteral values[] { + { static_cast(IndexCheckpointMode::IMMEDIATE), "IMMEDIATE" }, + { static_cast(IndexCheckpointMode::DEFERRED), "DEFERRED" } + }; + return values; +} + +template<> +const char* EnumUtil::ToChars(IndexCheckpointMode value) { + return StringUtil::EnumToString(GetIndexCheckpointModeValues(), 2, "IndexCheckpointMode", static_cast(value)); +} + +template<> +IndexCheckpointMode EnumUtil::FromString(const char *value) { + return static_cast(StringUtil::StringToEnum(GetIndexCheckpointModeValues(), 2, "IndexCheckpointMode", value)); +} + const StringUtil::EnumStringLiteral *GetIndexConstraintTypeValues() { static constexpr StringUtil::EnumStringLiteral values[] { { static_cast(IndexConstraintType::NONE), "NONE" }, @@ -4268,19 +4305,20 @@ const StringUtil::EnumStringLiteral *GetOptimizerTypeValues() { { static_cast(OptimizerType::SCALAR_FN_PUSHDOWN), "SCALAR_FN_PUSHDOWN" }, { static_cast(OptimizerType::DISTINCT_AGGREGATE_REWRITE), "DISTINCT_AGGREGATE_REWRITE" }, { static_cast(OptimizerType::AGGREGATE_REUSE), "AGGREGATE_REUSE" }, - { static_cast(OptimizerType::PROJECTION_PLACEMENT), "PROJECTION_PLACEMENT" } + { static_cast(OptimizerType::PROJECTION_PLACEMENT), "PROJECTION_PLACEMENT" }, + { static_cast(OptimizerType::REDUNDANT_ORDER_KEYS), "REDUNDANT_ORDER_KEYS" } }; return values; } template<> const char* EnumUtil::ToChars(OptimizerType value) { - return StringUtil::EnumToString(GetOptimizerTypeValues(), 46, "OptimizerType", static_cast(value)); + return StringUtil::EnumToString(GetOptimizerTypeValues(), 47, "OptimizerType", static_cast(value)); } template<> OptimizerType EnumUtil::FromString(const char *value) { - return static_cast(StringUtil::StringToEnum(GetOptimizerTypeValues(), 46, "OptimizerType", value)); + return static_cast(StringUtil::StringToEnum(GetOptimizerTypeValues(), 47, "OptimizerType", value)); } const StringUtil::EnumStringLiteral *GetOrderByColumnTypeValues() { diff --git a/src/duckdb/src/common/enums/optimizer_type.cpp b/src/duckdb/src/common/enums/optimizer_type.cpp index de7bb48e4..a11328ff2 100644 --- a/src/duckdb/src/common/enums/optimizer_type.cpp +++ b/src/duckdb/src/common/enums/optimizer_type.cpp @@ -58,6 +58,7 @@ static const DefaultOptimizerType internal_optimizer_types[] = { {"distinct_aggregate_rewrite", OptimizerType::DISTINCT_AGGREGATE_REWRITE}, {"aggregate_reuse", OptimizerType::AGGREGATE_REUSE}, {"projection_placement", OptimizerType::PROJECTION_PLACEMENT}, + {"redundant_order_keys", OptimizerType::REDUNDANT_ORDER_KEYS}, {nullptr, OptimizerType::INVALID}}; string OptimizerTypeToString(OptimizerType type) { diff --git a/src/duckdb/src/execution/index/art/art.cpp b/src/duckdb/src/execution/index/art/art.cpp index 0448539b7..079351b5f 100644 --- a/src/duckdb/src/execution/index/art/art.cpp +++ b/src/duckdb/src/execution/index/art/art.cpp @@ -1,9 +1,8 @@ #include "duckdb/execution/index/art/art.hpp" +#include "duckdb/common/helper.hpp" #include "duckdb/main/attached_database.hpp" #include "duckdb/common/types/conflict_manager.hpp" -#include "duckdb/common/unordered_map.hpp" -#include "duckdb/common/vector_operations/vector_operations.hpp" #include "duckdb/execution/expression_executor.hpp" #include "duckdb/execution/index/art/art_builder.hpp" #include "duckdb/execution/index/art/art_key.hpp" @@ -24,11 +23,17 @@ #include "duckdb/planner/expression/bound_function_expression.hpp" #include "duckdb/planner/expression/bound_constant_expression.hpp" #include "duckdb/storage/arena_allocator.hpp" +#include "duckdb/storage/index_storage_info.hpp" #include "duckdb/storage/metadata/metadata_reader.hpp" +#include "duckdb/storage/storage_info.hpp" #include "duckdb/execution/index/index_lock.hpp" #include "duckdb/storage/table/scan_state.hpp" #include "duckdb/storage/table_io_manager.hpp" #include "duckdb/storage/storage_manager.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" +#include "duckdb/storage/partial_block_manager.hpp" +#include "duckdb/storage/table/table_index_list.hpp" +#include namespace duckdb { @@ -87,9 +92,7 @@ static idx_t GetNode256LeafSegmentSize(const IndexStorageInfo &info, StorageVers ART::ART(const Identifier &name, const IndexConstraintType index_constraint_type, const vector &column_ids, TableIOManager &table_io_manager, const vector> &unbound_expressions, - AttachedDatabase &db, - const shared_ptr, ALLOCATOR_COUNT>> &allocators_ptr, - const IndexStorageInfo &info) + AttachedDatabase &db, const shared_ptr &allocators_ptr, const IndexStorageInfo &info) : BoundIndex(name, ART::TYPE_NAME, index_constraint_type, column_ids, table_io_manager, unbound_expressions, db), allocators(allocators_ptr), owns_data(false) { // FIXME: Use the new byte representation function to support nested types. @@ -123,7 +126,7 @@ ART::ART(const Identifier &name, const IndexConstraintType index_constraint_type auto &block_manager = table_io_manager.GetIndexBlockManager(); const auto leaf256_size = GetNode256LeafSegmentSize(info, db.GetStorageManager().GetStorageVersion()); - array, ALLOCATOR_COUNT> allocator_array = { + AllocatorArray allocator_array = { make_unsafe_uniq(prefix_size, block_manager), make_unsafe_uniq(sizeof(Leaf), block_manager), make_unsafe_uniq(sizeof(Node4), block_manager), @@ -134,8 +137,7 @@ ART::ART(const Identifier &name, const IndexConstraintType index_constraint_type make_unsafe_uniq(sizeof(Node15Leaf), block_manager), make_unsafe_uniq(leaf256_size, block_manager), }; - allocators = - make_shared_ptr, ALLOCATOR_COUNT>>(std::move(allocator_array)); + allocators = make_shared_ptr(std::move(allocator_array)); } if (!info.IsValid()) { @@ -167,6 +169,22 @@ ART::ART(const Identifier &name, const IndexConstraintType index_constraint_type } } +ART::ART(const ART &src, shared_ptr allocators_ptr) + : BoundIndex(src.name, ART::TYPE_NAME, src.index_constraint_type, src.column_ids, src.table_io_manager, + src.unbound_expressions, src.db), + root_ptr(src.root_ptr), allocators(std::move(allocators_ptr)), owns_data(true), + storage_version(src.storage_version), prefix_count(src.prefix_count) { + D_ASSERT(allocators); +} + +uint8_t ART::GetAllocatorCount(const ARTSerializationFormat format) { + if (format == ARTSerializationFormat::V1_0_0) { + return DEPRECATED_ALLOCATOR_COUNT; + } + + return ALLOCATOR_COUNT; +} + //===--------------------------------------------------------------------===// // Initialize Scans //===--------------------------------------------------------------------===// @@ -904,8 +922,8 @@ ARTSearchResult ART::ScanRange(ARTIndexScanState &scan_state, RowIdVectorOutput auto upper_bound = ARTKey::CreateKey(arena_allocator, scan_state.values[1], storage_version); - bool left_equal = scan_state.expressions[0] == ExpressionType ::COMPARE_GREATERTHANOREQUALTO; - bool right_equal = scan_state.expressions[1] == ExpressionType ::COMPARE_LESSTHANOREQUALTO; + bool left_equal = scan_state.expressions[0] == ExpressionType::COMPARE_GREATERTHANOREQUALTO; + bool right_equal = scan_state.expressions[1] == ExpressionType::COMPARE_LESSTHANOREQUALTO; return SearchCloseRange(key, upper_bound, left_equal, right_equal, row_ids); } @@ -1109,26 +1127,21 @@ void ART::TransformToDeprecated() { } } -IndexStorageInfo ART::PrepareSerialize(const case_insensitive_map_t &options, const bool v1_0_0_storage) { - if (v1_0_0_storage) { +IndexStorageInfo ART::PrepareSerialize(const ARTSerializationFormat target_format) { + if (target_format == ARTSerializationFormat::V1_0_0) { TransformToDeprecated(); } IndexStorageInfo info(name); info.root = root_ptr.Get(); - info.options = options; // It never hurts to serialize the storage version, even to older formats if (storage_version != StorageVersion::INVALID) { info.options["storage_version"] = Value::UBIGINT(static_cast(storage_version)); } - for (auto &allocator : *allocators) { - allocator->RemoveEmptyBuffers(); - } - #ifdef DEBUG - if (v1_0_0_storage) { + if (target_format == ARTSerializationFormat::V1_0_0) { D_ASSERT((*allocators)[NodePtr::GetAllocatorIdx(NType::NODE_7_LEAF)]->Empty()); D_ASSERT((*allocators)[NodePtr::GetAllocatorIdx(NType::NODE_15_LEAF)]->Empty()); D_ASSERT((*allocators)[NodePtr::GetAllocatorIdx(NType::NODE_256_LEAF)]->Empty()); @@ -1140,33 +1153,43 @@ IndexStorageInfo ART::PrepareSerialize(const case_insensitive_map_t &opti return info; } -IndexStorageInfo ART::SerializeToDisk(QueryContext context, const case_insensitive_map_t &options) { - IndexLock guard(*this); +ARTSerializationFormat ART::GetSerializationFormat(const StorageVersion storage_version) { + if (StorageManager::IsPriorToVersion(StorageVersion::V1_2_0, storage_version)) { + return ARTSerializationFormat::V1_0_0; + } - // If the storage format uses deprecated leaf storage, - // then we need to transform all nested leaves before serialization. - auto v1_0_0_option = options.find("v1_0_0_storage"); - bool v1_0_0_storage = v1_0_0_option == options.end() || v1_0_0_option->second != Value(false); - auto info = PrepareSerialize(options, v1_0_0_storage); - auto allocator_count = v1_0_0_storage ? DEPRECATED_ALLOCATOR_COUNT : ALLOCATOR_COUNT; + return ARTSerializationFormat::CURRENT; +} - // Store the data on disk as partial blocks and set the block ids. - WritePartialBlocks(context, v1_0_0_storage); +IndexCheckpointResult ART::Checkpoint(PartialBlockManager &partial_block_manager, const StorageVersion version) { + const auto target_format = GetSerializationFormat(version); + // This may mutate the live ART into a deprecated representation, but we accept this to prevent double copying. + auto storage_info = PrepareSerialize(target_format); - for (idx_t i = 0; i < allocator_count; i++) { - info.allocator_infos.push_back((*allocators)[i]->GetInfo()); + const auto new_allocators = make_shared_ptr(); + const auto allocator_count = GetAllocatorCount(target_format); + + // We allocate all allocators, but serialize in accordance with the target format. + for (idx_t i = 0; i < ALLOCATOR_COUNT; i++) { + auto &new_allocator = (*new_allocators)[i]; + + new_allocator = (*allocators)[i]->Persist(partial_block_manager); + + if (i < allocator_count) { + storage_info.allocator_infos.push_back(new_allocator->GetInfo()); + } } - return info; + auto shadow = unique_ptr(new ART(*this, new_allocators)); + return {make_shared_ptr(std::move(storage_info)), std::move(shadow)}; } -IndexStorageInfo ART::SerializeToWAL(const case_insensitive_map_t &options) { +IndexStorageInfo ART::SerializeToWAL(const StorageVersion target_version) { // If the storage format uses deprecated leaf storage, // then we need to transform all nested leaves before serialization. - auto v1_0_0_option = options.find("v1_0_0_storage"); - bool v1_0_0_storage = v1_0_0_option == options.end() || v1_0_0_option->second != Value(false); - auto info = PrepareSerialize(options, v1_0_0_storage); - auto allocator_count = v1_0_0_storage ? DEPRECATED_ALLOCATOR_COUNT : ALLOCATOR_COUNT; + const auto target_format = GetSerializationFormat(target_version); + auto info = PrepareSerialize(target_format); + const auto allocator_count = GetAllocatorCount(target_format); // Set the correct allocation sizes and get the map containing all buffers. for (idx_t i = 0; i < allocator_count; i++) { @@ -1175,22 +1198,12 @@ IndexStorageInfo ART::SerializeToWAL(const case_insensitive_map_t &option for (idx_t i = 0; i < allocator_count; i++) { info.allocator_infos.push_back((*allocators)[i]->GetInfo()); + D_ASSERT(info.buffers[i].size() == info.allocator_infos[i].buffer_ids.size()); } return info; } -void ART::WritePartialBlocks(QueryContext context, const bool v1_0_0_storage) { - auto &block_manager = table_io_manager.GetIndexBlockManager(); - PartialBlockManager partial_block_manager(context, block_manager, PartialBlockType::FULL_CHECKPOINT); - - idx_t allocator_count = v1_0_0_storage ? DEPRECATED_ALLOCATOR_COUNT : ALLOCATOR_COUNT; - for (idx_t i = 0; i < allocator_count; i++) { - (*allocators)[i]->SerializeBuffers(partial_block_manager); - } - partial_block_manager.FlushPartialBlocks(); -} - void ART::InitAllocators(const IndexStorageInfo &info) { for (idx_t i = 0; i < info.allocator_infos.size(); i++) { (*allocators)[i]->Init(info.allocator_infos[i]); diff --git a/src/duckdb/src/execution/index/bound_index.cpp b/src/duckdb/src/execution/index/bound_index.cpp index 257e2ce89..4b22a7e57 100644 --- a/src/duckdb/src/execution/index/bound_index.cpp +++ b/src/duckdb/src/execution/index/bound_index.cpp @@ -177,11 +177,7 @@ ErrorData BoundIndex::MergeCheckpointDelta(IndexDeltaType, BoundIndex &) { throw InternalException("BoundIndex::MergeCheckpointDelta is not supported for this index type"); } -IndexStorageInfo BoundIndex::SerializeToDisk(QueryContext context, const case_insensitive_map_t &options) { - throw NotImplementedException("The implementation of this index disk serialization does not exist."); -} - -IndexStorageInfo BoundIndex::SerializeToWAL(const case_insensitive_map_t &options) { +IndexStorageInfo BoundIndex::SerializeToWAL(const StorageVersion) { throw NotImplementedException("The implementation of this index WAL serialization does not exist."); } diff --git a/src/duckdb/src/execution/index/fixed_size_allocator.cpp b/src/duckdb/src/execution/index/fixed_size_allocator.cpp index 0dfc77d17..d69e5b855 100644 --- a/src/duckdb/src/execution/index/fixed_size_allocator.cpp +++ b/src/duckdb/src/execution/index/fixed_size_allocator.cpp @@ -48,7 +48,7 @@ IndexPointer FixedSizeAllocator::New() { if (!buffer_with_free_space.IsValid()) { // Add a new buffer. auto buffer_id = GetAvailableBufferId(); - buffers[buffer_id] = make_uniq(block_manager, memory_tag); + buffers[buffer_id] = make_shared_ptr(block_manager, memory_tag); buffers_with_free_space.insert(buffer_id); buffer_with_free_space = buffer_id; @@ -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) { @@ -278,40 +266,74 @@ FixedSizeAllocatorInfo FixedSizeAllocator::GetInfo() const { FixedSizeAllocatorInfo info; info.segment_size = segment_size; - for (const auto &buffer : buffers) { - info.buffer_ids.push_back(buffer.first); + for (const auto &[buffer_id, buffer] : buffers) { + if (buffer->IsEmpty()) { + continue; + } + + info.buffer_ids.push_back(buffer_id); // Memory safety check. - if (buffer.first > idx_t(MAX_ROW_ID)) { + if (buffer_id > idx_t(MAX_ROW_ID)) { throw InternalException("Initializing invalid buffer ID in FixedSizeAllocator::GetInfo"); } - info.block_pointers.push_back(buffer.second->block_pointer); - info.segment_counts.push_back(buffer.second->segment_count); - info.allocation_sizes.push_back(buffer.second->allocation_size); - } + info.block_pointers.push_back(buffer->block_pointer); + info.segment_counts.push_back(buffer->segment_count); + info.allocation_sizes.push_back(buffer->allocation_size); - for (auto &buffer_id : buffers_with_free_space) { - info.buffers_with_free_space.push_back(buffer_id); + if (buffers_with_free_space.find(buffer_id) != buffers_with_free_space.end()) { + info.buffers_with_free_space.push_back(buffer_id); + } } return info; } -void FixedSizeAllocator::SerializeBuffers(PartialBlockManager &partial_block_manager) { - for (auto &buffer : buffers) { - buffer.second->Serialize(partial_block_manager, available_segments_per_buffer, segment_size, bitmask_offset); +unsafe_unique_ptr FixedSizeAllocator::Persist(PartialBlockManager &partial_block_manager) { + auto result = make_unsafe_uniq(segment_size, block_manager, memory_tag); + + D_ASSERT(result->available_segments_per_buffer == available_segments_per_buffer); + D_ASSERT(result->bitmask_count == bitmask_count); + D_ASSERT(result->bitmask_offset == bitmask_offset); + + result->total_segment_count = total_segment_count; + + for (auto &[buffer_id, buffer] : buffers) { + if (buffer->IsEmpty()) { + continue; + } + + D_ASSERT(buffer->readers == 0); + if (buffer->RequiresPersist()) { + result->buffers[buffer_id] = + buffer->Persist(partial_block_manager, available_segments_per_buffer, segment_size, bitmask_offset); + } else { + result->buffers[buffer_id] = buffer; + } + + if (buffers_with_free_space.find(buffer_id) != buffers_with_free_space.end()) { + result->buffers_with_free_space.insert(buffer_id); + } } + + result->NextBufferWithFreeSpace(); + + return result; } vector FixedSizeAllocator::InitSerializationToWAL() { vector buffer_infos; - for (auto &buffer : buffers) { - buffer.second->SetAllocationSize(available_segments_per_buffer, segment_size, bitmask_offset); + for (auto &[buffer_id, buffer] : buffers) { + if (buffer->IsEmpty()) { + continue; + } + + buffer->SetAllocationSize(available_segments_per_buffer, segment_size, bitmask_offset); // Get a handle to the buffer's memory (offset 0). - SegmentHandle handle(*buffer.second, 0); - buffer_infos.emplace_back(handle.GetPtr(), buffer.second->allocation_size); + SegmentHandle handle(*buffer, 0); + buffer_infos.emplace_back(handle.GetPtr(), buffer->allocation_size); } return buffer_infos; } @@ -340,11 +362,11 @@ void FixedSizeAllocator::Init(const FixedSizeAllocatorInfo &info) { // create the FixedSizeBuffer if (info.transient_block_handles) { D_ASSERT(info.transient_block_handles->size() == info.buffer_ids.size()); - buffers[buffer_id] = make_uniq(block_manager, segment_count, allocation_size, - std::move((*info.transient_block_handles)[i])); + buffers[buffer_id] = make_shared_ptr(block_manager, segment_count, allocation_size, + std::move((*info.transient_block_handles)[i])); } else { buffers[buffer_id] = - make_uniq(block_manager, segment_count, allocation_size, buffer_block_pointer); + make_shared_ptr(block_manager, segment_count, allocation_size, buffer_block_pointer); } total_segment_count += segment_count; } @@ -369,7 +391,7 @@ void FixedSizeAllocator::Deserialize(MetadataManager &metadata_manager, const Bl auto segment_count = reader.Read(); auto allocation_size = reader.Read(); buffers[buffer_id] = - make_uniq(block_manager, segment_count, allocation_size, buffer_block_pointer); + make_shared_ptr(block_manager, segment_count, allocation_size, buffer_block_pointer); total_segment_count += segment_count; } for (idx_t i = 0; i < buffers_with_free_space_count; i++) { diff --git a/src/duckdb/src/execution/index/fixed_size_buffer.cpp b/src/duckdb/src/execution/index/fixed_size_buffer.cpp index 51f0470cc..eff38246c 100644 --- a/src/duckdb/src/execution/index/fixed_size_buffer.cpp +++ b/src/duckdb/src/execution/index/fixed_size_buffer.cpp @@ -1,5 +1,7 @@ #include "duckdb/execution/index/fixed_size_buffer.hpp" +#include "duckdb/common/assert.hpp" +#include "duckdb/common/shared_ptr_ipp.hpp" #include "duckdb/storage/block_manager.hpp" #include "duckdb/storage/buffer_manager.hpp" @@ -80,70 +82,44 @@ FixedSizeBuffer::~FixedSizeBuffer() { } } -void FixedSizeBuffer::Serialize(PartialBlockManager &partial_block_manager, const idx_t available_segments, - const idx_t segment_size, const idx_t bitmask_offset) { +shared_ptr FixedSizeBuffer::Persist(PartialBlockManager &partial_block_manager, + const idx_t available_segments, const idx_t segment_size, + const idx_t bitmask_offset) { D_ASSERT(readers == 0); + D_ASSERT(RequiresPersist()); - // Early-out, if the block is already on disk and not in memory. + // We cannot persist a buffer which is not in memory. if (!InMemory()) { - if (!OnDisk() || dirty) { - throw InternalException("invalid or missing buffer in FixedSizeAllocator"); - } - return; + throw InternalException("invalid or missing buffer in FixedSizeAllocator"); } - // Early-out, if the buffer is already on disk and not dirty. - if (!dirty && OnDisk()) { - return; - } - - // Adjust the allocation size. - D_ASSERT(segment_count != 0); - SetAllocationSize(available_segments, segment_size, bitmask_offset); - - // The buffer is in memory. - D_ASSERT(InMemory()); - if (OnDisk()) { - // We copied it onto a new buffer when loading from disk. - block_manager.MarkBlockAsModified(block_pointer.block_id); - } - - // Write the changes. - // First, we get a partial block allocation. - PartialBlockAllocation allocation = - partial_block_manager.GetBlockAllocation(NumericCast(allocation_size)); - block_pointer.block_id = allocation.state.block_id; - block_pointer.offset = allocation.state.offset; + const auto new_allocation_size = GetNewAllocationSize(available_segments, segment_size, bitmask_offset); + auto allocation = partial_block_manager.GetBlockAllocation(NumericCast(new_allocation_size)); + BlockPointer new_block_pointer(allocation.state.block_id, allocation.state.offset); + auto result = + make_shared_ptr(block_manager, segment_count, new_allocation_size, new_block_pointer); auto &buffer_manager = block_manager.buffer_manager; if (allocation.partial_block) { // There is space, so we copy to an existing partial block. - D_ASSERT(block_pointer.offset > 0); + D_ASSERT(new_block_pointer.offset > 0); auto &p_block_for_index = allocation.partial_block->Cast(); auto dst_handle = buffer_manager.Pin(p_block_for_index.block_handle); - memcpy(dst_handle.GetDataMutable() + block_pointer.offset, buffer_handle.Ptr(), allocation_size); - + memcpy(dst_handle.GetDataMutable() + new_block_pointer.offset, buffer_handle.Ptr(), new_allocation_size); } else { // No partial block available, so we create a new partial block. - D_ASSERT(block_handle); - D_ASSERT(!block_pointer.offset); - auto p_block_for_index = make_uniq(allocation.state, block_manager, block_handle); + D_ASSERT(!new_block_pointer.offset); + auto new_block_handle = buffer_manager.Allocate(MemoryTag::ART_INDEX, &block_manager, false); + memcpy(new_block_handle.GetDataMutable(), buffer_handle.Ptr(), new_allocation_size); + auto p_block_for_index = + make_uniq(allocation.state, block_manager, new_block_handle.GetBlockHandle()); allocation.partial_block = std::move(p_block_for_index); } - // We are done with this buffer. - // To use the fixed-size buffer again, we need to re-load it from disk. - buffer_handle.Destroy(); - - // Register the partial block and the block handle. partial_block_manager.RegisterPartialBlock(std::move(allocation)); - block_handle = block_manager.RegisterBlock(block_pointer.block_id); - D_ASSERT(block_handle->BlockId() < MAXIMUM_BLOCK); - - // We persisted any changes, so the fixed-size buffer is no longer dirty. - dirty = false; + return result; } void FixedSizeBuffer::LoadFromDisk() { @@ -165,9 +141,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 +154,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,21 +187,42 @@ 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) { return; } + allocation_size = GetNewAllocationSize(available_segments, segment_size, bitmask_offset); +} + +idx_t FixedSizeBuffer::GetNewAllocationSize(const idx_t available_segments, const idx_t segment_size, + const idx_t bitmask_offset) { // We traverse from the back. A binary search would be faster. // However, buffers are often (almost) full, so the overhead is acceptable. @@ -236,7 +238,7 @@ void FixedSizeBuffer::SetAllocationSize(const idx_t available_segments, const id break; } } - allocation_size = max_offset * segment_size + bitmask_offset; + return max_offset * segment_size + bitmask_offset; } SegmentHandle::SegmentHandle(FixedSizeBuffer &buffer_p, const idx_t offset) : buffer_ptr(buffer_p) { diff --git a/src/duckdb/src/execution/index/unbound_index.cpp b/src/duckdb/src/execution/index/unbound_index.cpp index 7e4b76737..f230c2075 100644 --- a/src/duckdb/src/execution/index/unbound_index.cpp +++ b/src/duckdb/src/execution/index/unbound_index.cpp @@ -1,6 +1,5 @@ #include "duckdb/execution/index/unbound_index.hpp" -#include "duckdb/common/algorithm.hpp" #include "duckdb/common/types/column/column_data_collection.hpp" #include "duckdb/execution/index/bound_index.hpp" #include "duckdb/parser/parsed_data/create_index_info.hpp" @@ -8,16 +7,17 @@ #include "duckdb/storage/block_manager.hpp" #include "duckdb/storage/index_storage_info.hpp" #include "duckdb/storage/table_io_manager.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" namespace duckdb { UnboundIndex::UnboundIndex(unique_ptr create_info, IndexStorageInfo storage_info_p, TableIOManager &table_io_manager, AttachedDatabase &db) : Index(create_info->Cast().column_ids, table_io_manager, db), create_info(std::move(create_info)), - storage_info(std::move(storage_info_p)) { + storage_info(make_shared_ptr(std::move(storage_info_p))) { // Memory safety check. - for (idx_t info_idx = 0; info_idx < storage_info.allocator_infos.size(); info_idx++) { - auto &info = storage_info.allocator_infos[info_idx]; + for (idx_t info_idx = 0; info_idx < storage_info->allocator_infos.size(); info_idx++) { + auto &info = storage_info->allocator_infos[info_idx]; for (idx_t buffer_idx = 0; buffer_idx < info.buffer_ids.size(); buffer_idx++) { if (info.buffer_ids[buffer_idx] > idx_t(MAX_ROW_ID)) { throw InternalException("found invalid buffer ID in UnboundIndex constructor"); @@ -45,7 +45,7 @@ void UnboundIndex::ResetStorage() { } storage_reclaimed = true; auto &block_manager = table_io_manager.GetIndexBlockManager(); - for (auto &info : storage_info.allocator_infos) { + for (auto &info : storage_info->allocator_infos) { for (auto &block : info.block_pointers) { if (block.IsValid()) { block_manager.MarkBlockAsModified(block.block_id); @@ -54,14 +54,8 @@ void UnboundIndex::ResetStorage() { } } -IndexStorageInfo UnboundIndex::CopyStorageInfo() const { - IndexStorageInfo result(storage_info.name); - result.root = storage_info.root; - result.options = storage_info.options; - result.allocator_infos = storage_info.allocator_infos; - result.buffers = storage_info.buffers; - result.root_block_ptr = storage_info.root_block_ptr; - return result; +IndexCheckpointResult UnboundIndex::Checkpoint(PartialBlockManager &, StorageVersion) { + return {storage_info, nullptr}; } unique_ptr UnboundIndex::Bind(IndexBinder &binder, const vector &physical_column_types) { diff --git a/src/duckdb/src/execution/operator/order/physical_top_n.cpp b/src/duckdb/src/execution/operator/order/physical_top_n.cpp index 4e59e3fcd..617c49423 100644 --- a/src/duckdb/src/execution/operator/order/physical_top_n.cpp +++ b/src/duckdb/src/execution/operator/order/physical_top_n.cpp @@ -5,6 +5,7 @@ #include "duckdb/common/arena_containers/arena_vector.hpp" #include "duckdb/execution/expression_executor.hpp" #include "duckdb/function/create_sort_key.hpp" +#include "duckdb/storage/buffer_manager.hpp" #include "duckdb/storage/data_table.hpp" #include "duckdb/planner/filter/table_filter_function_helpers.hpp" diff --git a/src/duckdb/src/execution/operator/persistent/physical_insert.cpp b/src/duckdb/src/execution/operator/persistent/physical_insert.cpp index a9d417bb3..7518e87bc 100644 --- a/src/duckdb/src/execution/operator/persistent/physical_insert.cpp +++ b/src/duckdb/src/execution/operator/persistent/physical_insert.cpp @@ -4,6 +4,7 @@ #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" #include "duckdb/common/types/column/column_data_collection.hpp" #include "duckdb/common/types/conflict_manager.hpp" +#include "duckdb/common/types/constraint_conflict_info.hpp" #include "duckdb/common/vector_operations/vector_operations.hpp" #include "duckdb/execution/expression_executor.hpp" #include "duckdb/execution/operator/persistent/collection_merger.hpp" diff --git a/src/duckdb/src/execution/operator/scan/physical_table_scan.cpp b/src/duckdb/src/execution/operator/scan/physical_table_scan.cpp index 94cfebe0a..fd2c1f669 100644 --- a/src/duckdb/src/execution/operator/scan/physical_table_scan.cpp +++ b/src/duckdb/src/execution/operator/scan/physical_table_scan.cpp @@ -10,6 +10,7 @@ #include "duckdb/main/database.hpp" #include "duckdb/execution/physical_table_scan_enum.hpp" #include "duckdb/main/settings.hpp" +#include "duckdb/parallel/pipeline.hpp" #include 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 7ee10a131..ad27a4bfa 100644 --- a/src/duckdb/src/execution/operator/schema/physical_create_index.cpp +++ b/src/duckdb/src/execution/operator/schema/physical_create_index.cpp @@ -9,6 +9,8 @@ #include "duckdb/execution/index/bound_index.hpp" #include "duckdb/main/client_context.hpp" #include "duckdb/main/database_manager.hpp" +#include "duckdb/parser/constraints/unique_constraint.hpp" +#include "duckdb/parser/parsed_data/alter_table_info.hpp" #include "duckdb/planner/constraints/bound_not_null_constraint.hpp" #include "duckdb/storage/table/append_state.hpp" #include "duckdb/storage/table/data_table_info.hpp" @@ -145,6 +147,11 @@ SinkFinalizeType PhysicalCreateIndex::Finalize(Pipeline &pipeline, Event &event, auto &schema = table.schema; info->column_ids = storage_ids; + // Read local storage before ALTER moves it; CREATE UNIQUE INDEX also needs it for the delete index. + auto &local_storage = LocalStorage::Get(context, storage.db); + auto local_table_storage = local_storage.GetStorage(storage); + + auto check_mode = ConstraintCheckMode::DEFAULT; if (!alter_table_info) { // Ensure that the index does not yet exist in the catalog. auto entry = @@ -172,19 +179,24 @@ SinkFinalizeType PhysicalCreateIndex::Finalize(Pipeline &pipeline, Event &event, // PRIMARY KEY columns cannot be NULL. if (info->constraint_type == IndexConstraintType::PRIMARY) { - auto &local_storage = LocalStorage::Get(context, storage.db); for (const auto &column_id : storage_ids) { BoundNotNullConstraint not_null {PhysicalIndex(column_id)}; local_storage.VerifyNewConstraint(storage, not_null); } } + auto &constraint_info = alter_table_info->Cast(); + check_mode = constraint_info.constraint->Cast().check_mode; auto &catalog = Catalog::GetCatalog(context, info->GetQualifiedName().Catalog()); catalog.Alter(context, *alter_table_info); } // Add the index to the storage. - storage.AddIndex(std::move(bound_index)); + auto index_entry = storage.GetDataTableInfo()->GetIndexes().AddIndex(std::move(bound_index), check_mode); + if (local_table_storage) { + // Existing transaction-local rows are verified when committing, so we only add a delete index. + index_entry->InitializeLocalDeleteIndex(local_table_storage->delete_indexes); + } return SinkFinalizeType::READY; } diff --git a/src/duckdb/src/execution/radix_partitioned_hashtable.cpp b/src/duckdb/src/execution/radix_partitioned_hashtable.cpp index b323e5235..6e24d1321 100644 --- a/src/duckdb/src/execution/radix_partitioned_hashtable.cpp +++ b/src/duckdb/src/execution/radix_partitioned_hashtable.cpp @@ -12,6 +12,7 @@ #include "duckdb/execution/ht_entry.hpp" #include "duckdb/execution/operator/aggregate/physical_hash_aggregate.hpp" #include "duckdb/planner/expression/bound_reference_expression.hpp" +#include "duckdb/storage/buffer_manager.hpp" #include "duckdb/storage/temporary_memory_manager.hpp" namespace duckdb { diff --git a/src/duckdb/src/function/scalar/string/like.cpp b/src/duckdb/src/function/scalar/string/like.cpp index a9cd8250c..8dc2c6f3c 100644 --- a/src/duckdb/src/function/scalar/string/like.cpp +++ b/src/duckdb/src/function/scalar/string/like.cpp @@ -40,11 +40,8 @@ static bool GlobMatchesBracket(char s, const char *pattern, idx_t plen, idx_t &p break; } bool matches; - if (pattern[pidx + 1] == '-') { + if (pattern[pidx + 1] == '-' && pidx + 2 < plen && pattern[pidx + 2] != ']') { // range! find the next character in the range - if (pidx + 2 == plen) { - break; - } auto next_char = pattern[pidx + 2]; // check if the current character is within the range matches = s >= p && s <= next_char; diff --git a/src/duckdb/src/function/table/read_duckdb.cpp b/src/duckdb/src/function/table/read_duckdb.cpp index c9988f0db..86be08fd5 100644 --- a/src/duckdb/src/function/table/read_duckdb.cpp +++ b/src/duckdb/src/function/table/read_duckdb.cpp @@ -210,7 +210,7 @@ DuckDBReader::DuckDBReader(ClientContext &context_p, OpenFileInfo file_p, const vector> tables; vector> candidate_tables; catalog.ScanSchemas(context, [&](SchemaCatalogEntry &schema) { - schema.Scan(CatalogType::TABLE_ENTRY, [&](CatalogEntry &entry) { + schema.Scan(context, CatalogType::TABLE_ENTRY, [&](CatalogEntry &entry) { if (entry.type != CatalogType::TABLE_ENTRY) { return; } diff --git a/src/duckdb/src/function/table/system/duckdb_constraints.cpp b/src/duckdb/src/function/table/system/duckdb_constraints.cpp index 182d74af0..41ed56b85 100644 --- a/src/duckdb/src/function/table/system/duckdb_constraints.cpp +++ b/src/duckdb/src/function/table/system/duckdb_constraints.cpp @@ -2,6 +2,7 @@ #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" #include "duckdb/catalog/catalog_entry/schema_catalog_entry.hpp" #include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" +#include "duckdb/common/enum_util.hpp" #include "duckdb/common/exception.hpp" #include "duckdb/function/table/system_functions.hpp" #include "duckdb/main/client_context.hpp" @@ -90,6 +91,9 @@ static unique_ptr DuckDBConstraintsBind(ClientContext &context, Ta names.emplace_back("referenced_column_names"); return_types.push_back(LogicalType::LIST(LogicalType::VARCHAR)); + names.emplace_back("constraint_check_mode"); + return_types.emplace_back(LogicalType::VARCHAR); + return nullptr; } @@ -245,6 +249,8 @@ void DuckDBConstraintsFunction(ClientContext &context, TableFunctionInput &data_ auto &referenced_table = output.data[13]; // referenced_column_names, LIST(VARCHAR) auto &referenced_column_names = output.data[14]; + // constraint_check_mode, VARCHAR + auto &constraint_check_mode = output.data[15]; while (data.offset < data.entries.size() && count < STANDARD_VECTOR_SIZE) { auto &entry = data.entries[data.offset]; @@ -326,6 +332,10 @@ void DuckDBConstraintsFunction(ClientContext &context, TableFunctionInput &data_ constraint_name_vec.Append(Value(std::move(constraint_name))); referenced_table.Append(info.referenced_table.empty() ? Value() : Value(info.referenced_table)); referenced_column_names.Append(Value::LIST(LogicalType::VARCHAR, std::move(referenced_column_name_list))); + auto check_mode = constraint->type == ConstraintType::UNIQUE + ? constraint->Cast().check_mode + : ConstraintCheckMode::DEFAULT; + constraint_check_mode.Append(Value(EnumUtil::ToString(check_mode))); count++; } diff --git a/src/duckdb/src/function/table/table_scan.cpp b/src/duckdb/src/function/table/table_scan.cpp index a3aeab7c7..3fec18b03 100644 --- a/src/duckdb/src/function/table/table_scan.cpp +++ b/src/duckdb/src/function/table/table_scan.cpp @@ -18,6 +18,7 @@ #include "duckdb/function/table_function.hpp" #include "duckdb/main/attached_database.hpp" #include "duckdb/main/client_config.hpp" +#include "duckdb/main/client_context.hpp" #include "duckdb/main/client_data.hpp" #include "duckdb/main/settings.hpp" #include "duckdb/main/database.hpp" diff --git a/src/duckdb/src/function/table/version/pragma_version.cpp b/src/duckdb/src/function/table/version/pragma_version.cpp index 506e2df72..c039a8b94 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-alpha43482" +#define DUCKDB_PATCH_VERSION "0-alpha48042" #endif #ifndef DUCKDB_MINOR_VERSION #define DUCKDB_MINOR_VERSION 1 @@ -8,10 +8,10 @@ #define DUCKDB_MAJOR_VERSION 2 #endif #ifndef DUCKDB_VERSION -#define DUCKDB_VERSION "v2.1.0-alpha43482" +#define DUCKDB_VERSION "v2.1.0-alpha48042" #endif #ifndef DUCKDB_SOURCE_ID -#define DUCKDB_SOURCE_ID "cf0b14db3e" +#define DUCKDB_SOURCE_ID "2bb5316a8b" #endif #include "duckdb/function/table/system_functions.hpp" #include "duckdb/main/database.hpp" diff --git a/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_schema_entry.hpp b/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_schema_entry.hpp index 0eccfbaf4..9ee6a5349 100644 --- a/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_schema_entry.hpp +++ b/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_schema_entry.hpp @@ -71,6 +71,7 @@ class DuckSchemaEntry : public SchemaCatalogEntry { void Alter(CatalogTransaction transaction, AlterInfo &info) override; void Scan(ClientContext &context, CatalogType type, const std::function &callback) override; void Scan(CatalogType type, const std::function &callback) override; + void Scan(CatalogType type, VisibilityBound bound, const std::function &callback); void DropEntry(ClientContext &context, DropInfo &info) override; optional_ptr LookupEntry(CatalogTransaction transaction, const EntryLookupInfo &lookup_info) override; CatalogSet::EntryLookup LookupEntryDetailed(CatalogTransaction transaction, diff --git a/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp b/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp index 4dfb394e8..a4181124e 100644 --- a/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp +++ b/src/duckdb/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp @@ -74,8 +74,8 @@ class DuckTableEntry : public TableCatalogEntry { optional_ptr GetTrigger(CatalogTransaction transaction, const Identifier &name) const override; void ScanTriggers(CatalogTransaction transaction, const std::function &callback) const override; - //! Scan all triggers without a transaction (used by checkpoint writer) - void ScanTriggersNonTransactional(const std::function &callback); + //! Scan the triggers visible at the bound (used by checkpoint writer) + void ScanTriggers(VisibilityBound bound, const std::function &callback); //! Drop a trigger by name bool DropTrigger(CatalogTransaction transaction, const Identifier &name, bool cascade) override; diff --git a/src/duckdb/src/include/duckdb/catalog/catalog_set.hpp b/src/duckdb/src/include/duckdb/catalog/catalog_set.hpp index b12422b2a..fabbabee0 100644 --- a/src/duckdb/src/include/duckdb/catalog/catalog_set.hpp +++ b/src/duckdb/src/include/duckdb/catalog/catalog_set.hpp @@ -100,8 +100,8 @@ class CatalogSet { //! entry void Undo(CatalogEntry &entry); - //! Scan the catalog set, invoking the callback method for every committed entry - DUCKDB_API void Scan(const std::function &callback); + //! Scan the catalog set, invoking the callback method for every entry committed below the bound + DUCKDB_API void Scan(VisibilityBound bound, const std::function &callback); //! Scan the catalog set, invoking the callback method for every entry DUCKDB_API void ScanWithPrefix(CatalogTransaction transaction, const std::function &callback, const Identifier &prefix); @@ -149,7 +149,6 @@ class CatalogSet { CatalogEntry &GetEntryForTransaction(CatalogTransaction transaction, CatalogEntry ¤t, bool &visible); //! Given a root entry, gets the entry valid for this transaction CatalogEntry &GetEntryForTransaction(CatalogTransaction transaction, CatalogEntry ¤t); - CatalogEntry &GetCommittedEntry(CatalogEntry ¤t); optional_ptr GetEntryInternal(CatalogTransaction transaction, const Identifier &name); optional_ptr CreateCommittedEntry(unique_ptr entry); diff --git a/src/duckdb/src/include/duckdb/catalog/dependency_manager.hpp b/src/duckdb/src/include/duckdb/catalog/dependency_manager.hpp index 553f74696..e6b35ae32 100644 --- a/src/duckdb/src/include/duckdb/catalog/dependency_manager.hpp +++ b/src/duckdb/src/include/duckdb/catalog/dependency_manager.hpp @@ -94,7 +94,7 @@ class DependencyManager { void AddOwnership(CatalogTransaction transaction, CatalogEntry &owner, CatalogEntry &entry); //! Get the order of entries needed by EXPORT, the objects with no dependencies are exported first - void ReorderEntries(catalog_entry_vector_t &entries); + void ReorderEntries(catalog_entry_vector_t &entries, VisibilityBound bound); void ReorderEntries(catalog_entry_vector_t &entries, ClientContext &context); private: diff --git a/src/duckdb/src/include/duckdb/catalog/duck_catalog.hpp b/src/duckdb/src/include/duckdb/catalog/duck_catalog.hpp index 7b69e1ca0..461244a4e 100644 --- a/src/duckdb/src/include/duckdb/catalog/duck_catalog.hpp +++ b/src/duckdb/src/include/duckdb/catalog/duck_catalog.hpp @@ -11,6 +11,7 @@ #include "duckdb/catalog/catalog.hpp" namespace duckdb { +class DuckSchemaEntry; //! The Catalog object represents the catalog of the database. class DuckCatalog : public Catalog { @@ -39,7 +40,7 @@ class DuckCatalog : public Catalog { public: DUCKDB_API optional_ptr CreateSchema(CatalogTransaction transaction, CreateSchemaInfo &info) override; DUCKDB_API void ScanSchemas(ClientContext &context, std::function callback) override; - DUCKDB_API void ScanSchemas(std::function callback); + DUCKDB_API void ScanSchemas(VisibilityBound bound, std::function callback); DUCKDB_API optional_ptr LookupSchema(CatalogTransaction transaction, const EntryLookupInfo &schema_lookup, diff --git a/src/duckdb/src/include/duckdb/common/enum_util.hpp b/src/duckdb/src/include/duckdb/common/enum_util.hpp index 3d26e4b65..16b224a20 100644 --- a/src/duckdb/src/include/duckdb/common/enum_util.hpp +++ b/src/duckdb/src/include/duckdb/common/enum_util.hpp @@ -40,6 +40,8 @@ enum class ARTScanProgress : uint8_t; enum class ARTSearchResult : uint8_t; +enum class ARTSerializationFormat : uint8_t; + enum class AccessMode : uint8_t; enum class AdaptiveFilterSource : uint8_t; @@ -152,7 +154,7 @@ enum class CompressionValidity : uint8_t; enum class ConflictManagerMode : uint8_t; -enum class ConstraintTiming : uint8_t; +enum class ConstraintCheckMode : uint8_t; enum class ConstraintType : uint8_t; @@ -290,6 +292,8 @@ enum class IndexAppendMode : uint8_t; enum class IndexBindState : uint8_t; +enum class IndexCheckpointMode : uint8_t; + enum class IndexConstraintType : uint8_t; enum class IndexDeltaType : uint8_t; @@ -675,6 +679,9 @@ const char* EnumUtil::ToChars(ARTScanProgress value); template<> const char* EnumUtil::ToChars(ARTSearchResult value); +template<> +const char* EnumUtil::ToChars(ARTSerializationFormat value); + template<> const char* EnumUtil::ToChars(AccessMode value); @@ -844,7 +851,7 @@ template<> const char* EnumUtil::ToChars(ConflictManagerMode value); template<> -const char* EnumUtil::ToChars(ConstraintTiming value); +const char* EnumUtil::ToChars(ConstraintCheckMode value); template<> const char* EnumUtil::ToChars(ConstraintType value); @@ -1050,6 +1057,9 @@ const char* EnumUtil::ToChars(IndexAppendMode value); template<> const char* EnumUtil::ToChars(IndexBindState value); +template<> +const char* EnumUtil::ToChars(IndexCheckpointMode value); + template<> const char* EnumUtil::ToChars(IndexConstraintType value); @@ -1621,6 +1631,9 @@ ARTScanProgress EnumUtil::FromString(const char *value); template<> ARTSearchResult EnumUtil::FromString(const char *value); +template<> +ARTSerializationFormat EnumUtil::FromString(const char *value); + template<> AccessMode EnumUtil::FromString(const char *value); @@ -1790,7 +1803,7 @@ template<> ConflictManagerMode EnumUtil::FromString(const char *value); template<> -ConstraintTiming EnumUtil::FromString(const char *value); +ConstraintCheckMode EnumUtil::FromString(const char *value); template<> ConstraintType EnumUtil::FromString(const char *value); @@ -1996,6 +2009,9 @@ IndexAppendMode EnumUtil::FromString(const char *value); template<> IndexBindState EnumUtil::FromString(const char *value); +template<> +IndexCheckpointMode EnumUtil::FromString(const char *value); + template<> IndexConstraintType EnumUtil::FromString(const char *value); diff --git a/src/duckdb/src/include/duckdb/common/enums/constraint_check_mode.hpp b/src/duckdb/src/include/duckdb/common/enums/constraint_check_mode.hpp new file mode 100644 index 000000000..d290e4ce0 --- /dev/null +++ b/src/duckdb/src/include/duckdb/common/enums/constraint_check_mode.hpp @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/common/enums/constraint_check_mode.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/common/constants.hpp" + +namespace duckdb { + +enum class ConstraintCheckMode : uint8_t { DEFAULT, IMMEDIATE, DEFERRED }; + +} // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/common/enums/database_modification_type.hpp b/src/duckdb/src/include/duckdb/common/enums/database_modification_type.hpp index faafd4dea..b9dc97b60 100644 --- a/src/duckdb/src/include/duckdb/common/enums/database_modification_type.hpp +++ b/src/duckdb/src/include/duckdb/common/enums/database_modification_type.hpp @@ -17,8 +17,11 @@ struct DatabaseModificationType { static constexpr idx_t INSERT_DATA = 1ULL << 0ULL; static constexpr idx_t DELETE_DATA = 1ULL << 1ULL; static constexpr idx_t UPDATE_DATA = 1ULL << 2ULL; + //! An ALTER TABLE that may change table storage: not renames, defaults, DROP NOT NULL or comments static constexpr idx_t ALTER_TABLE = 1ULL << 3ULL; + //! Creating a table, index or trigger, or replacing a schema: not entries without table storage static constexpr idx_t CREATE_CATALOG_ENTRY = 1ULL << 4ULL; + //! Dropping a table, index or trigger, or anything with CASCADE: not entries without table storage static constexpr idx_t DROP_CATALOG_ENTRY = 1ULL << 5ULL; static constexpr idx_t SEQUENCE = 1ULL << 6ULL; static constexpr idx_t CREATE_INDEX = 1ULL << 7ULL; diff --git a/src/duckdb/src/include/duckdb/common/enums/optimizer_type.hpp b/src/duckdb/src/include/duckdb/common/enums/optimizer_type.hpp index 6b7a0105c..5556ad1b4 100644 --- a/src/duckdb/src/include/duckdb/common/enums/optimizer_type.hpp +++ b/src/duckdb/src/include/duckdb/common/enums/optimizer_type.hpp @@ -59,7 +59,8 @@ enum class OptimizerType : uint32_t { SCALAR_FN_PUSHDOWN = 42, DISTINCT_AGGREGATE_REWRITE = 43, AGGREGATE_REUSE = 44, - PROJECTION_PLACEMENT = 45 + PROJECTION_PLACEMENT = 45, + REDUNDANT_ORDER_KEYS = 46 }; string OptimizerTypeToString(OptimizerType type); diff --git a/src/duckdb/src/include/duckdb/execution/index/art/art.hpp b/src/duckdb/src/include/duckdb/execution/index/art/art.hpp index 053b52b6b..e554ef556 100644 --- a/src/duckdb/src/include/duckdb/execution/index/art/art.hpp +++ b/src/duckdb/src/include/duckdb/execution/index/art/art.hpp @@ -11,12 +11,12 @@ #include "duckdb/execution/index/bound_index.hpp" #include "duckdb/execution/index/index_type.hpp" #include "duckdb/execution/index/art/node.hpp" -#include "duckdb/common/array.hpp" namespace duckdb { enum class VerifyExistenceType : uint8_t { APPEND = 0, APPEND_FK = 1, DELETE_FK = 2 }; enum class ARTConflictType : uint8_t { NO_CONFLICT = 0, CONSTRAINT = 1 }; +enum class ARTSerializationFormat : uint8_t { V1_0_0 = 0, CURRENT = 1 }; //! Result of collecting row IDs; capacity exhaustion cannot be resumed. enum class ARTSearchResult : uint8_t { COMPLETED, CAPACITY_EXCEEDED }; @@ -50,11 +50,13 @@ class ART : public BoundIndex { //! FixedSizeAllocator count of deprecated ARTs. static constexpr uint8_t DEPRECATED_ALLOCATOR_COUNT = ALLOCATOR_COUNT - 3; +public: + using AllocatorArray = array, ALLOCATOR_COUNT>; + public: ART(const Identifier &name, const IndexConstraintType index_constraint_type, const vector &column_ids, TableIOManager &table_io_manager, const vector> &unbound_expressions, - AttachedDatabase &db, - const shared_ptr, ALLOCATOR_COUNT>> &allocators_ptr = nullptr, + AttachedDatabase &db, const shared_ptr &allocators_ptr = nullptr, const IndexStorageInfo &info = IndexStorageInfo()); //! Create a index instance of this type. @@ -69,7 +71,7 @@ class ART : public BoundIndex { //! Root of the tree. NodePtr root_ptr = NodePtr(); //! Fixed-size allocators holding the ART nodes. - shared_ptr, ALLOCATOR_COUNT>> allocators; + shared_ptr allocators; //! True, if the ART owns its data. bool owns_data; //! Storage version that the ART was created in, used for backwards compatible key generation @@ -141,11 +143,13 @@ class ART : public BoundIndex { //! Vacuums the ART storage. void Vacuum(IndexLock &state) override DUCKDB_REQUIRES(state); - //! Serializes ART memory to disk and returns the ART storage information. - IndexStorageInfo SerializeToDisk(QueryContext context, const case_insensitive_map_t &options) override - DUCKDB_EXCLUDES(lock); //! Serializes ART memory to the WAL and returns the ART storage information. - IndexStorageInfo SerializeToWAL(const case_insensitive_map_t &options) override; + IndexStorageInfo SerializeToWAL(StorageVersion target_version) override; + //! ART checkpoints are deferred so mutations can be routed through checkpoint deltas. + IndexCheckpointMode GetCheckpointMode() const override { + return IndexCheckpointMode::DEFERRED; + } + IndexCheckpointResult Checkpoint(PartialBlockManager &partial_block_manager, const StorageVersion version) override; //! Returns the in-memory usage of the ART. idx_t GetInMemorySize(IndexLock &index_lock) const override DUCKDB_REQUIRES(index_lock); @@ -182,9 +186,14 @@ class ART : public BoundIndex { } private: + //! Constructor for creating a shadow index based on the source index. + ART(const ART &src, shared_ptr allocators_ptr); //! The number of bytes fitting in the prefix. uint8_t prefix_count; + //! Returns how many allocators are used based on the target serialization format. + static uint8_t GetAllocatorCount(ARTSerializationFormat format); + ARTSearchResult ScanInternal(IndexScanState &state, RowIdVectorOutput &row_ids) const; ARTSearchResult ScanRange(ARTIndexScanState &scan_state, RowIdVectorOutput &row_ids) const; ARTSearchResult ScanBatch(DataChunk &input, RowIdVectorOutput &row_ids) const; @@ -211,9 +220,12 @@ class ART : public BoundIndex { void InitAllocators(const IndexStorageInfo &info); void TransformToDeprecated(); - IndexStorageInfo PrepareSerialize(const case_insensitive_map_t &options, const bool v1_0_0_storage); + //! Gathers metadata used at serialization, optionally transforms the in-memory ART to a deprecated representation + //! based on the target serialization format. + IndexStorageInfo PrepareSerialize(ARTSerializationFormat target_format); + //! Get the serialization format of the ART based on the active storage version. + static ARTSerializationFormat GetSerializationFormat(StorageVersion storage_version); void Deserialize(const BlockPointer &pointer); - void WritePartialBlocks(QueryContext context, const bool v1_0_0_storage); void SetPrefixCount(const IndexStorageInfo &info); string ToStringInternal(bool display_ascii); diff --git a/src/duckdb/src/include/duckdb/execution/index/art/art_operator.hpp b/src/duckdb/src/include/duckdb/execution/index/art/art_operator.hpp index 112164245..7fd8f6850 100644 --- a/src/duckdb/src/include/duckdb/execution/index/art/art_operator.hpp +++ b/src/duckdb/src/include/duckdb/execution/index/art/art_operator.hpp @@ -149,15 +149,15 @@ class ARTOperator { status = GateStatus::GATE_SET; continue; } - // A unique ART may temporarily contain a gated two-row leaf during commit for - // DELETE + INSERT of the same key: commit appends the new row first, then - // commit-delete cleanup removes the old row ID. No other main-ART append should - // enter during that window because commit-time main-index appends are serialized - // by the WAL lock or transaction manager commit lock. - // - // Local append and delete indexes should not contain such gates either. - // Note that VerifyLeaf may still legitimately observe the temporary duplicate - // leaf state. + // A key in a unique ART has a single row ID. The exception is a commit that deleted the key's + // committed row and appends the key again: the new row ID sits next to the deleted one (a gate) + // until the commit removes the deleted row ID. Commit-time appends are serialized, so this gate + // was created by the current commit, and this insert appends the same key a second time, e.g., + // for a deferred constraint. That is a constraint violation, provided that the gate holds the row + // this commit deleted. Otherwise, the index is corrupted. + if (GateContainsDeletedRow(arena, art, active_node_ptr, key, delete_index_info)) { + return ARTConflictType::CONSTRAINT; + } throw FatalException("Corrupted unique ART index \"%s\": encountered an existing gated leaf in unique " "index while inserting", art.name); @@ -334,6 +334,26 @@ class ARTOperator { } private: + //! Returns true, if the gate contains the row ID that a delete index stores for the key. + static bool GateContainsDeletedRow(ArenaAllocator &arena, const ART &art, const NodePtr &gate_ptr, + const ARTKey &key, DeleteIndexInfo delete_index_info) { + if (!delete_index_info.delete_indexes) { + return false; + } + for (auto &delete_index : *delete_index_info.delete_indexes) { + auto &delete_art = delete_index.get().Cast(); + auto delete_leaf_ptr = Lookup(delete_art, delete_art.root_ptr, key, 0); + if (!delete_leaf_ptr || delete_leaf_ptr.Get().GetType() != NType::LEAF_INLINED) { + continue; + } + auto deleted_row_id = ARTKey::CreateARTKey(arena, delete_leaf_ptr.Get().GetRowId()); + if (LookupInLeaf(art, gate_ptr, deleted_row_id)) { + return true; + } + } + return false; + } + static ARTConflictType InsertIntoInlined(ArenaAllocator &arena, ART &art, NodePtr &node_ptr, const ARTKey &key, const ARTKey &row_id, const idx_t depth, const GateStatus status, DeleteIndexInfo delete_index_info, const IndexAppendMode append_mode) { diff --git a/src/duckdb/src/include/duckdb/execution/index/bound_index.hpp b/src/duckdb/src/include/duckdb/execution/index/bound_index.hpp index 5b38fbcf1..a10a0d6e8 100644 --- a/src/duckdb/src/include/duckdb/execution/index/bound_index.hpp +++ b/src/duckdb/src/include/duckdb/execution/index/bound_index.hpp @@ -8,18 +8,15 @@ #pragma once +#include "duckdb/common/error_data.hpp" #include "duckdb/common/mutex.hpp" #include "duckdb/execution/index/unbound_index.hpp" #include "duckdb/common/enums/index_constraint_type.hpp" -#include "duckdb/common/types/constraint_conflict_info.hpp" #include "duckdb/common/types/data_chunk.hpp" -#include "duckdb/common/unordered_set.hpp" #include "duckdb/execution/expression_executor.hpp" -#include "duckdb/parser/parsed_expression.hpp" #include "duckdb/planner/expression.hpp" #include "duckdb/storage/index.hpp" -#include "duckdb/storage/table_storage_info.hpp" namespace duckdb { class BoundIndex; @@ -196,12 +193,8 @@ class BoundIndex : public Index { //! Returns true if the index is affected by updates on the specified column IDs, and false otherwise bool IndexIsUpdated(const vector &column_ids) const; - //! Serializes index memory to disk and returns the index storage information. - virtual IndexStorageInfo SerializeToDisk(QueryContext context, const case_insensitive_map_t &options) - DUCKDB_EXCLUDES(lock); - //! Serializes index memory to the WAL and returns the index storage information. - //! The caller must have exclusive access to the index. - virtual IndexStorageInfo SerializeToWAL(const case_insensitive_map_t &options); + //! Serializes index memory to the WAL, the caller must have exclusive access to the index. + virtual IndexStorageInfo SerializeToWAL(StorageVersion storage_version); //! Execute the index expressions on an input chunk void ExecuteExpressions(DataChunk &input, DataChunk &result) const; diff --git a/src/duckdb/src/include/duckdb/execution/index/fixed_size_allocator.hpp b/src/duckdb/src/include/duckdb/execution/index/fixed_size_allocator.hpp index 5cbc92cc7..e6ac2b80a 100644 --- a/src/duckdb/src/include/duckdb/execution/index/fixed_size_allocator.hpp +++ b/src/duckdb/src/include/duckdb/execution/index/fixed_size_allocator.hpp @@ -9,6 +9,7 @@ #pragma once #include "duckdb/common/constants.hpp" +#include "duckdb/common/shared_ptr.hpp" #include "duckdb/common/types/validity_mask.hpp" #include "duckdb/common/unordered_map.hpp" #include "duckdb/common/unordered_set.hpp" @@ -146,8 +147,9 @@ class FixedSizeAllocator { //! Returns all FixedSizeAllocator information for serialization FixedSizeAllocatorInfo GetInfo() const; - //! Serializes all in-memory buffers - void SerializeBuffers(PartialBlockManager &partial_block_manager); + //! Creates a checkpoint allocator, sharing clean buffers and independently persisting dirty ones. + //! The source must remain read-only until the checkpoint allocator is installed or discarded. + unsafe_unique_ptr Persist(PartialBlockManager &partial_block_manager); //! Sets the allocation sizes and returns data to serialize each buffer vector InitSerializationToWAL(); //! Initialize a fixed-size allocator from allocator storage information @@ -184,7 +186,7 @@ class FixedSizeAllocator { idx_t total_segment_count; //! Buffers containing the segments. - unordered_map> buffers; + unordered_map> buffers; //! Buffers with free space. unordered_set buffers_with_free_space; //! Caches the next buffer to be filled up. 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..84f92ff76 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,8 @@ #pragma once +#include "duckdb/common/shared_ptr_ipp.hpp" +#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" @@ -52,6 +54,11 @@ class FixedSizeBuffer { ~FixedSizeBuffer(); +public: + bool IsEmpty() const { + return segment_count == 0; + } + private: //! Returns a pointer to the buffer in memory, and calls Deserialize, if the buffer is not in memory. //! DEPRECATED. Use segment handles. @@ -76,16 +83,25 @@ class FixedSizeBuffer { return block_pointer.IsValid(); } - //! Serializes a buffer, if dirty or not on disk. - void Serialize(PartialBlockManager &partial_block_manager, const idx_t available_segments, const idx_t segment_size, - const idx_t bitmask_offset); + //! Returns true, if the block is modified or does not exist on-disk + bool RequiresPersist() const { + return dirty || !OnDisk(); + } + + //! Constructs a new FixedSizeBuffer based on the live FixedSizeBuffer, which is prepared for + //! serialization without affecting the in-memory buffer. + shared_ptr Persist(PartialBlockManager &partial_block_manager, const idx_t available_segments, + const idx_t segment_size, const idx_t bitmask_offset); //! 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, marks the buffer as modified. + 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); + idx_t GetNewAllocationSize(const idx_t available_segments, const idx_t segment_size, const idx_t bitmask_offset); private: //! Block manager of the database instance 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..c31deec2c 100644 --- a/src/duckdb/src/include/duckdb/execution/index/unbound_index.hpp +++ b/src/duckdb/src/include/duckdb/execution/index/unbound_index.hpp @@ -66,7 +66,8 @@ class UnboundIndex final : public Index { //! The CreateInfo of the index. unique_ptr create_info; //! The serialized storage information of the index. - IndexStorageInfo storage_info; + //! This information is owned solely by the unbound index, except during checkpointing. + shared_ptr storage_info; //! Buffered for index operations during WAL replay. They are replayed upon index binding. BufferedIndexReplays buffered_replays; @@ -103,15 +104,15 @@ class UnboundIndex final : public Index { return create_info->Cast(); } const IndexStorageInfo &GetStorageInfo() const { - return storage_info; + return *storage_info; } - IndexStorageInfo CopyStorageInfo() const; const vector> &GetParsedExpressions() const { return GetCreateInfo().parsed_expressions; } const Identifier &GetTableName() const { return GetCreateInfo().table; } + IndexCheckpointResult Checkpoint(PartialBlockManager &partial_block_manager, StorageVersion version) override; //! Buffers an insert or delete (replay_type) chunk, to be replayed once the index is bound. //! table_chunk uses physical table layout: data[j] holds physical column j. It may be sparse, diff --git a/src/duckdb/src/include/duckdb/execution/operator/schema/physical_create_index.hpp b/src/duckdb/src/include/duckdb/execution/operator/schema/physical_create_index.hpp index 41e651016..c250445d4 100644 --- a/src/duckdb/src/include/duckdb/execution/operator/schema/physical_create_index.hpp +++ b/src/duckdb/src/include/duckdb/execution/operator/schema/physical_create_index.hpp @@ -18,6 +18,7 @@ class TableCatalogEntry; struct AlterTableInfo; class DuckTableEntry; +class LogicalOperator; //! Physical index creation operator. class PhysicalCreateIndex : public PhysicalOperator { diff --git a/src/duckdb/src/include/duckdb/main/settings.hpp b/src/duckdb/src/include/duckdb/main/settings.hpp index 05ec1df43..8bc14e731 100644 --- a/src/duckdb/src/include/duckdb/main/settings.hpp +++ b/src/duckdb/src/include/duckdb/main/settings.hpp @@ -616,6 +616,19 @@ struct DebugCheckpointAbortSetting { static void OnSet(SettingCallbackInfo &info, Value &input); }; +struct DebugCheckpointScanSleepMsSetting { + using RETURN_TYPE = idx_t; + static constexpr const char *Name = "debug_checkpoint_scan_sleep_ms"; + static constexpr const char *Description = + "DEBUG SETTING: time to sleep after a checkpoint has written a table's row groups, before it installs them"; + static constexpr const char *InputType = "UBIGINT"; + static constexpr bool IsDebug = false; + static constexpr bool IsDeprecated = false; + static constexpr const char *DefaultValue = "0"; + static constexpr SettingScopeTarget Scope = SettingScopeTarget::GLOBAL_DEFAULT; + static constexpr idx_t SettingIndex = NEXT_SETTING_INDEX(); +}; + struct DebugCheckpointSleepMsSetting { using RETURN_TYPE = idx_t; static constexpr const char *Name = "debug_checkpoint_sleep_ms"; diff --git a/src/duckdb/src/include/duckdb/optimizer/remove_redundant_order_keys.hpp b/src/duckdb/src/include/duckdb/optimizer/remove_redundant_order_keys.hpp new file mode 100644 index 000000000..2195fd42f --- /dev/null +++ b/src/duckdb/src/include/duckdb/optimizer/remove_redundant_order_keys.hpp @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/optimizer/remove_redundant_order_keys.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/planner/logical_operator.hpp" + +namespace duckdb { + +class RemoveRedundantOrderKeys { +public: + void Optimize(LogicalOperator &op); +}; + +} // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/optimizer/statistics_propagator.hpp b/src/duckdb/src/include/duckdb/optimizer/statistics_propagator.hpp index d8bf6a724..d18660fa0 100644 --- a/src/duckdb/src/include/duckdb/optimizer/statistics_propagator.hpp +++ b/src/duckdb/src/include/duckdb/optimizer/statistics_propagator.hpp @@ -31,7 +31,7 @@ enum class StatisticsPropagationMode : uint8_t { FILTER_SIMPLIFICATION, FULL }; class StatisticsPropagator { public: - StatisticsPropagator(Optimizer &optimizer, LogicalOperator &root, + StatisticsPropagator(Optimizer &optimizer, unique_ptr &root, StatisticsPropagationMode mode = StatisticsPropagationMode::FULL); unique_ptr PropagateStatistics(unique_ptr &node_ptr); @@ -149,8 +149,8 @@ class StatisticsPropagator { Optimizer &optimizer; ClientContext &context; StatisticsPropagationMode mode; - //! The root of the query plan - optional_ptr root; + //! Reference to the owning pointer so root replacements remain visible + unique_ptr &root; //! The map of ColumnBinding -> statistics for the various nodes column_binding_map_t> statistics_map; //! The statistics of a materialized CTE definition, which hold for every reference to it diff --git a/src/duckdb/src/include/duckdb/optimizer/window_self_join.hpp b/src/duckdb/src/include/duckdb/optimizer/window_self_join.hpp index 3ed80d451..808922ef6 100644 --- a/src/duckdb/src/include/duckdb/optimizer/window_self_join.hpp +++ b/src/duckdb/src/include/duckdb/optimizer/window_self_join.hpp @@ -11,6 +11,7 @@ #include "duckdb/optimizer/optimizer.hpp" #include "duckdb/optimizer/column_binding_replacer.hpp" +#include "duckdb/planner/bound_parameter_map.hpp" namespace duckdb { @@ -27,6 +28,8 @@ class WindowSelfJoinOptimizer { unique_ptr OptimizeInternal(unique_ptr op, ColumnBindingReplacer &replacer); Optimizer &optimizer; + //! The parameters of the enclosing PREPARE, which copied parameter expressions must keep pointing to + optional_ptr parameter_data; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/parser/constraints/unique_constraint.hpp b/src/duckdb/src/include/duckdb/parser/constraints/unique_constraint.hpp index a3150c3ae..bea73db7b 100644 --- a/src/duckdb/src/include/duckdb/parser/constraints/unique_constraint.hpp +++ b/src/duckdb/src/include/duckdb/parser/constraints/unique_constraint.hpp @@ -9,25 +9,24 @@ #pragma once #include "duckdb/common/vector.hpp" +#include "duckdb/common/enums/constraint_check_mode.hpp" #include "duckdb/common/enums/index_constraint_type.hpp" #include "duckdb/parser/column_list.hpp" #include "duckdb/parser/constraint.hpp" namespace duckdb { -enum class ConstraintTiming : uint8_t { DEFAULT, IMMEDIATE, DEFERRED }; - class UniqueConstraint : public Constraint { public: static constexpr const ConstraintType TYPE = ConstraintType::UNIQUE; public: DUCKDB_API UniqueConstraint(const LogicalIndex index, const bool is_primary_key, - ConstraintTiming timing = ConstraintTiming::DEFAULT); + ConstraintCheckMode check_mode = ConstraintCheckMode::DEFAULT); DUCKDB_API UniqueConstraint(const LogicalIndex index, Identifier column_name, const bool is_primary_key, - ConstraintTiming timing = ConstraintTiming::DEFAULT); + ConstraintCheckMode check_mode = ConstraintCheckMode::DEFAULT); DUCKDB_API UniqueConstraint(vector columns, const bool is_primary_key, - ConstraintTiming timing = ConstraintTiming::DEFAULT); + ConstraintCheckMode check_mode = ConstraintCheckMode::DEFAULT); public: DUCKDB_API string ToString() const override; @@ -71,8 +70,8 @@ class UniqueConstraint : public Constraint { vector columns; //! Whether this is a PRIMARY KEY constraint, or a UNIQUE constraint. bool is_primary_key; - //! The declared constraint timing, or DEFAULT when no modifier was specified. - ConstraintTiming timing; + //! The declared constraint check mode, or DEFAULT when no modifier was specified. + ConstraintCheckMode check_mode; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/parser/parsed_data/alter_info.hpp b/src/duckdb/src/include/duckdb/parser/parsed_data/alter_info.hpp index 63219e254..f5c32a814 100644 --- a/src/duckdb/src/include/duckdb/parser/parsed_data/alter_info.hpp +++ b/src/duckdb/src/include/duckdb/parser/parsed_data/alter_info.hpp @@ -10,6 +10,7 @@ #include "duckdb/common/identifier.hpp" #include "duckdb/common/enums/catalog_type.hpp" +#include "duckdb/common/enums/database_modification_type.hpp" #include "duckdb/parser/parsed_data/parse_info.hpp" #include "duckdb/parser/qualified_name.hpp" #include "duckdb/common/enums/on_entry_not_found.hpp" @@ -97,6 +98,7 @@ struct AlterInfo : public ParseInfo { AlterEntryData GetAlterEntryData() const; bool IsAddPrimaryKey() const; bool IsAddUniqueConstraint() const; + DatabaseModificationType GetModifications() const; protected: explicit AlterInfo(AlterType type); diff --git a/src/duckdb/src/include/duckdb/parser/parsed_data/create_info.hpp b/src/duckdb/src/include/duckdb/parser/parsed_data/create_info.hpp index 614f210e2..dd3914fdd 100644 --- a/src/duckdb/src/include/duckdb/parser/parsed_data/create_info.hpp +++ b/src/duckdb/src/include/duckdb/parser/parsed_data/create_info.hpp @@ -9,6 +9,7 @@ #pragma once #include "duckdb/common/enums/catalog_type.hpp" +#include "duckdb/common/enums/database_modification_type.hpp" #include "duckdb/common/identifier.hpp" #include "duckdb/parser/parsed_data/parse_info.hpp" #include "duckdb/parser/qualified_name.hpp" @@ -53,6 +54,7 @@ struct CreateInfo : public ParseInfo { InsertionOrderPreservingMap tags; public: + DatabaseModificationType GetModifications() const; const QualifiedName &GetQualifiedName() const { return qualified_name; } diff --git a/src/duckdb/src/include/duckdb/parser/parsed_data/drop_info.hpp b/src/duckdb/src/include/duckdb/parser/parsed_data/drop_info.hpp index 892b27b9f..ca4194bd3 100644 --- a/src/duckdb/src/include/duckdb/parser/parsed_data/drop_info.hpp +++ b/src/duckdb/src/include/duckdb/parser/parsed_data/drop_info.hpp @@ -9,6 +9,7 @@ #pragma once #include "duckdb/common/enums/catalog_type.hpp" +#include "duckdb/common/enums/database_modification_type.hpp" #include "duckdb/common/identifier.hpp" #include "duckdb/parser/parsed_data/parse_info.hpp" #include "duckdb/parser/parsed_data/extra_drop_info.hpp" @@ -63,6 +64,7 @@ struct DropInfo : public ParseInfo { public: virtual unique_ptr Copy() const; + DatabaseModificationType GetModifications() const; string ToString() const; void Serialize(Serializer &serializer) const override; diff --git a/src/duckdb/src/include/duckdb/parser/peg/ast/column_constraint_entry.hpp b/src/duckdb/src/include/duckdb/parser/peg/ast/column_constraint_entry.hpp index a8b9e0c0e..ec10dccc4 100644 --- a/src/duckdb/src/include/duckdb/parser/peg/ast/column_constraint_entry.hpp +++ b/src/duckdb/src/include/duckdb/parser/peg/ast/column_constraint_entry.hpp @@ -11,7 +11,7 @@ namespace duckdb { struct ColumnConstraintTypeInfo { bool is_primary_key = false; ConstraintType type = ConstraintType::INVALID; - ConstraintTiming timing = ConstraintTiming::DEFAULT; + ConstraintCheckMode check_mode = ConstraintCheckMode::DEFAULT; }; struct ColumnConstraintEntry { diff --git a/src/duckdb/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp b/src/duckdb/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp index d7baacc9b..33733c0b1 100644 --- a/src/duckdb/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp +++ b/src/duckdb/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp @@ -96,8 +96,8 @@ DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.ColumnElements", DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.ColumnList", ColumnList); DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.CommonTableExpressionMap", CommonTableExpressionMap); DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.ComparisonExpressionTail", ComparisonExpressionTail); +DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.ConstraintCheckMode", ConstraintCheckMode); DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.ConstraintColumnDefinition", ConstraintColumnDefinition); -DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.ConstraintTiming", ConstraintTiming); DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.CopyDatabaseType", CopyDatabaseType); DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.CreateTableColumnElement", CreateTableColumnElement); DUCKDB_REGISTER_TRANSFORM_RESULT_TYPE("duckdb.transform_result.CreateTableDefinition", CreateTableDefinition); @@ -5363,19 +5363,19 @@ class PEGTransformerFactory { static unique_ptr TransformUniqueConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result); static ColumnConstraintEntry TransformUniqueConstraint(PEGTransformer &transformer, - const optional &constraint_timing); + const optional &constraint_timing); static unique_ptr TransformPrimaryKeyConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result); static ColumnConstraintEntry TransformPrimaryKeyConstraint(PEGTransformer &transformer, - const optional &constraint_timing); + const optional &constraint_timing); static unique_ptr TransformConstraintTimingInternal(PEGTransformer &transformer, ParseResult &parse_result); static unique_ptr TransformImmediateConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result); - static ConstraintTiming TransformImmediateConstraint(PEGTransformer &transformer); + static ConstraintCheckMode TransformImmediateConstraint(PEGTransformer &transformer); static unique_ptr TransformDeferredConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result); - static ConstraintTiming TransformDeferredConstraint(PEGTransformer &transformer); + static ConstraintCheckMode TransformDeferredConstraint(PEGTransformer &transformer); static unique_ptr TransformDefaultValueInternal(PEGTransformer &transformer, ParseResult &parse_result); static ColumnConstraintEntry TransformDefaultValue(PEGTransformer &transformer, @@ -5437,14 +5437,14 @@ class PEGTransformerFactory { ColumnConstraintEntry check_constraint); static unique_ptr TransformTopPrimaryKeyConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result); - static unique_ptr TransformTopPrimaryKeyConstraint(PEGTransformer &transformer, - const vector &column_id_list, - const optional &constraint_timing); + static unique_ptr + TransformTopPrimaryKeyConstraint(PEGTransformer &transformer, const vector &column_id_list, + const optional &constraint_timing); static unique_ptr TransformTopUniqueConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result); static unique_ptr TransformTopUniqueConstraint(PEGTransformer &transformer, const vector &column_id_list, - const optional &constraint_timing); + const optional &constraint_timing); static unique_ptr TransformTopForeignKeyConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result); static unique_ptr TransformTopForeignKeyConstraint(PEGTransformer &transformer, diff --git a/src/duckdb/src/include/duckdb/planner/binder.hpp b/src/duckdb/src/include/duckdb/planner/binder.hpp index cee63274f..96d366208 100644 --- a/src/duckdb/src/include/duckdb/planner/binder.hpp +++ b/src/duckdb/src/include/duckdb/planner/binder.hpp @@ -268,7 +268,7 @@ class Binder : public enable_shared_from_this { const ColumnList &columns); unique_ptr BindUniqueConstraint(const Constraint &constraint, const Identifier &table, const ColumnList &columns); - static void VerifyConstraintTimingStorageVersion(const Constraint &constraint, Catalog &catalog, bool temporary); + static void VerifyConstraintCheckModeStorageVersion(const Constraint &constraint, Catalog &catalog, bool temporary); BoundStatement BindAlterAddIndex(BoundStatement &result, CatalogEntry &entry, unique_ptr alter_info); diff --git a/src/duckdb/src/include/duckdb/planner/expression_binder/index_binder.hpp b/src/duckdb/src/include/duckdb/planner/expression_binder/index_binder.hpp index ca186ee69..fae9d560a 100644 --- a/src/duckdb/src/include/duckdb/planner/expression_binder/index_binder.hpp +++ b/src/duckdb/src/include/duckdb/planner/expression_binder/index_binder.hpp @@ -18,6 +18,7 @@ namespace duckdb { class BoundColumnRefExpression; +class LogicalOperator; //! The IndexBinder binds indexes and expressions within index statements. class IndexBinder : public ExpressionBinder { diff --git a/src/duckdb/src/include/duckdb/storage/checkpoint/checkpoint_options.hpp b/src/duckdb/src/include/duckdb/storage/checkpoint/checkpoint_options.hpp index 8e2fc8883..ce6cbf652 100644 --- a/src/duckdb/src/include/duckdb/storage/checkpoint/checkpoint_options.hpp +++ b/src/duckdb/src/include/duckdb/storage/checkpoint/checkpoint_options.hpp @@ -28,9 +28,8 @@ struct CheckpointOptions { optional_idx checkpoint_id; //! What this checkpoint sees: timestamps below this bound are written VisibilityBound visibility_bound; - //! The WAL lock - in case we are holding it during the entire checkpoint. - //! This is only required if we are doing a checkpoint instead of writing to the WAL - optional_ptr> wal_lock; + //! The commit lock, when the committing transaction holds it for the whole checkpoint (checkpoint instead of WAL) + optional_ptr> commit_lock; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/storage/checkpoint/table_data_writer.hpp b/src/duckdb/src/include/duckdb/storage/checkpoint/table_data_writer.hpp index ae137da59..19bf21a6b 100644 --- a/src/duckdb/src/include/duckdb/storage/checkpoint/table_data_writer.hpp +++ b/src/duckdb/src/include/duckdb/storage/checkpoint/table_data_writer.hpp @@ -8,10 +8,14 @@ #pragma once +#include "duckdb/common/vector.hpp" #include "duckdb/parallel/task_executor.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" #include "duckdb/storage/checkpoint/row_group_writer.hpp" +#include "duckdb/storage/index_storage_info.hpp" namespace duckdb { +class TableIndexList; class DuckTableEntry; class TableStatistics; class SingleFileCheckpointWriter; @@ -35,8 +39,11 @@ class TableDataWriter { virtual void WriteUnchangedTable(MetaBlockPointer pointer, const vector &metadata_pointers, idx_t total_rows, idx_t next_row_id) = 0; virtual void FinalizeTable(const TableStatistics &global_stats, DataTableInfo &info, RowGroupCollection &collection, + const vector> &index_infos, Serializer &serializer) = 0; + virtual unique_ptr GetRowGroupWriter(RowGroup &row_group) = 0; + virtual unique_ptr GetTableIndexWriter(StorageVersion version) = 0; virtual void AddRowGroup(RowGroupPointer &&row_group_pointer, unique_ptr writer); virtual CheckpointOptions GetCheckpointOptions() const = 0; @@ -98,8 +105,11 @@ class SingleFileTableDataWriter : public TableDataWriter { void WriteUnchangedTable(MetaBlockPointer pointer, const vector &metadata_pointers, idx_t total_rows, idx_t next_row_id) override; void FinalizeTable(const TableStatistics &global_stats, DataTableInfo &info, RowGroupCollection &collection, - Serializer &serializer) override; + const vector> &index_infos, Serializer &serializer) override; + unique_ptr GetRowGroupWriter(RowGroup &row_group) override; + unique_ptr GetTableIndexWriter(StorageVersion version) override; + CheckpointOptions GetCheckpointOptions() const override; void FlushPartialBlocks() override; MetadataManager &GetMetadataManager() override; diff --git a/src/duckdb/src/include/duckdb/storage/checkpoint/table_index_writer.hpp b/src/duckdb/src/include/duckdb/storage/checkpoint/table_index_writer.hpp new file mode 100644 index 000000000..6faa1a573 --- /dev/null +++ b/src/duckdb/src/include/duckdb/storage/checkpoint/table_index_writer.hpp @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// duckdb/storage/checkpoint/table_index_writer.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/common/shared_ptr_ipp.hpp" +#include "duckdb/common/unique_ptr.hpp" +#include "duckdb/storage/index_storage_info.hpp" + +namespace duckdb { +class PartialBlockManager; +class SingleFileCheckpointWriter; +class BoundIndex; + +//! Checkpointing result for an index. The shadow index may be empty either when +//! the index is unbound, or the index does not support deferred checkpointing. +struct IndexCheckpointResult { + shared_ptr storage_info; + unique_ptr shadow_index; +}; + +class TableIndexWriter { +protected: + explicit TableIndexWriter(PartialBlockManager &partial_block_manager, StorageVersion version); + +public: + virtual ~TableIndexWriter(); + +public: + //! Segments managed by this block manager will be packed across indexes. + PartialBlockManager &GetSharedPartialBlockManager() const { + return partial_block_manager; + } + //! Segments managed by this block manager will be packer on a per-index basis. + virtual PartialBlockManager CreatePerIndexPartialBlockManager() = 0; + //! Writes the index buffers to disk + virtual void Flush() = 0; + //! Get the targeted storage version for the current writer + StorageVersion GetStorageVersion() const; + +protected: + //! Used to colocate blocks across indexes, used when index supports deferred checkpointing. + PartialBlockManager &partial_block_manager; + StorageVersion storage_version; +}; + +class SingleFileTableIndexWriter : public TableIndexWriter { +public: + explicit SingleFileTableIndexWriter(SingleFileCheckpointWriter &checkpoint_manager, StorageVersion version); + +private: + PartialBlockManager CreatePerIndexPartialBlockManager() override; + void Flush() override; + +private: + SingleFileCheckpointWriter &checkpoint_manager; +}; + +} // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/storage/checkpoint_manager.hpp b/src/duckdb/src/include/duckdb/storage/checkpoint_manager.hpp index 7f85f63c0..5cca2ef96 100644 --- a/src/duckdb/src/include/duckdb/storage/checkpoint_manager.hpp +++ b/src/duckdb/src/include/duckdb/storage/checkpoint_manager.hpp @@ -10,6 +10,7 @@ #include "duckdb/catalog/catalog.hpp" #include "duckdb/storage/partial_block_manager.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" namespace duckdb { @@ -52,10 +53,9 @@ class ActiveCheckpointWrapper { ~ActiveCheckpointWrapper(); - //! Begin the transaction within the newly created connection. - void GetCheckpointTransaction(CheckpointOptions &options); + //! Registers the checkpoint (id, bound, active checkpoint), through a checkpoint transaction if there is a context + void Begin(CheckpointOptions &options); void Commit(); - bool HasCheckpointContext() const; private: AttachedDatabase &db; @@ -63,6 +63,8 @@ class ActiveCheckpointWrapper { unique_ptr checkpoint_connection; optional_ptr checkpoint_context; optional_ptr checkpoint_transaction; + //! Whether Begin registered an active checkpoint that Commit has not reset yet + bool active = false; }; class CheckpointWriter { @@ -134,6 +136,7 @@ class SingleFileCheckpointReader final : public CheckpointReader { }; class SingleFileCheckpointWriter final : public CheckpointWriter { + friend class SingleFileTableIndexWriter; friend class SingleFileRowGroupWriter; friend class SingleFileTableDataWriter; @@ -148,6 +151,8 @@ class SingleFileCheckpointWriter final : public CheckpointWriter { unique_ptr GetTableDataWriter(TableCatalogEntry &table) override; BlockManager &GetBlockManager(); + //! Make a partial block scope for an index that persists immediately. + PartialBlockManager CreatePerIndexPartialBlockManager(); CheckpointOptions GetCheckpointOptions() const { return options; } @@ -167,6 +172,8 @@ class SingleFileCheckpointWriter final : public CheckpointWriter { //! Because this is single-file storage, we can share partial blocks across //! an entire checkpoint. PartialBlockManager partial_block_manager; + //! Need to have a separate partial block manager for indexes due to different block shapes. + PartialBlockManager index_partial_block_manager; //! Checkpoint type CheckpointOptions options; //! Block usage count for verification purposes diff --git a/src/duckdb/src/include/duckdb/storage/data_table.hpp b/src/duckdb/src/include/duckdb/storage/data_table.hpp index 60964eb3d..7be39c290 100644 --- a/src/duckdb/src/include/duckdb/storage/data_table.hpp +++ b/src/duckdb/src/include/duckdb/storage/data_table.hpp @@ -9,6 +9,7 @@ #pragma once #include "duckdb/common/enums/column_segment_info_scan_type.hpp" +#include "duckdb/common/enums/constraint_check_mode.hpp" #include "duckdb/common/unique_ptr.hpp" #include "duckdb/storage/table/data_table_info.hpp" #include "duckdb/storage/table/persistent_table_data.hpp" @@ -35,6 +36,7 @@ class Serializer; class StorageManager; class TableCatalogEntry; class TableIOManager; +class TableStorageInfo; class Vector; class Transaction; class WriteAheadLog; @@ -227,8 +229,6 @@ class DataTable : public enable_shared_from_this { //! Sets statistics of a physical column within the table void SetDistinct(column_t column_id, unique_ptr distinct_stats); - //! Obtains a lock during a checkpoint operation that prevents other threads from reading this table - unique_ptr GetCheckpointLock(); //! Checkpoint the table to the specified table data writer void Checkpoint(TableDataWriter &writer, Serializer &serializer); //! Accumulates the table's on-disk blocks for reclamation into the drop state. @@ -290,7 +290,7 @@ class DataTable : public enable_shared_from_this { //! AddIndex initializes an index and adds it to the table's index list. //! It is either empty, or initialized via its index storage information. void AddIndex(const ColumnList &columns, const vector &column_indexes, const IndexConstraintType type, - IndexStorageInfo index_info); + IndexStorageInfo index_info, ConstraintCheckMode check_mode); //! AddIndex moves an index to this table's index list. void AddIndex(unique_ptr index); diff --git a/src/duckdb/src/include/duckdb/storage/index.hpp b/src/duckdb/src/include/duckdb/storage/index.hpp index 53004460c..c03e2a581 100644 --- a/src/duckdb/src/include/duckdb/storage/index.hpp +++ b/src/duckdb/src/include/duckdb/storage/index.hpp @@ -14,11 +14,23 @@ #include "duckdb/common/typedefs.hpp" #include "duckdb/common/vector.hpp" #include "duckdb/common/unordered_set.hpp" +#include "duckdb/storage/storage_info.hpp" namespace duckdb { class AttachedDatabase; class TableIOManager; +class TableIndexWriter; +class PartialBlockManager; +struct IndexCheckpointResult; + +//! Describes how an index participates in a checkpoint when persisting data to the filesystem. +//! +//! IMMEDIATE (default): flush buffers under the index lock. +//! DEFERRED: share blocks across indexes and finish flushing after releasing the lock. Return a logically equivalent +//! shadow with independent backing for modified buffers; keep the live index usable until installation after flushing. +//! Concurrent changes use deltas, requiring SupportsDeltaIndexes(), CreateEmptyCopy(), and MergeCheckpointDelta(). +enum class IndexCheckpointMode : uint8_t { IMMEDIATE, DEFERRED }; //! The index is an abstract base class that serves as the basis for indexes class Index { @@ -57,6 +69,14 @@ class Index { //! The index constraint type virtual IndexConstraintType GetConstraintType() const = 0; + //! Returns how this index participates in a checkpoint, defaults to IMMEDIATE mode. + virtual IndexCheckpointMode GetCheckpointMode() const { + return IndexCheckpointMode::IMMEDIATE; + } + + //! Checkpoint an index, based on the mode supported by the index flushing may be deferred. + virtual IndexCheckpointResult Checkpoint(PartialBlockManager &partial_block_manager, const StorageVersion version); + //! Returns unique flag bool IsUnique() const { auto type = GetConstraintType(); diff --git a/src/duckdb/src/include/duckdb/storage/index_storage_info.hpp b/src/duckdb/src/include/duckdb/storage/index_storage_info.hpp index 3f29aebd7..65c5cd5b9 100644 --- a/src/duckdb/src/include/duckdb/storage/index_storage_info.hpp +++ b/src/duckdb/src/include/duckdb/storage/index_storage_info.hpp @@ -9,6 +9,7 @@ #pragma once #include "duckdb/common/case_insensitive_map.hpp" +#include "duckdb/common/enums/constraint_check_mode.hpp" #include "duckdb/common/identifier.hpp" #include "duckdb/common/shared_ptr.hpp" #include "duckdb/common/types/value.hpp" @@ -91,6 +92,7 @@ struct IndexInfo { bool is_unique; bool is_primary; bool is_foreign; + ConstraintCheckMode check_mode = ConstraintCheckMode::DEFAULT; unordered_set column_set; }; diff --git a/src/duckdb/src/include/duckdb/storage/storage_info.hpp b/src/duckdb/src/include/duckdb/storage/storage_info.hpp index 6bbc06c35..050517de5 100644 --- a/src/duckdb/src/include/duckdb/storage/storage_info.hpp +++ b/src/duckdb/src/include/duckdb/storage/storage_info.hpp @@ -9,11 +9,8 @@ #pragma once #include "duckdb/common/constants.hpp" -#include "duckdb/common/limits.hpp" #include "duckdb/common/string.hpp" #include "duckdb/common/vector_size.hpp" -#include "duckdb/common/optional_idx.hpp" -#include "duckdb/common/types/string_type.hpp" #include "duckdb/common/query_context.hpp" namespace duckdb { diff --git a/src/duckdb/src/include/duckdb/storage/storage_manager.hpp b/src/duckdb/src/include/duckdb/storage/storage_manager.hpp index 816209350..1c693224c 100644 --- a/src/duckdb/src/include/duckdb/storage/storage_manager.hpp +++ b/src/duckdb/src/include/duckdb/storage/storage_manager.hpp @@ -90,9 +90,9 @@ class StorageManager { bool WALStartCheckpoint(MetaBlockPointer meta_block, CheckpointOptions &options, ActiveCheckpointWrapper &active_checkpoint); //! Finishes a checkpoint - void WALFinishCheckpoint(unique_lock &wal_lock); - // Get the WAL lock - unique_lock GetWALLock(); + void WALFinishCheckpoint(unique_lock &commit_lock); + //! Acquires commit_lock + unique_lock GetCommitLock(); //! Returns the database file path string GetDBPath() const { @@ -174,8 +174,8 @@ class StorageManager { string wal_path; //! The WriteAheadLog of the storage manager unique_ptr wal; - //! Mutex used to control writes to the WAL - mutex wal_lock; + //! Held by every commit with changes from its append to its commit or revert, and by a checkpoint at start and end + mutex commit_lock; //! Whether or not the database is opened in read-only mode bool read_only; //! When loading a database, we do not yet set the wal-field. Therefore, GetWriteAheadLog must diff --git a/src/duckdb/src/include/duckdb/storage/table/append_state.hpp b/src/duckdb/src/include/duckdb/storage/table/append_state.hpp index 2275a27e5..912c93037 100644 --- a/src/duckdb/src/include/duckdb/storage/table/append_state.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/append_state.hpp @@ -25,7 +25,6 @@ class TableCatalogEntry; template struct SegmentNode; class RowGroupSegmentTree; -class CheckpointLock; struct TableAppendState; @@ -100,7 +99,6 @@ struct TableAppendState { RowGroupAppendState row_group_append_state; unique_lock append_lock; - shared_ptr table_lock; row_t row_start; row_t current_row; //! The total number of rows appended by the append operation diff --git a/src/duckdb/src/include/duckdb/storage/table/data_table_info.hpp b/src/duckdb/src/include/duckdb/storage/table/data_table_info.hpp index a8cff5000..9331af9d9 100644 --- a/src/duckdb/src/include/duckdb/storage/table/data_table_info.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/data_table_info.hpp @@ -45,9 +45,6 @@ struct DataTableInfo { } //! Find and move out an IndexStorageInfo by name from the stored collection. IndexStorageInfo ExtractIndexStorageInfo(const Identifier &name); - unique_ptr GetSharedLock() { - return checkpoint_lock.GetSharedLock(); - } bool AppendRequiresNewRowGroup(RowGroupCollection &collection, optional_idx checkpoint_id); optional_idx CheckpointRowGroupCount(const CheckpointOptions &options) const; void VerifyIndexBuffers() const; @@ -73,8 +70,6 @@ struct DataTableInfo { TableIndexList indexes; //! Index storage information of the indexes created by this table vector index_storage_infos; - //! Lock held while checkpointing - StorageLock checkpoint_lock; //! The last seen checkpoint while doing a concurrent operation, if any optional_idx last_seen_checkpoint; //! The amount of row groups the checkpoint is processing diff --git a/src/duckdb/src/include/duckdb/storage/table/delete_state.hpp b/src/duckdb/src/include/duckdb/storage/table/delete_state.hpp index 662a26ebd..d3a05eeeb 100644 --- a/src/duckdb/src/include/duckdb/storage/table/delete_state.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/delete_state.hpp @@ -18,7 +18,6 @@ struct TableDeleteState { bool has_delete_constraints = false; DataChunk verify_chunk; vector col_ids; - shared_ptr checkpoint_lock; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/storage/table/in_memory_checkpoint.hpp b/src/duckdb/src/include/duckdb/storage/table/in_memory_checkpoint.hpp index 0c801c612..b87794965 100644 --- a/src/duckdb/src/include/duckdb/storage/table/in_memory_checkpoint.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/in_memory_checkpoint.hpp @@ -52,8 +52,9 @@ class InMemoryTableDataWriter : public TableDataWriter { void WriteUnchangedTable(MetaBlockPointer pointer, const vector &metadata_pointers, idx_t total_rows, idx_t next_row_id) override; void FinalizeTable(const TableStatistics &global_stats, DataTableInfo &info, RowGroupCollection &collection, - Serializer &serializer) override; + const vector> &index_infos, Serializer &serializer) override; unique_ptr GetRowGroupWriter(RowGroup &row_group) override; + unique_ptr GetTableIndexWriter(StorageVersion version) override; void FlushPartialBlocks() override; CheckpointOptions GetCheckpointOptions() const override; MetadataManager &GetMetadataManager() override; diff --git a/src/duckdb/src/include/duckdb/storage/table/index_entry.hpp b/src/duckdb/src/include/duckdb/storage/table/index_entry.hpp index 03a73a009..840429f10 100644 --- a/src/duckdb/src/include/duckdb/storage/table/index_entry.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/index_entry.hpp @@ -8,11 +8,13 @@ #pragma once +#include "duckdb/common/enums/constraint_check_mode.hpp" #include "duckdb/common/enums/index_removal_type.hpp" #include "duckdb/common/optional_ptr.hpp" -#include "duckdb/common/shared_ptr.hpp" #include "duckdb/execution/index/bound_index.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" #include "duckdb/storage/index.hpp" +#include "duckdb/storage/storage_info.hpp" #include "duckdb/storage/storage_lock.hpp" #include @@ -24,6 +26,7 @@ class ConflictManager; class IndexEntry; class IndexBinder; class TableIndexList; +class TableIndexWriter; struct IndexStorageInfo; //! IndexBindState transitions index binding phases and marks entries whose physical index has been destroyed. @@ -111,7 +114,7 @@ class IndexWriteHandle { //! The IndexEntry provides a stable logical identity which refers to an interchangeable snapshot of an index. class IndexEntry : public enable_shared_from_this { public: - explicit IndexEntry(unique_ptr index); + IndexEntry(unique_ptr index, ConstraintCheckMode check_mode); //! Append a chunk to the physical index, buffering it while the index is unbound. void Append(DataChunk &chunk, Vector &row_ids); //! Appends a chunk using delete and checkpoint indexes where required. @@ -126,6 +129,8 @@ class IndexEntry : public enable_shared_from_this { optional_idx active_checkpoint); //! Returns whether the physical index enforces a unique constraint. bool IsUnique() const; + //! Returns whether the constraint enforced by the physical index is only checked when committing. + bool IsDeferred() const; //! Returns whether the physical index matches the foreign key columns and role. bool IsForeignKeyIndex(const vector &fk_keys, ForeignKeyType fk_type) const; //! Returns the name of the physical index. @@ -161,14 +166,18 @@ class IndexEntry : public enable_shared_from_this { IndexInfo GetStorageInfo() const; //! Returns the in-memory size of the physical index, or zero if it is unbound. idx_t GetInMemorySize() const; - //! Serializes the physical index for a checkpoint. - IndexStorageInfo SerializeToDisk(QueryContext context, const case_insensitive_map_t &options); + //! Persist the index through the index writer and optionally returns a shadow index. + IndexCheckpointResult Checkpoint(TableIndexWriter &writer); + //! Install a shadow index in place of the current live index. + void InstallShadow(unique_ptr shadow_index); //! Serializes the bound physical index for the write-ahead log. - IndexStorageInfo SerializeToWAL(const case_insensitive_map_t &options); + IndexStorageInfo SerializeToWAL(const StorageVersion version); //! Merges checkpoint deltas into the bound physical index and marks the checkpoint as written. void MergeCheckpointDeltas(optional_idx checkpoint_id); - //! Adds transaction-local copies of the physical index to the target lists when required. + //! Adds the required transaction-local delete and append indexes. void InitializeLocalIndexes(TableIndexList &delete_indexes, TableIndexList &append_indexes) const; + //! Adds only the transaction-local delete index. + void InitializeLocalDeleteIndex(TableIndexList &delete_indexes) const; public: //! Acquire shared access to a stable physical index. @@ -193,12 +202,20 @@ class IndexEntry : public enable_shared_from_this { friend class IndexReadHandle; template friend class IndexWriteHandle; + void InitializeLocalIndexesInternal(TableIndexList &delete_indexes, + optional_ptr append_indexes) const; + //! Replace the physical index, this assumes an exclusive lock on the index is being held. + void InstallShadowInternal(unique_ptr shadow_index); + +private: atomic bind_state; //! Phase-fair lock protecting the physical index and all delta indexes owned by this entry. mutable StorageLock lock; //! The physical index owned by this stable logical entry. unique_ptr owned_index; + //! Whether the enforced constraint is deferred. Derived from the catalog constraint, not serialized. + const ConstraintCheckMode check_mode; IndexDeltas deltas; }; diff --git a/src/duckdb/src/include/duckdb/storage/table/row_group.hpp b/src/duckdb/src/include/duckdb/storage/table/row_group.hpp index 6fc033ba5..b740aa728 100644 --- a/src/duckdb/src/include/duckdb/storage/table/row_group.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/row_group.hpp @@ -95,6 +95,31 @@ struct RowGroupWriteData { optional_idx write_count; }; +//! The version info of a row group, shared with the row groups a checkpoint or an ALTER copies it into +class RowGroupVersionInfo { +public: + explicit RowGroupVersionInfo(vector deletes_pointers = {}); + + //! Loads the persisted deletes if necessary + optional_ptr Get(BlockManager &block_manager); + //! Does not load persisted deletes + optional_ptr GetIfLoaded() const { + return version_info.load(); + } + shared_ptr GetOrCreate(BlockManager &block_manager); + bool HasUnloadedDeletes() const; + const vector &GetDeletesPointers() const { + return deletes_pointers; + } + +private: + mutex lock; + vector deletes_pointers; + atomic deletes_is_loaded; + shared_ptr owned_version_info; + atomic> version_info; +}; + class RowGroup : public SegmentBase { public: friend class ColumnData; @@ -109,9 +134,7 @@ class RowGroup : public SegmentBase { //! The RowGroupCollection this row-group is a part of reference collection; //! The version info of the row_group (inserted and deleted tuple info) - atomic> version_info; - //! The owned version info of the row_group (inserted and deleted tuple info) - shared_ptr owned_version_info; + shared_ptr version_info; //! The column data of the row_group (mutable because `const` can lazily load) mutable vector> columns; @@ -146,7 +169,7 @@ class RowGroup : public SegmentBase { void CommitDrop(); void InitializeEmpty(const vector &types, ColumnDataType data_type); - bool HasChanges() const; + bool HasChanges(VisibilityBound bound) const; //! Initialize a scan over this row_group bool InitializeScan(CollectionScanState &state, SegmentNode &node); @@ -277,8 +300,6 @@ class RowGroup : public SegmentBase { optional_ptr GetVersionInfo(); optional_ptr GetVersionInfoIfLoaded() const; shared_ptr GetOrCreateVersionInfoPtr(); - shared_ptr GetOrCreateVersionInfoInternal(); - void SetVersionInfo(shared_ptr version); ColumnData &GetColumn(storage_t c) const; void LoadColumn(storage_t c) const; @@ -301,12 +322,10 @@ class RowGroup : public SegmentBase { vector column_pointers; //! Whether or not each column is loaded (mutable because `const` can lazy load) mutable unique_ptr[]> is_loaded; - vector deletes_pointers; bool has_metadata_blocks = false; vector extra_metadata_blocks; bool has_per_column_metadata_blocks = false; PerColumnMetadataBlocks per_column_metadata_blocks; - atomic deletes_is_loaded; atomic allocation_size; //! The row id column data (mutable because `const` can lazy load) mutable unique_ptr row_id_column_data; diff --git a/src/duckdb/src/include/duckdb/storage/table/row_group_collection.hpp b/src/duckdb/src/include/duckdb/storage/table/row_group_collection.hpp index 5f5fc7db3..0e4e7b70f 100644 --- a/src/duckdb/src/include/duckdb/storage/table/row_group_collection.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/row_group_collection.hpp @@ -37,6 +37,7 @@ struct ColumnSegmentInfo; class MetadataManager; struct VacuumState; struct CollectionCheckpointState; + struct PersistentCollectionData; class CheckpointTask; class TableIOManager; @@ -68,6 +69,24 @@ struct ColumnSegmentInfoScanState { ColumnSegmentInfoScanOptions options; }; +//! The row groups that existed when the checkpoint started +struct CollectionCheckpointSnapshot { + shared_ptr row_groups; + idx_t segment_count = 0; + //! The row group count recorded by the first append that saw this checkpoint, if any + optional_idx row_group_count; +}; + +//! The rewritten row groups, installed by InstallCheckpoint +struct CollectionCheckpointResult { + //! Null if the table was unchanged + shared_ptr row_groups; + idx_t total_rows = 0; + idx_t next_row_id = 0; + //! The live row groups past this count were appended while the checkpoint ran + idx_t checkpointed_row_group_count = 0; +}; + class RowGroupCollection { public: RowGroupCollection(shared_ptr info, TableIOManager &io_manager, vector types, @@ -145,7 +164,11 @@ class RowGroupCollection { void UpdateColumn(TransactionData transaction, DuckTableEntry &table_entry, Vector &row_ids, const vector &column_path, DataChunk &updates); - void Checkpoint(TableDataWriter &writer, TableStatistics &global_stats); + CollectionCheckpointSnapshot SnapshotForCheckpoint(TableDataWriter &writer) const; + CollectionCheckpointResult Checkpoint(TableDataWriter &writer, TableStatistics &global_stats, + const CollectionCheckpointSnapshot &snapshot); + //! Takes over the row groups appended meanwhile. Runs under the table's append lock + void InstallCheckpoint(CollectionCheckpointResult result, TableStatistics &checkpoint_stats); //! Decides how vacuum handles this table's indexes. VacuumIndexStrategy @@ -189,7 +212,6 @@ class RowGroupCollection { void VerifyNewConstraint(const QueryContext &context, DuckTransaction &transaction, DataTable &parent, const BoundConstraint &constraint); - void SetStats(TableStatistics &new_stats); void CopyStats(TableStatistics &stats); unique_ptr CopyStats(const StorageIndex &column_id); unique_ptr GetSample(); diff --git a/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp b/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp index cf5627b13..60cd19ed4 100644 --- a/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp @@ -50,7 +50,7 @@ class RowVersionManager { vector Checkpoint(RowGroupWriter &writer); static shared_ptr Deserialize(MetaBlockPointer delete_pointer, MetadataManager &manager); - bool HasUnserializedChanges(); + bool HasUnserializedChanges(VisibilityBound bound); bool HasDeletes(); bool HasUncommittedChanges(); vector GetStoragePointers(); @@ -59,7 +59,9 @@ class RowVersionManager { mutex version_lock; FixedSizeAllocator allocator; vector> vector_info; - optional_idx uncheckpointed_delete_commit; + //! The newest and the oldest delete commit that has not been written by a checkpoint yet + optional_idx newest_uncheckpointed_delete_commit; + optional_idx oldest_uncheckpointed_delete_commit; vector storage_pointers; //! Whether a compression pass may achieve anything: set when version ids are modified, cleared when a //! pass finds no ids that could still compress. For deserialized version info this is derived from the 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 7541492cb..3ae022601 100644 --- a/src/duckdb/src/include/duckdb/storage/table/scan_state.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/scan_state.hpp @@ -351,15 +351,6 @@ struct TableScanOptions { bool force_fetch_row = false; }; -class CheckpointLock { -public: - explicit CheckpointLock(unique_ptr lock_p) : lock(std::move(lock_p)) { - } - -private: - unique_ptr lock; -}; - class TableScanState { public: TableScanState(); @@ -371,8 +362,6 @@ class TableScanState { CollectionScanState local_state; //! Options for scanning TableScanOptions options; - //! Shared lock over the checkpoint to prevent checkpoints while reading - shared_ptr checkpoint_lock; //! Filter info ScanFilterInfo filters; //! Sampling info @@ -432,8 +421,6 @@ struct ParallelTableScanState { ParallelCollectionScanState scan_state; //! Parallel scan state for the transaction-local state ParallelCollectionScanState local_state; - //! Shared lock over the checkpoint to prevent checkpoints while reading - shared_ptr checkpoint_lock; }; struct PrefetchState { 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 d4ab10d52..16a59a1c8 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 @@ -11,6 +11,8 @@ #include "duckdb/common/mutex.hpp" #include "duckdb/common/enums/index_removal_type.hpp" #include "duckdb/common/optional_ptr.hpp" +#include "duckdb/common/shared_ptr_ipp.hpp" +#include "duckdb/common/vector.hpp" #include "duckdb/storage/table/index_entry.hpp" #include "duckdb/execution/index/bound_index.hpp" #include "duckdb/storage/index.hpp" @@ -28,20 +30,10 @@ class IndexWriteHandle; class IndexBinder; struct IndexStorageInfo; struct DataTableInfo; +struct IndexCheckpointResult; template class TableIndexIterationHelper; - -struct IndexSerializationInfo { - case_insensitive_map_t options; -}; - -// IndexStorageInfo is move-only. Keep every serialized info in owned_infos and expose stable ordered references. -struct IndexSerializationResult { - //! The ordered list of references to serialize - preserves iteration order of index_entries - vector> ordered_infos; - //! Storage for index infos to keep the references in ordered_infos alive. - vector owned_infos; -}; +class TableIndexWriter; class TableIndexList { public: @@ -49,8 +41,8 @@ class TableIndexList { //! Iterates over shared ownership of stable index entries while holding the entry-list lock. TableIndexIterationHelper> IndexEntries() const; - //! Adds an index entry to the list of index entries. - void AddIndex(unique_ptr index); + //! Adds an index entry to the list of index entries, and returns it. + shared_ptr AddIndex(unique_ptr index, ConstraintCheckMode check_mode); //! Initializes the transaction-local delete and append indexes. void InitializeLocalIndexes(TableIndexList &delete_indexes, TableIndexList &append_indexes) const; //! Appends a chunk to all index entries. @@ -93,7 +85,7 @@ class TableIndexList { } //! Returns true, if there are unique indexes. bool HasUniqueIndexes() const; - //! Verifies all unique ART indexes, optionally recording conflicts. + //! Verifies all unique ART indexes that are not deferred, optionally recording conflicts. void VerifyUniqueIndexes(optional_ptr delete_indexes, DataChunk &chunk, optional_ptr manager) const; //! Vacuums all bound indexes. @@ -131,16 +123,16 @@ class TableIndexList { //! Verify a foreign key constraint. void VerifyForeignKey(optional_ptr delete_indexes, const vector &fk_keys, DataChunk &chunk, ConflictManager &conflict_manager); + //! Persist index buffers and install replacements before returning their metadata. + vector> Checkpoint(TableIndexWriter &writer); //! Returns the physical table columns referenced by any index. unordered_set GetIndexedColumns() const; //! Returns the column sets of unique indexes matching the conflict target. vector> GetConflictTargetColumns(const ConflictInfo &conflict_info) const; //! Get the combined column ids of the unique indexes. unordered_set GetUniqueIndexColumns() const; - //! Serialize all indexes of the table. - IndexSerializationResult SerializeToDisk(QueryContext context, const IndexSerializationInfo &info); //! Serializes the index matching the name for the write-ahead log, if it exists. - unique_ptr SerializeToWAL(const Identifier &name, const case_insensitive_map_t &options); + unique_ptr SerializeToWAL(const Identifier &name, const StorageVersion version); public: //! Initialize an index_chunk from a table. diff --git a/src/duckdb/src/include/duckdb/storage/table/table_statistics.hpp b/src/duckdb/src/include/duckdb/storage/table/table_statistics.hpp index 7f9ac2b86..9a86f1aa3 100644 --- a/src/duckdb/src/include/duckdb/storage/table/table_statistics.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/table_statistics.hpp @@ -43,7 +43,6 @@ class TableStatistics { void MergeStats(TableStatisticsLock &lock, idx_t i, BaseStatistics &stats, StatsMergeType merge_type = StatsMergeType::MERGE_STATS); - void SetStats(TableStatistics &other); void CopyStats(TableStatistics &other); void CopyStats(TableStatisticsLock &lock, TableStatistics &other); unique_ptr CopyStats(const StorageIndex &i); diff --git a/src/duckdb/src/include/duckdb/storage/table/update_segment.hpp b/src/duckdb/src/include/duckdb/storage/table/update_segment.hpp index 871d33226..b4c07a07b 100644 --- a/src/duckdb/src/include/duckdb/storage/table/update_segment.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/update_segment.hpp @@ -69,11 +69,11 @@ class UpdateSegment { StringHeap heap; public: - typedef void (*initialize_update_function_t)(UpdateInfo &base_info, Vector &base_data, UpdateInfo &update_info, - UnifiedVectorFormat &update, const SelectionVector &sel); - typedef void (*merge_update_function_t)(UpdateInfo &base_info, Vector &base_data, UpdateInfo &update_info, - UnifiedVectorFormat &update, row_t *ids, idx_t count, - const SelectionVector &sel, idx_t row_group_start); + typedef void (*merge_update_function_t)(UpdateInfo &info, UnifiedVectorFormat &update, row_t *ids, idx_t count, + const SelectionVector &sel, idx_t vector_offset); + typedef void (*save_undo_function_t)(UpdateInfo &source, UpdateInfo &root, row_t *ids, idx_t count, + const SelectionVector &sel, idx_t vector_offset, UpdateInfo &target); + typedef void (*rollback_update_function_t)(UpdateInfo &root, UpdateInfo &rollback_info); typedef void (*fetch_update_function_t)(const SnapshotView &view, UpdateInfo &info, Vector &result); typedef void (*fetch_committed_function_t)(UpdateInfo &info, Vector &result); typedef void (*fetch_committed_range_function_t)(UpdateInfo &info, idx_t start, idx_t end, idx_t result_offset, @@ -81,28 +81,25 @@ class UpdateSegment { typedef void (*fetch_rows_function_t)(const SnapshotView &view, UpdateInfo &info, const idx_t *offsets, const SelectionVector &sel, idx_t fetch_offset, idx_t count, idx_t vector_offset, Vector &result, idx_t result_offset); - typedef void (*rollback_update_function_t)(UpdateInfo &base_info, UpdateInfo &rollback_info); typedef idx_t (*statistics_update_function_t)(UpdateSegment *segment, SegmentStatistics &stats, UnifiedVectorFormat &update, idx_t count, SelectionVector &sel); typedef idx_t (*get_effective_updates_t)(UnifiedVectorFormat &update_format, row_t *ids, idx_t count, SelectionVector &sel, Vector &base_data, idx_t id_offset); private: - initialize_update_function_t initialize_update_function; merge_update_function_t merge_update_function; + save_undo_function_t save_undo_function; + rollback_update_function_t rollback_update_function; fetch_update_function_t fetch_update_function; fetch_committed_function_t fetch_committed_function; fetch_committed_range_function_t fetch_committed_range; fetch_rows_function_t fetch_rows_function; - rollback_update_function_t rollback_update_function; statistics_update_function_t statistics_update_function; get_effective_updates_t get_effective_updates; private: UndoBufferPointer GetUpdateNode(StorageLockKey &lock, idx_t vector_idx) const; void InitializeUpdateInfo(idx_t vector_idx); - void InitializeUpdateInfo(UpdateInfo &info, row_t *ids, const SelectionVector &sel, idx_t count, idx_t vector_index, - idx_t vector_offset); void ReallocateRootInfoIfNeeded(UpdateInfo ¤t_info, idx_t update_count, idx_t vector_index); }; diff --git a/src/duckdb/src/include/duckdb/transaction/duck_transaction.hpp b/src/duckdb/src/include/duckdb/transaction/duck_transaction.hpp index cbfa0ae62..5b2430ada 100644 --- a/src/duckdb/src/include/duckdb/transaction/duck_transaction.hpp +++ b/src/duckdb/src/include/duckdb/transaction/duck_transaction.hpp @@ -16,7 +16,6 @@ #include "duckdb/common/enums/active_transaction_state.hpp" namespace duckdb { -class CheckpointLock; class CommitDropState; class DuckTableEntry; class RowGroupCollection; @@ -24,7 +23,6 @@ class RowVersionManager; class DuckTransactionManager; class StorageLockKey; class StorageCommitState; -struct DataTableInfo; struct UndoBufferProperties; struct CommitInfo { @@ -72,6 +70,10 @@ class DuckTransaction : public Transaction { bool ShouldWriteToWAL(AttachedDatabase &db); ErrorData PreFlushOptimisticBlocks(AttachedDatabase &db) noexcept; + //! Appends the local storage to the tables; with a WAL, the commit state records the optimistically written blocks + ErrorData AppendLocalStorage(ClientContext &context, AttachedDatabase &db, + unique_ptr &commit_state) noexcept; + //! Writes the undo buffer to the WAL, with the commit state of AppendLocalStorage ErrorData WriteToWAL(ClientContext &context, AttachedDatabase &db, unique_ptr &commit_state) noexcept; //! Commit the current transaction with the given commit identifier. Returns an error message if the transaction @@ -93,8 +95,8 @@ class DuckTransaction : public Transaction { idx_t base_row); void PushSequenceUsage(SequenceCatalogEntry &entry, const SequenceData &data); void PushAppend(DuckTableEntry &table_entry, idx_t row_start, idx_t row_count); - UndoBufferReference CreateUpdateInfo(DuckTableEntry &table_entry, idx_t type_size, idx_t entries, - idx_t row_group_start); + UndoBufferReference CreateUpdateInfo(DuckTableEntry &table_entry, idx_t type_size, idx_t capacity, + idx_t value_capacity, idx_t row_group_start); DuckTransactionManager &GetTransactionManager(); bool IsDuckTransaction() const override { @@ -107,9 +109,6 @@ class DuckTransaction : public Transaction { unique_ptr TryGetCheckpointLock(); - //! Get a shared lock on a table - shared_ptr SharedLockTable(DataTableInfo &info); - void SetIsCheckpointTransaction() { is_checkpoint_transaction = true; } @@ -128,14 +127,6 @@ class DuckTransaction : public Transaction { mutex sequence_lock; //! Map of all sequences that were used during the transaction and the value they had in this transaction reference_map_t> sequence_usage; - //! Lock for the active_locks map - mutex active_locks_lock; - struct ActiveTableLock { - mutex checkpoint_lock_mutex; // protects access to the checkpoint_lock field in this class - weak_ptr checkpoint_lock; - }; - //! Active locks on tables - reference_map_t> active_locks; //! Flag to prevent auto-checkpointing inside a checkpoint transaction. bool is_checkpoint_transaction = false; }; diff --git a/src/duckdb/src/include/duckdb/transaction/duck_transaction_manager.hpp b/src/duckdb/src/include/duckdb/transaction/duck_transaction_manager.hpp index 2579da7b9..82bd4d417 100644 --- a/src/duckdb/src/include/duckdb/transaction/duck_transaction_manager.hpp +++ b/src/duckdb/src/include/duckdb/transaction/duck_transaction_manager.hpp @@ -56,7 +56,7 @@ class DuckTransactionManager : public TransactionManager { transaction_t GetLastCommit() const { return last_commit; } - //! Wait until every published commit is durable. Called under the WAL lock, so no new commit can + //! Wait until every published commit is durable. Called under the commit lock, so no new commit can //! enter its sync window and the wait is bounded by the syncs in flight void WaitForDurability(); optional_idx GetActiveCheckpoint() const { diff --git a/src/duckdb/src/include/duckdb/transaction/update_info.hpp b/src/duckdb/src/include/duckdb/transaction/update_info.hpp index 10603f009..bd7d422f0 100644 --- a/src/duckdb/src/include/duckdb/transaction/update_info.hpp +++ b/src/duckdb/src/include/duckdb/transaction/update_info.hpp @@ -14,6 +14,7 @@ #include "duckdb/common/types/validity_mask.hpp" #include "duckdb/transaction/undo_buffer_allocator.hpp" #include "duckdb/common/atomic.hpp" +#include "duckdb/common/vector.hpp" namespace duckdb { class UpdateSegment; @@ -22,8 +23,12 @@ class DuckTableEntry; //! UpdateInfo is a class that represents a set of updates applied to a single vector. //! The UpdateInfo struct contains metadata associated with the update. -//! After the UpdateInfo, we must ALWAYS allocate the list of tuples and the data as contiguous arrays: -//! [UpdateInfo][TUPLES (sel_t[max])][DATA (T[max])] +//! The root UpdateInfo of a vector holds the latest values, and is laid out as: +//! [UpdateInfo][TUPLES (sel_t[max])][VALUES (T[max])] +//! The UpdateInfo of a transaction holds the values from before its updates. Tuples that had no update before have no +//! value: they are invalid in the value mask, and their value before the update is the base value. The values of +//! the other tuples are stored in order of the tuples: +//! [UpdateInfo][TUPLES (sel_t[max])][VALUE MASK (validity_t[max / 64])][VALUES (T[value_max])] //! The required allocation size can be obtained using UpdateInfo::GetAllocSize struct UpdateInfo { //! The update segment that this update info affects @@ -46,6 +51,10 @@ struct UpdateInfo { UndoBufferPointer prev; //! The next update info in the chain (or nullptr if it is the last) UndoBufferPointer next; + //! The offset of the value mask (or 0 for the root) + idx_t value_mask_offset; + //! The maximum amount of values that can fit into this UpdateInfo (equal to max for the root) + sel_t value_max; //! The row ids of the tuples that have been updated. This should always be kept sorted! sel_t *GetTuples(); @@ -58,6 +67,9 @@ struct UpdateInfo { return reinterpret_cast(GetValues()); } + //! For each tuple, whether a value from before the update is stored - only for transactions + ValidityMask GetValueMask(); + bool AppliesToTransaction(const SnapshotView &view) { // these tuples are either committed outside this transaction's snapshot or not committed yet, use // tuples stored in this version @@ -68,21 +80,16 @@ struct UpdateInfo { return !view.Sees(version_number.load()); } - //! Loop over the update chain and execute the specified callback on all UpdateInfo's that are relevant for that - //! transaction in-order of newest to oldest - template - static void UpdatesForTransaction(UpdateInfo ¤t, const SnapshotView &view, T &&callback) { - if (current.AppliesToTransaction(view)) { - callback(current); - } - auto update_ptr = current.next; + //! Collect the update infos after the root that are not visible to the transaction, newest to oldest + static void GetInvisibleUpdates(UpdateInfo &root, const SnapshotView &view, vector &result) { + auto update_ptr = root.next; while (update_ptr.IsSet()) { auto pin = update_ptr.Pin(); auto &info = Get(pin); + update_ptr = info.next; if (info.AppliesToTransaction(view)) { - callback(info); + result.push_back(std::move(pin)); } - update_ptr = info.next; } } @@ -93,18 +100,18 @@ struct UpdateInfo { bool HasPrev() const; bool HasNext() const; static UpdateInfo &Get(UndoBufferReference &entry); - //! Returns the total allocation size for an UpdateInfo entry with max capacity (STANDARD_VECTOR_SIZE) - static idx_t GetAllocSize(idx_t type_size); - //! Returns the total allocation size for an UpdateInfo entry with a specific capacity + //! Returns the total allocation size for a root UpdateInfo entry with a specific capacity static idx_t GetAllocSize(idx_t type_size, idx_t capacity); + //! Returns the total allocation size for the UpdateInfo entry of a transaction + static idx_t GetAllocSize(idx_t type_size, idx_t capacity, idx_t value_capacity); //! Computes a compact capacity for a given count (rounds up with growth headroom) static idx_t GetCompactCapacity(idx_t count); - //! Initialize an UpdateInfo struct that has been allocated using GetAllocSize (i.e. has extra space after it) - static void Initialize(UpdateInfo &info, DuckTableEntry &table_entry, transaction_t transaction_id, - idx_t row_group_start); - //! Initialize with a specific capacity (for compact allocations) + //! Initialize a root UpdateInfo struct that has been allocated using GetAllocSize static void Initialize(UpdateInfo &info, DuckTableEntry &table_entry, transaction_t transaction_id, idx_t row_group_start, idx_t capacity); + //! Initialize the UpdateInfo struct of a transaction that has been allocated using GetAllocSize + static void Initialize(UpdateInfo &info, DuckTableEntry &table_entry, transaction_t transaction_id, + idx_t row_group_start, idx_t capacity, idx_t value_capacity); }; } // namespace duckdb diff --git a/src/duckdb/src/main/config.cpp b/src/duckdb/src/main/config.cpp index 4f2a9f1c0..8f48c7456 100644 --- a/src/duckdb/src/main/config.cpp +++ b/src/duckdb/src/main/config.cpp @@ -108,6 +108,7 @@ static const ConfigurationOption internal_options[] = { DUCKDB_GLOBAL(CustomUserAgentSetting), DUCKDB_SETTING(DebugAsofIejoinSetting), DUCKDB_SETTING_CALLBACK(DebugCheckpointAbortSetting), + DUCKDB_SETTING(DebugCheckpointScanSleepMsSetting), DUCKDB_SETTING(DebugCheckpointSleepMsSetting), DUCKDB_SETTING(DebugDisableOptimizerSetting), DUCKDB_SETTING(EnableCachingOperatorsSetting), diff --git a/src/duckdb/src/main/query_profiler.cpp b/src/duckdb/src/main/query_profiler.cpp index c19c44e26..c3d1ea27e 100644 --- a/src/duckdb/src/main/query_profiler.cpp +++ b/src/duckdb/src/main/query_profiler.cpp @@ -20,6 +20,7 @@ #include "duckdb/main/profiler/gathered_metrics.hpp" #include "duckdb/main/settings.hpp" #include "duckdb/storage/buffer/buffer_pool.hpp" +#include "duckdb/storage/buffer_manager.hpp" #include "duckdb/common/json_document.hpp" #include diff --git a/src/duckdb/src/optimizer/filter_statistics.cpp b/src/duckdb/src/optimizer/filter_statistics.cpp index bbfe23996..578b7b8d2 100644 --- a/src/duckdb/src/optimizer/filter_statistics.cpp +++ b/src/duckdb/src/optimizer/filter_statistics.cpp @@ -57,7 +57,7 @@ void FilterStatisticsOptimizer::Optimize(unique_ptr &plan) { bool filter_bindings_changed = false; optimizer.RunOptimizer(OptimizerType::STATISTICS_PROPAGATION, [&]() { - StatisticsPropagator propagator(optimizer, *plan, StatisticsPropagationMode::FILTER_SIMPLIFICATION); + StatisticsPropagator propagator(optimizer, plan, StatisticsPropagationMode::FILTER_SIMPLIFICATION); propagator.PropagateStatistics(plan); filter_bindings_changed = propagator.FilterBindingsChanged(); }); diff --git a/src/duckdb/src/optimizer/optimizer.cpp b/src/duckdb/src/optimizer/optimizer.cpp index 26408f9bd..edc5d2078 100644 --- a/src/duckdb/src/optimizer/optimizer.cpp +++ b/src/duckdb/src/optimizer/optimizer.cpp @@ -27,6 +27,7 @@ #include "duckdb/optimizer/limit_pushdown.hpp" #include "duckdb/optimizer/regex_range_filter.hpp" #include "duckdb/optimizer/remove_duplicate_groups.hpp" +#include "duckdb/optimizer/remove_redundant_order_keys.hpp" #include "duckdb/optimizer/remove_unused_columns.hpp" #include "duckdb/optimizer/row_group_pruner.hpp" #include "duckdb/optimizer/rule/distinct_aggregate_optimizer.hpp" @@ -372,6 +373,11 @@ void Optimizer::RunBuiltInOptimizers() { plan = join_elimination.Optimize(std::move(plan)); }); + RunOptimizer(OptimizerType::REDUNDANT_ORDER_KEYS, [&]() { + RemoveRedundantOrderKeys remove_redundant_order_keys; + remove_redundant_order_keys.Optimize(*plan); + }); + // rewrites UNNESTs in DelimJoins by moving them to the projection RunOptimizer(OptimizerType::UNNEST_REWRITER, [&]() { UnnestRewriter unnest_rewriter; @@ -455,7 +461,7 @@ void Optimizer::RunBuiltInOptimizers() { bool removed_expressions = false; if (!CTEContainsDML(*plan)) { RunOptimizer(OptimizerType::STATISTICS_PROPAGATION, [&]() { - StatisticsPropagator propagator(*this, *plan); + StatisticsPropagator propagator(*this, plan); propagator.PropagateStatistics(plan); statistics_map = propagator.GetStatisticsMap(); propagated_statistics = true; @@ -466,7 +472,7 @@ void Optimizer::RunBuiltInOptimizers() { MultiStageAggregateRewriter costed_rewriter(*this, AggregateRewritePolicy::COST_BASED, false, statistics_map); costed_rewriter.VisitOperator(plan); if (costed_rewriter.WasChanged()) { - StatisticsPropagator propagator(*this, *plan); + StatisticsPropagator propagator(*this, plan); propagator.PropagateStatistics(plan); statistics_map = propagator.GetStatisticsMap(); removed_expressions |= propagator.HasRemovedExpressions(); diff --git a/src/duckdb/src/optimizer/pushdown/pushdown_aggregate.cpp b/src/duckdb/src/optimizer/pushdown/pushdown_aggregate.cpp index d35277dc3..32d1aeefb 100644 --- a/src/duckdb/src/optimizer/pushdown/pushdown_aggregate.cpp +++ b/src/duckdb/src/optimizer/pushdown/pushdown_aggregate.cpp @@ -100,7 +100,7 @@ unique_ptr FilterPushdown::PushdownAggregate(unique_ptrchildren[0] = child_pushdown.Rewrite(std::move(op->children[0])); - return FinishPushdown(std::move(op)); + return PushFinalFilters(std::move(op)); } } // namespace duckdb diff --git a/src/duckdb/src/optimizer/pushdown/pushdown_window.cpp b/src/duckdb/src/optimizer/pushdown/pushdown_window.cpp index 8f0ad59f3..ddfb40889 100644 --- a/src/duckdb/src/optimizer/pushdown/pushdown_window.cpp +++ b/src/duckdb/src/optimizer/pushdown/pushdown_window.cpp @@ -59,7 +59,7 @@ unique_ptr CreateAuxiliaryFilter(const Expression &partition, const Filt // It must preserve ordering const auto arg_props = partition_func.Function().GetArgProperties(0); - if (!IsKnownMonotonic(arg_props.monotonicity)) { + if (!IsMonotonicIncreasing(arg_props.monotonicity)) { return nullptr; } @@ -196,6 +196,6 @@ unique_ptr FilterPushdown::PushdownWindow(unique_ptrchildren[0] = pushdown.Rewrite(std::move(op->children[0])); filters = std::move(leftover_filters); - return FinishPushdown(std::move(op)); + return PushFinalFilters(std::move(op)); } } // namespace duckdb diff --git a/src/duckdb/src/optimizer/remove_redundant_order_keys.cpp b/src/duckdb/src/optimizer/remove_redundant_order_keys.cpp new file mode 100644 index 000000000..7f5c65f98 --- /dev/null +++ b/src/duckdb/src/optimizer/remove_redundant_order_keys.cpp @@ -0,0 +1,361 @@ +#include "duckdb/optimizer/remove_redundant_order_keys.hpp" + +#include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" +#include "duckdb/parser/constraints/not_null_constraint.hpp" +#include "duckdb/parser/constraints/unique_constraint.hpp" +#include "duckdb/planner/column_binding_map.hpp" +#include "duckdb/planner/expression/bound_columnref_expression.hpp" +#include "duckdb/planner/expression/bound_window_expression.hpp" +#include "duckdb/planner/expression_binder/base_select_binder.hpp" +#include "duckdb/planner/operator/logical_aggregate.hpp" +#include "duckdb/planner/operator/logical_distinct.hpp" +#include "duckdb/planner/operator/logical_get.hpp" +#include "duckdb/planner/operator/logical_order.hpp" +#include "duckdb/planner/operator/logical_projection.hpp" + +namespace duckdb { + +namespace { + +class ApplyFunctionalDependencies : public LogicalOperatorVisitor { +public: + ApplyFunctionalDependencies() { + } + + void VisitOperator(LogicalOperator &op) override; + void VisitExpression(unique_ptr *expression) override; + +private: + void VisitLogicalGet(LogicalGet &get); + void VisitLogicalAggregate(LogicalAggregate &aggr); + void VisitLogicalDistinct(LogicalDistinct &distinct); + void VisitLogicalProjection(LogicalProjection &projection); + void VisitLogicalOrder(LogicalOrder &order); + + void VisitWindowExpression(BoundWindowExpression &wexpr) const; + + void VisitPartitioning(vector> &partitions) const; + void VisitOrderBys(vector &orders) const; + + vector result; +}; + +void ApplyFunctionalDependencies::VisitLogicalGet(LogicalGet &get) { + VisitOperatorChildren(get); + + auto table = get.GetTable(); + if (!table || !table->IsDuckTable() || !get.projected_input.empty() || get.GetColumnIds().empty()) { + return; + } + unordered_map column_bindings; + for (auto &binding : get.GetColumnBindings()) { + auto &column_index = get.GetColumnIndex(binding); + if (!column_index.HasPrimaryIndex() || column_index.HasChildren() || column_index.IsVirtualColumn()) { + continue; + } + column_bindings.emplace(column_index.GetPrimaryIndex(), binding); + } + auto &columns = table->GetColumns(); + auto &constraints = table->GetConstraints(); + unordered_set not_null_columns; + for (auto &constraint : constraints) { + if (constraint->type == ConstraintType::NOT_NULL) { + not_null_columns.insert(constraint->Cast().index.index); + } + } + for (auto &constraint : constraints) { + if (constraint->type != ConstraintType::UNIQUE) { + continue; + } + auto &unique = constraint->Cast(); + auto &names = unique.GetColumnNames(); + if (!unique.HasIndex() && !std::all_of(names.begin(), names.end(), + [&](const Identifier &name) { return columns.ColumnExists(name); })) { + continue; + } + column_binding_set_t key; + bool usable = true; + for (auto &column : unique.GetLogicalIndexes(columns)) { + auto entry = column_bindings.find(column.index); + if (entry == column_bindings.end() || + (!unique.IsPrimaryKey() && not_null_columns.find(column.index) == not_null_columns.end())) { + usable = false; + break; + } + key.insert(entry->second); + } + if (usable && !key.empty()) { + result.push_back(std::move(key)); + } + } +} + +void ApplyFunctionalDependencies::VisitLogicalAggregate(LogicalAggregate &aggr) { + VisitOperatorChildren(aggr); + + // TODO: use child FDs to simplify groupings. + result.clear(); + + if (aggr.groups.empty() || aggr.grouping_sets.size() > 1 || !aggr.grouping_functions.empty()) { + return; + } + column_binding_set_t key; + if (aggr.grouping_sets.empty()) { + for (auto group_idx : ProjectionIndex::GetIndexes(aggr.groups.size())) { + key.insert(ColumnBinding(aggr.group_index, group_idx)); + } + } else { + for (auto group_idx : aggr.grouping_sets[0]) { + if (group_idx.GetIndex() >= aggr.groups.size()) { + return; + } + key.insert(ColumnBinding(aggr.group_index, group_idx)); + } + } + if (!key.empty()) { + result.push_back(std::move(key)); + } +} + +void ApplyFunctionalDependencies::VisitLogicalDistinct(LogicalDistinct &distinct) { + VisitOperatorChildren(distinct); + + // TODO: use child FDs to simplify groupings. + result.clear(); + + column_binding_set_t key; + bool all_columns = true; + for (auto &target : distinct.distinct_targets) { + if (target->GetExpressionType() != ExpressionType::BOUND_COLUMN_REF) { + all_columns = false; + break; + } + key.insert(target->Cast().Binding()); + } + if (all_columns && !key.empty()) { + result.push_back(std::move(key)); + } +} + +void ApplyFunctionalDependencies::VisitLogicalProjection(LogicalProjection &projection) { + VisitOperatorChildren(projection); + + vector child_sets; + child_sets.swap(result); + if (child_sets.empty()) { + return; + } + column_binding_map_t forwarded; + for (auto expr_idx : ProjectionIndex::GetIndexes(projection.expressions.size())) { + auto &expr = *projection.expressions[expr_idx]; + if (expr.GetExpressionType() == ExpressionType::BOUND_COLUMN_REF) { + forwarded.emplace(expr.Cast().Binding(), + ColumnBinding(projection.table_index, expr_idx)); + } + } + for (auto &child_set : child_sets) { + column_binding_set_t key; + bool complete = true; + for (auto &binding : child_set) { + auto entry = forwarded.find(binding); + if (entry == forwarded.end()) { + complete = false; + break; + } + key.insert(entry->second); + } + if (complete) { + result.push_back(std::move(key)); + } + } +} + +void ApplyFunctionalDependencies::VisitLogicalOrder(LogicalOrder &order) { + VisitOperatorChildren(order); + + VisitOrderBys(order.orders); +} + +void ApplyFunctionalDependencies::VisitOrderBys(vector &orders) const { + if (orders.size() < 2) { + return; + } + + // Remove orderings that are FD on the prefix + vector> refs; + refs.emplace_back(*orders[0].expression); + for (idx_t i = 1; i < orders.size();) { + auto &expr = orders[i].expression; + if (BaseSelectBinder::IsFunctionallyDependent(expr, refs)) { + orders.erase(orders.begin() + NumericCast(i)); + continue; + } + refs.emplace_back(*expr); + ++i; + } + + // Remove trailing orderings that are FD on a primary key + if (result.empty()) { + return; + } + + column_binding_set_t prefix; + for (idx_t i = 0; i + 1 < orders.size(); i++) { + auto &expr = *orders[i].expression; + if (expr.GetExpressionType() != ExpressionType::BOUND_COLUMN_REF) { + continue; + } + prefix.insert(expr.Cast().Binding()); + for (auto &unique_set : result) { + bool covered = true; + for (auto &binding : unique_set) { + if (prefix.find(binding) == prefix.end()) { + covered = false; + break; + } + } + if (!covered) { + continue; + } + orders.erase(orders.begin() + NumericCast(i + 1), orders.end()); + return; + } + } +} + +void ApplyFunctionalDependencies::VisitPartitioning(vector> &partitions) const { + // Remove partitioning keys that are FD on the others + for (idx_t i = 0; i < partitions.size();) { + vector> refs; + for (idx_t j = 0; j < partitions.size(); ++j) { + if (i != j) { + refs.emplace_back(*partitions[j]); + } + } + if (BaseSelectBinder::IsFunctionallyDependent(partitions[i], refs)) { + partitions.erase(partitions.begin() + NumericCast(i)); + } else { + ++i; + } + } + + // Extract the partitioning references + column_binding_set_t partition_bindings; + for (auto &expr : partitions) { + if (expr->GetExpressionType() != ExpressionType::BOUND_COLUMN_REF) { + continue; + } + partition_bindings.insert(expr->Cast().Binding()); + } + + // Find the smallest unique set that is covered by the bindings + optional_idx smallest; + for (idx_t i = 0; i < result.size(); ++i) { + auto &unique_set = result.at(i); + bool covered = true; + for (auto &binding : unique_set) { + if (!partition_bindings.count(binding)) { + covered = false; + break; + } + } + if (!covered) { + continue; + } + if (!smallest.IsValid() || unique_set.size() < result.at(smallest.GetIndex()).size()) { + smallest = i; + } + } + if (smallest.IsValid()) { + // Replace the partitioning with the unique bindings + auto &unique_set = result.at(smallest.GetIndex()); + vector> reduced; + for (auto &expr : partitions) { + if (expr->GetExpressionType() != ExpressionType::BOUND_COLUMN_REF) { + continue; + } + const auto &ref = expr->Cast(); + if (!unique_set.count(ref.Binding())) { + continue; + } + reduced.emplace_back(ref.Copy()); + } + std::swap(partitions, reduced); + } +} + +void ApplyFunctionalDependencies::VisitWindowExpression(BoundWindowExpression &wexpr) const { + // Equivalence 1: Reduce partitioning to minimal functional dependency + VisitPartitioning(wexpr.PartitionsMutable()); + + // Equivalence 2: Truncate ordering at minimal functional dependency + VisitOrderBys(wexpr.OrderByMutable()); + + // Equivalence 3: Remove a single ORDER BY if it is FD on the partitioning + // Note that this implies that the ordering expression is constant on the partition, + // so there is only one peer group, which is the same as having no ORDER BY clause. + auto &partition_bys = wexpr.Partitions(); + auto &order_bys = wexpr.OrderByMutable(); + if (!partition_bys.empty() && order_bys.size() == 1) { + vector> refs; + for (auto &arg : partition_bys) { + refs.emplace_back(*arg); + } + if (BaseSelectBinder::IsFunctionallyDependent(order_bys[0].expression, refs)) { + order_bys.clear(); + } + } +} + +void ApplyFunctionalDependencies::VisitExpression(unique_ptr *expression) { + switch ((*expression)->GetExpressionClass()) { + case ExpressionClass::BOUND_WINDOW: + VisitWindowExpression((*expression)->Cast()); + break; + default: + break; + } +} + +void ApplyFunctionalDependencies::VisitOperator(LogicalOperator &op) { + switch (op.type) { + case LogicalOperatorType::LOGICAL_ORDER_BY: + VisitLogicalOrder(op.Cast()); + break; + case LogicalOperatorType::LOGICAL_FILTER: + case LogicalOperatorType::LOGICAL_LIMIT: + VisitOperatorChildren(op); + break; + case LogicalOperatorType::LOGICAL_WINDOW: + VisitOperatorChildren(op); + VisitOperatorExpressions(op); + // Windowing only adds bindings + break; + case LogicalOperatorType::LOGICAL_PROJECTION: + VisitLogicalProjection(op.Cast()); + break; + case LogicalOperatorType::LOGICAL_GET: + VisitLogicalGet(op.Cast()); + break; + case LogicalOperatorType::LOGICAL_AGGREGATE_AND_GROUP_BY: + VisitLogicalAggregate(op.Cast()); + break; + case LogicalOperatorType::LOGICAL_DISTINCT: + VisitLogicalDistinct(op.Cast()); + break; + default: + VisitOperatorChildren(op); + // Clear the keys because we don't know if the operator preserves them. + result.clear(); + break; + } +} + +} // namespace + +void RemoveRedundantOrderKeys::Optimize(LogicalOperator &op) { + ApplyFunctionalDependencies visitor; + visitor.VisitOperator(op); +} + +} // namespace duckdb diff --git a/src/duckdb/src/optimizer/rule/list_comprehension_rewrite.cpp b/src/duckdb/src/optimizer/rule/list_comprehension_rewrite.cpp index 0367106cc..ee4356164 100644 --- a/src/duckdb/src/optimizer/rule/list_comprehension_rewrite.cpp +++ b/src/duckdb/src/optimizer/rule/list_comprehension_rewrite.cpp @@ -43,14 +43,6 @@ bool IsStructPack(const BoundFunctionExpression &expr) { return expr.Function().GetName() == "struct_pack"; } -optional_ptr UnwrapCasts(optional_ptr expr) { - while (BoundCastExpression::IsCast(*expr)) { - auto &cast_expr = expr->Cast(); - expr = BoundCastExpression::ChildMutable(cast_expr).get(); - } - return expr; -} - optional_ptr FindStructPackChildByName(BoundFunctionExpression &struct_pack, const string &name) { D_ASSERT(struct_pack.GetReturnType().id() == LogicalTypeId::STRUCT); for (auto &child : struct_pack.GetChildren()) { @@ -95,8 +87,16 @@ void RemoveIndexInputSlot(unique_ptr &expr) { }); } -bool MatchesStructFieldProjection(Expression &expr, const string &field_name) { - auto base = UnwrapCasts(expr); +bool MatchesStructFieldProjection(Expression &expr, const string &field_name, bool allow_boolean_cast = false) { + optional_ptr base = expr; + if (allow_boolean_cast && BoundCastExpression::IsCast(*base) && base->GetReturnType() == LogicalType::BOOLEAN) { + auto &cast_expr = base->Cast(); + if (BoundCastExpression::IsTryCast(cast_expr)) { + return false; + } + // The rewritten filter recreates this single BOOLEAN cast. + base = BoundCastExpression::ChildMutable(cast_expr).get(); + } if (base->GetExpressionClass() != ExpressionClass::BOUND_FUNCTION) { return false; } @@ -105,7 +105,7 @@ bool MatchesStructFieldProjection(Expression &expr, const string &field_name) { return false; } - auto struct_arg = UnwrapCasts(*extract_expr.GetChildren()[0]); + auto &struct_arg = extract_expr.GetChildren()[0]; if (struct_arg->GetExpressionClass() != ExpressionClass::BOUND_REF) { return false; } @@ -114,7 +114,7 @@ bool MatchesStructFieldProjection(Expression &expr, const string &field_name) { return false; } - auto field_arg = UnwrapCasts(*extract_expr.GetChildren()[1]); + auto &field_arg = extract_expr.GetChildren()[1]; if (field_arg->GetExpressionClass() != ExpressionClass::BOUND_CONSTANT) { return false; } @@ -198,7 +198,7 @@ unique_ptr MatchListComprehensionRewrite(ClientContext & if (!list_filter_bind.lambda_expr || !root_bind.lambda_expr) { return nullptr; } - if (!MatchesStructFieldProjection(*list_filter_bind.lambda_expr, "filter") || + if (!MatchesStructFieldProjection(*list_filter_bind.lambda_expr, "filter", true) || !MatchesStructFieldProjection(*root_bind.lambda_expr, "result")) { return nullptr; } @@ -216,7 +216,7 @@ unique_ptr MatchListComprehensionRewrite(ClientContext & if (!inner_bind.lambda_expr) { return nullptr; } - auto inner_base = UnwrapCasts(*inner_bind.lambda_expr); + auto &inner_base = inner_bind.lambda_expr; if (inner_base->GetExpressionClass() != ExpressionClass::BOUND_FUNCTION) { return nullptr; } diff --git a/src/duckdb/src/optimizer/statistics/operator/propagate_aggregate.cpp b/src/duckdb/src/optimizer/statistics/operator/propagate_aggregate.cpp index 23b10cbf4..4de83ed3c 100644 --- a/src/duckdb/src/optimizer/statistics/operator/propagate_aggregate.cpp +++ b/src/duckdb/src/optimizer/statistics/operator/propagate_aggregate.cpp @@ -533,9 +533,14 @@ unique_ptr StatisticsPropagator::PropagateStatistics(LogicalAggr } aggr.distinct_validity = distinct_validity; + const auto is_ungrouped = aggr.groups.empty(); // after we propagate statistics - try to directly execute aggregates using statistics TryExecuteAggregates(aggr, node_ptr); + if (is_ungrouped) { + // Ungrouped aggregate always returns exactly one row, even over empty input + return make_uniq(1U, 1U); + } // the max cardinality of an aggregate is the max cardinality of the input (i.e. when every row is a unique // group) return std::move(node_stats); diff --git a/src/duckdb/src/optimizer/statistics_propagator.cpp b/src/duckdb/src/optimizer/statistics_propagator.cpp index 8d200e6f4..db3d72baa 100644 --- a/src/duckdb/src/optimizer/statistics_propagator.cpp +++ b/src/duckdb/src/optimizer/statistics_propagator.cpp @@ -26,9 +26,9 @@ namespace duckdb { -StatisticsPropagator::StatisticsPropagator(Optimizer &optimizer_p, LogicalOperator &root_p, +StatisticsPropagator::StatisticsPropagator(Optimizer &optimizer_p, unique_ptr &root_p, StatisticsPropagationMode mode_p) - : optimizer(optimizer_p), context(optimizer.context), mode(mode_p), root(&root_p) { + : optimizer(optimizer_p), context(optimizer.context), mode(mode_p), root(root_p) { root->ResolveOperatorTypes(); } diff --git a/src/duckdb/src/optimizer/window_self_join.cpp b/src/duckdb/src/optimizer/window_self_join.cpp index 5cae25f20..670f0135d 100644 --- a/src/duckdb/src/optimizer/window_self_join.cpp +++ b/src/duckdb/src/optimizer/window_self_join.cpp @@ -1,8 +1,10 @@ #include "duckdb/optimizer/window_self_join.hpp" #include "duckdb/optimizer/optimizer.hpp" #include "duckdb/planner/binder.hpp" +#include "duckdb/planner/operator/logical_prepare.hpp" #include "duckdb/planner/operator/logical_window.hpp" #include "duckdb/planner/operator/logical_comparison_join.hpp" +#include "duckdb/planner/operator/logical_cross_product.hpp" #include "duckdb/planner/operator/logical_aggregate.hpp" #include "duckdb/planner/expression/bound_window_expression.hpp" #include "duckdb/planner/expression/bound_columnref_expression.hpp" @@ -156,9 +158,6 @@ bool WindowSelfJoinOptimizer::CanOptimize(const BoundWindowExpression &w_expr, default: return false; } - if (w_expr.Partitions().empty()) { - return false; - } if (w_expr.WindowExclude() != WindowExcludeMode::NO_OTHER) { return false; } @@ -193,6 +192,9 @@ bool WindowSelfJoinOptimizer::CanOptimize(const LogicalOperator &op) { unique_ptr WindowSelfJoinOptimizer::OptimizeInternal(unique_ptr op, ColumnBindingReplacer &replacer) { + if (op->type == LogicalOperatorType::LOGICAL_PREPARE) { + parameter_data = op->Cast().prepared->value_map; + } if (op->type == LogicalOperatorType::LOGICAL_WINDOW) { auto &window = op->Cast(); @@ -222,7 +224,7 @@ unique_ptr WindowSelfJoinOptimizer::OptimizeInternal(unique_ptr unique_ptr copy_child; // Reuse the LogicalOperatorDeepCopy from CTE inlining, as the copying requirements are similar (copying an // operator subtree and replacing table indices) - LogicalOperatorDeepCopy deep_copy(optimizer.binder, nullptr); + LogicalOperatorDeepCopy deep_copy(optimizer.binder, parameter_data); try { copy_child = deep_copy.DeepCopy(window.children[0]); } catch (std::exception &ex) { @@ -273,18 +275,23 @@ unique_ptr WindowSelfJoinOptimizer::OptimizeInternal(unique_ptr throw InternalException("LogicalAggregate types size mismatch"); } - // Inner Join on the partition keys - auto join = make_uniq(JoinType::INNER); - for (size_t i = 0; i < partitions.size(); ++i) { - auto left_expr = partitions[i]->Copy(); - auto right_expr = make_uniq(partitions[i]->GetReturnType(), - ColumnBinding(group_index, ProjectionIndex(i))); - join->conditions.push_back( - JoinCondition(std::move(left_expr), std::move(right_expr), ExpressionType::COMPARE_NOT_DISTINCT_FROM)); + // Inner Join on the partition keys, or cross join against the aggregate + unique_ptr join; + if (partitions.empty()) { + join = LogicalCrossProduct::Create(std::move(original_child), std::move(agg_op)); + } else { + auto cjoin = make_uniq(JoinType::INNER); + for (size_t i = 0; i < partitions.size(); ++i) { + auto left_expr = partitions[i]->Copy(); + auto right_expr = make_uniq(partitions[i]->GetReturnType(), + ColumnBinding(group_index, ProjectionIndex(i))); + cjoin->conditions.push_back(JoinCondition(std::move(left_expr), std::move(right_expr), + ExpressionType::COMPARE_NOT_DISTINCT_FROM)); + } + join = std::move(cjoin); + join->children.push_back(std::move(original_child)); + join->children.push_back(std::move(agg_op)); } - - join->children.push_back(std::move(original_child)); - join->children.push_back(std::move(agg_op)); join->ResolveOperatorTypes(); // Replace aggregate bindings @@ -296,7 +303,7 @@ unique_ptr WindowSelfJoinOptimizer::OptimizeInternal(unique_ptr replacer.replacement_bindings.emplace_back(old_binding, new_binding); } - return std::move(join); + return join; } else if (!op->children.empty()) { for (auto &child : op->children) { child = OptimizeInternal(std::move(child), replacer); diff --git a/src/duckdb/src/parser/constraints/unique_constraint.cpp b/src/duckdb/src/parser/constraints/unique_constraint.cpp index 4d4c6fbe0..63b48c5c0 100644 --- a/src/duckdb/src/parser/constraints/unique_constraint.cpp +++ b/src/duckdb/src/parser/constraints/unique_constraint.cpp @@ -7,22 +7,23 @@ namespace duckdb { UniqueConstraint::UniqueConstraint() : Constraint(ConstraintType::UNIQUE), index(DConstants::INVALID_INDEX), is_primary_key(false), - timing(ConstraintTiming::DEFAULT) { + check_mode(ConstraintCheckMode::DEFAULT) { } -UniqueConstraint::UniqueConstraint(const LogicalIndex index, const bool is_primary_key, ConstraintTiming timing) - : Constraint(ConstraintType::UNIQUE), index(index), is_primary_key(is_primary_key), timing(timing) { +UniqueConstraint::UniqueConstraint(const LogicalIndex index, const bool is_primary_key, ConstraintCheckMode check_mode) + : Constraint(ConstraintType::UNIQUE), index(index), is_primary_key(is_primary_key), check_mode(check_mode) { } UniqueConstraint::UniqueConstraint(const LogicalIndex index, Identifier column_name_p, const bool is_primary_key, - ConstraintTiming timing) - : UniqueConstraint(index, is_primary_key, timing) { + ConstraintCheckMode check_mode) + : UniqueConstraint(index, is_primary_key, check_mode) { columns.emplace_back(std::move(column_name_p)); } -UniqueConstraint::UniqueConstraint(vector columns, const bool is_primary_key, ConstraintTiming timing) +UniqueConstraint::UniqueConstraint(vector columns, const bool is_primary_key, + ConstraintCheckMode check_mode) : Constraint(ConstraintType::UNIQUE), index(DConstants::INVALID_INDEX), columns(std::move(columns)), - is_primary_key(is_primary_key), timing(timing) { + is_primary_key(is_primary_key), check_mode(check_mode) { } string UniqueConstraint::ToString() const { @@ -34,18 +35,18 @@ string UniqueConstraint::ToString() const { base += SQLIdentifier(columns[i]); } base += ")"; - if (timing != ConstraintTiming::DEFAULT) { - base += " " + EnumUtil::ToString(timing); + if (check_mode != ConstraintCheckMode::DEFAULT) { + base += " " + EnumUtil::ToString(check_mode); } return base; } unique_ptr UniqueConstraint::Copy() const { if (!HasIndex()) { - return make_uniq(columns, is_primary_key, timing); + return make_uniq(columns, is_primary_key, check_mode); } - return make_uniq(index, columns.empty() ? Identifier() : columns[0], is_primary_key, timing); + return make_uniq(index, columns.empty() ? Identifier() : columns[0], is_primary_key, check_mode); } bool UniqueConstraint::IsPrimaryKey() const { @@ -53,7 +54,7 @@ bool UniqueConstraint::IsPrimaryKey() const { } bool UniqueConstraint::IsDeferred() const { - return timing == ConstraintTiming::DEFERRED; + return check_mode == ConstraintCheckMode::DEFERRED; } IndexConstraintType UniqueConstraint::GetIndexConstraintType() const { diff --git a/src/duckdb/src/parser/parsed_data/alter_info.cpp b/src/duckdb/src/parser/parsed_data/alter_info.cpp index c8093c416..38f57252a 100644 --- a/src/duckdb/src/parser/parsed_data/alter_info.cpp +++ b/src/duckdb/src/parser/parsed_data/alter_info.cpp @@ -21,6 +21,23 @@ AlterEntryData AlterInfo::GetAlterEntryData() const { return AlterEntryData(GetQualifiedName(), if_not_found); } +DatabaseModificationType AlterInfo::GetModifications() const { + if (type != AlterType::ALTER_TABLE) { + return DatabaseModificationType(); + } + switch (Cast().alter_table_type) { + case AlterTableType::RENAME_COLUMN: + case AlterTableType::RENAME_TABLE: + case AlterTableType::SET_DEFAULT: + case AlterTableType::DROP_NOT_NULL: + case AlterTableType::SET_COLUMN_COMMENT: + // the new version of the table keeps the storage of the old one + return DatabaseModificationType(); + default: + return DatabaseModificationType::ALTER_TABLE; + } +} + bool AlterInfo::IsAddUniqueConstraint() const { if (type != AlterType::ALTER_TABLE) { return false; diff --git a/src/duckdb/src/parser/parsed_data/create_info.cpp b/src/duckdb/src/parser/parsed_data/create_info.cpp index f59cbf30b..2f55a8a6b 100644 --- a/src/duckdb/src/parser/parsed_data/create_info.cpp +++ b/src/duckdb/src/parser/parsed_data/create_info.cpp @@ -6,6 +6,23 @@ namespace duckdb { +DatabaseModificationType CreateInfo::GetModifications() const { + switch (type) { + case CatalogType::TABLE_ENTRY: + case CatalogType::INDEX_ENTRY: + case CatalogType::TRIGGER_ENTRY: + return DatabaseModificationType::CREATE_CATALOG_ENTRY; + case CatalogType::SCHEMA_ENTRY: + // replacing a schema drops the existing one + if (on_conflict == OnCreateConflict::REPLACE_ON_CONFLICT) { + return DatabaseModificationType::CREATE_CATALOG_ENTRY; + } + return DatabaseModificationType(); + default: + return DatabaseModificationType(); + } +} + void CreateInfo::CopyProperties(CreateInfo &other) const { other.type = type; other.SetQualifiedName(GetQualifiedName()); diff --git a/src/duckdb/src/parser/parsed_data/drop_info.cpp b/src/duckdb/src/parser/parsed_data/drop_info.cpp index 56ae87cf5..656e227b3 100644 --- a/src/duckdb/src/parser/parsed_data/drop_info.cpp +++ b/src/duckdb/src/parser/parsed_data/drop_info.cpp @@ -14,6 +14,14 @@ DropInfo::DropInfo(const DropInfo &info) qualified_name(info.qualified_name) { } +DatabaseModificationType DropInfo::GetModifications() const { + if (type == CatalogType::TABLE_ENTRY || type == CatalogType::INDEX_ENTRY || cascade) { + return DatabaseModificationType::DROP_CATALOG_ENTRY; + } + // a schema is only dropped without CASCADE when it is empty + return DatabaseModificationType(); +} + unique_ptr DropInfo::Copy() const { return make_uniq(*this); } diff --git a/src/duckdb/src/parser/peg/transformer/transform_create_table.cpp b/src/duckdb/src/parser/peg/transformer/transform_create_table.cpp index e6ff456b0..1164222b8 100644 --- a/src/duckdb/src/parser/peg/transformer/transform_create_table.cpp +++ b/src/duckdb/src/parser/peg/transformer/transform_create_table.cpp @@ -170,7 +170,7 @@ PEGTransformerFactory::TransformCreateTableColumnList(PEGTransformer &transforme } else if (constraint_type.type == ConstraintType::UNIQUE) { result.constraints.push_back( make_uniq(LogicalIndex(col_idx), column_result.column_definition.GetName(), - constraint_type.is_primary_key, constraint_type.timing)); + constraint_type.is_primary_key, constraint_type.check_mode)); } } result.columns.AddColumn(std::move(column_result.column_definition)); @@ -265,6 +265,10 @@ ConstraintColumnDefinition PEGTransformerFactory::TransformColumnDefinition( } else if (cc_entry.constraint_name == "ForeignKeyConstraint") { auto &fk_constraint = cc_entry.constraint->Cast(); fk_constraint.fk_columns.push_back(qualified_name.Name()); + if (!fk_constraint.pk_columns.empty() && fk_constraint.pk_columns.size() != 1) { + throw ParserException( + "The number of referencing and referenced columns for foreign keys must be the same"); + } accumulated_constraints.constraints.push_back(std::move(cc_entry.constraint)); } else if (cc_entry.constraint_name == "ColumnCollation") { if (has_generated) { @@ -361,16 +365,16 @@ unique_ptr PEGTransformerFactory::TransformTopCheckConstraint(PEGTra unique_ptr PEGTransformerFactory::TransformTopPrimaryKeyConstraint(PEGTransformer &transformer, const vector &column_id_list, - const optional &constraint_timing) { - auto timing = constraint_timing.value_or(ConstraintTiming::DEFAULT); - return make_uniq(StringsToIdentifiers(column_id_list), true, timing); + const optional &constraint_timing) { + auto check_mode = constraint_timing.value_or(ConstraintCheckMode::DEFAULT); + return make_uniq(StringsToIdentifiers(column_id_list), true, check_mode); } unique_ptr PEGTransformerFactory::TransformTopUniqueConstraint(PEGTransformer &transformer, const vector &column_id_list, - const optional &constraint_timing) { - auto timing = constraint_timing.value_or(ConstraintTiming::DEFAULT); - return make_uniq(StringsToIdentifiers(column_id_list), false, timing); + const optional &constraint_timing) { + auto check_mode = constraint_timing.value_or(ConstraintCheckMode::DEFAULT); + return make_uniq(StringsToIdentifiers(column_id_list), false, check_mode); } ColumnConstraintEntry PEGTransformerFactory::TransformCheckConstraint(PEGTransformer &transformer, @@ -470,28 +474,30 @@ string PEGTransformerFactory::TransformSetDefaultKeyAction(PEGTransformer &trans ColumnConstraintEntry PEGTransformerFactory::TransformPrimaryKeyConstraint(PEGTransformer &transformer, - const optional &constraint_timing) { + const optional &constraint_timing) { ColumnConstraintEntry entry; entry.constraint_name = "PrimaryKeyConstraint"; - entry.constraint_type_info = {true, ConstraintType::UNIQUE, constraint_timing.value_or(ConstraintTiming::DEFAULT)}; + entry.constraint_type_info = {true, ConstraintType::UNIQUE, + constraint_timing.value_or(ConstraintCheckMode::DEFAULT)}; return entry; } ColumnConstraintEntry PEGTransformerFactory::TransformUniqueConstraint(PEGTransformer &transformer, - const optional &constraint_timing) { + const optional &constraint_timing) { ColumnConstraintEntry entry; entry.constraint_name = "UniqueConstraint"; - entry.constraint_type_info = {false, ConstraintType::UNIQUE, constraint_timing.value_or(ConstraintTiming::DEFAULT)}; + entry.constraint_type_info = {false, ConstraintType::UNIQUE, + constraint_timing.value_or(ConstraintCheckMode::DEFAULT)}; return entry; } -ConstraintTiming PEGTransformerFactory::TransformImmediateConstraint(PEGTransformer &transformer) { - return ConstraintTiming::IMMEDIATE; +ConstraintCheckMode PEGTransformerFactory::TransformImmediateConstraint(PEGTransformer &transformer) { + return ConstraintCheckMode::IMMEDIATE; } -ConstraintTiming PEGTransformerFactory::TransformDeferredConstraint(PEGTransformer &transformer) { - return ConstraintTiming::DEFERRED; +ConstraintCheckMode PEGTransformerFactory::TransformDeferredConstraint(PEGTransformer &transformer) { + return ConstraintCheckMode::DEFERRED; } bool PEGTransformerFactory::TransformNullConstraint(PEGTransformer &transformer) { @@ -507,7 +513,7 @@ ColumnConstraintEntry PEGTransformerFactory::TransformNotNullConstraint(PEGTrans ColumnConstraintEntry entry; entry.constraint_name = "NotNullConstraint"; entry.constraint_type_info = {false, child ? ConstraintType::NOT_NULL : ConstraintType::INVALID, - ConstraintTiming::DEFAULT}; + ConstraintCheckMode::DEFAULT}; return entry; } diff --git a/src/duckdb/src/parser/peg/transformer/transform_generated.cpp b/src/duckdb/src/parser/peg/transformer/transform_generated.cpp index 9f601ea14..f9efedb65 100644 --- a/src/duckdb/src/parser/peg/transformer/transform_generated.cpp +++ b/src/duckdb/src/parser/peg/transformer/transform_generated.cpp @@ -3097,10 +3097,10 @@ PEGTransformerFactory::TransformNotNullColumnConstraintInternal(PEGTransformer & unique_ptr PEGTransformerFactory::TransformUniqueConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result) { auto &list_pr = parse_result.Cast(); - optional constraint_timing {}; + optional constraint_timing {}; auto &constraint_timing_opt = list_pr.GetChild(1).Cast(); if (constraint_timing_opt.HasResult()) { - auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); + auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); constraint_timing = constraint_timing_value; } auto result = TransformUniqueConstraint(transformer, constraint_timing); @@ -3110,10 +3110,10 @@ unique_ptr PEGTransformerFactory::TransformUniqueConstrain unique_ptr PEGTransformerFactory::TransformPrimaryKeyConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result) { auto &list_pr = parse_result.Cast(); - optional constraint_timing {}; + optional constraint_timing {}; auto &constraint_timing_opt = list_pr.GetChild(2).Cast(); if (constraint_timing_opt.HasResult()) { - auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); + auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); constraint_timing = constraint_timing_value; } auto result = TransformPrimaryKeyConstraint(transformer, constraint_timing); @@ -3124,20 +3124,20 @@ unique_ptr PEGTransformerFactory::TransformConstraintTimin ParseResult &parse_result) { auto &list_pr = parse_result.Cast(); auto &choice_pr = list_pr.Child(0); - auto result = transformer.Transform(choice_pr.GetResult()); - return make_uniq>(result); + auto result = transformer.Transform(choice_pr.GetResult()); + return make_uniq>(result); } unique_ptr PEGTransformerFactory::TransformImmediateConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result) { auto result = TransformImmediateConstraint(transformer); - return make_uniq>(result); + return make_uniq>(result); } unique_ptr PEGTransformerFactory::TransformDeferredConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result) { auto result = TransformDeferredConstraint(transformer); - return make_uniq>(result); + return make_uniq>(result); } unique_ptr PEGTransformerFactory::TransformDefaultValueInternal(PEGTransformer &transformer, @@ -3304,10 +3304,10 @@ PEGTransformerFactory::TransformTopPrimaryKeyConstraintInternal(PEGTransformer & ParseResult &parse_result) { auto &list_pr = parse_result.Cast(); auto column_id_list = transformer.Transform>(list_pr.GetChild(2)); - optional constraint_timing {}; + optional constraint_timing {}; auto &constraint_timing_opt = list_pr.GetChild(3).Cast(); if (constraint_timing_opt.HasResult()) { - auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); + auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); constraint_timing = constraint_timing_value; } auto result = TransformTopPrimaryKeyConstraint(transformer, column_id_list, constraint_timing); @@ -3318,10 +3318,10 @@ unique_ptr PEGTransformerFactory::TransformTopUniqueConstraintInternal(PEGTransformer &transformer, ParseResult &parse_result) { auto &list_pr = parse_result.Cast(); auto column_id_list = transformer.Transform>(list_pr.GetChild(1)); - optional constraint_timing {}; + optional constraint_timing {}; auto &constraint_timing_opt = list_pr.GetChild(2).Cast(); if (constraint_timing_opt.HasResult()) { - auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); + auto constraint_timing_value = transformer.Transform(constraint_timing_opt.GetResult()); constraint_timing = constraint_timing_value; } auto result = TransformTopUniqueConstraint(transformer, column_id_list, constraint_timing); diff --git a/src/duckdb/src/parser/peg/transformer/transform_generated_trampoline.cpp b/src/duckdb/src/parser/peg/transformer/transform_generated_trampoline.cpp index 725ad8f66..67dcd838b 100644 --- a/src/duckdb/src/parser/peg/transformer/transform_generated_trampoline.cpp +++ b/src/duckdb/src/parser/peg/transformer/transform_generated_trampoline.cpp @@ -10083,9 +10083,9 @@ void PEGTransformerFactory::InitializeUniqueConstraintTrampoline(PEGTransformer unique_ptr PEGTransformerFactory::FinalizeUniqueConstraintTrampoline(PEGTransformer &transformer, GeneratedTransformProcess &process) { - optional constraint_timing {}; + optional constraint_timing {}; if (process.child_results[0]) { - constraint_timing = process.TakeResult(0); + constraint_timing = process.TakeResult(0); } auto result = TransformUniqueConstraint(transformer, constraint_timing); return make_uniq>(std::move(result)); @@ -10104,9 +10104,9 @@ void PEGTransformerFactory::InitializePrimaryKeyConstraintTrampoline(PEGTransfor unique_ptr PEGTransformerFactory::FinalizePrimaryKeyConstraintTrampoline(PEGTransformer &transformer, GeneratedTransformProcess &process) { - optional constraint_timing {}; + optional constraint_timing {}; if (process.child_results[0]) { - constraint_timing = process.TakeResult(0); + constraint_timing = process.TakeResult(0); } auto result = TransformPrimaryKeyConstraint(transformer, constraint_timing); return make_uniq>(std::move(result)); @@ -10129,8 +10129,8 @@ void PEGTransformerFactory::InitializeConstraintTimingTrampoline(PEGTransformer unique_ptr PEGTransformerFactory::FinalizeConstraintTimingTrampoline(PEGTransformer &transformer, GeneratedTransformProcess &process) { - auto result = process.TakeResult(0); - return make_uniq>(result); + auto result = process.TakeResult(0); + return make_uniq>(result); } void PEGTransformerFactory::InitializeImmediateConstraintTrampoline(PEGTransformer &transformer, @@ -10142,7 +10142,7 @@ unique_ptr PEGTransformerFactory::FinalizeImmediateConstraintTrampoline(PEGTransformer &transformer, GeneratedTransformProcess &process) { auto result = TransformImmediateConstraint(transformer); - return make_uniq>(result); + return make_uniq>(result); } void PEGTransformerFactory::InitializeDeferredConstraintTrampoline(PEGTransformer &transformer, @@ -10154,7 +10154,7 @@ unique_ptr PEGTransformerFactory::FinalizeDeferredConstraintTrampoline(PEGTransformer &transformer, GeneratedTransformProcess &process) { auto result = TransformDeferredConstraint(transformer); - return make_uniq>(result); + return make_uniq>(result); } void PEGTransformerFactory::InitializeDefaultValueTrampoline(PEGTransformer &transformer, @@ -10467,9 +10467,9 @@ unique_ptr PEGTransformerFactory::FinalizeTopPrimaryKeyConstraintTrampoline(PEGTransformer &transformer, GeneratedTransformProcess &process) { auto column_id_list = process.TakeResult>(0); - optional constraint_timing {}; + optional constraint_timing {}; if (process.child_results[1]) { - constraint_timing = process.TakeResult(1); + constraint_timing = process.TakeResult(1); } auto result = TransformTopPrimaryKeyConstraint(transformer, column_id_list, constraint_timing); return make_uniq>>(std::move(result)); @@ -10490,9 +10490,9 @@ unique_ptr PEGTransformerFactory::FinalizeTopUniqueConstraintTrampoline(PEGTransformer &transformer, GeneratedTransformProcess &process) { auto column_id_list = process.TakeResult>(0); - optional constraint_timing {}; + optional constraint_timing {}; if (process.child_results[1]) { - constraint_timing = process.TakeResult(1); + constraint_timing = process.TakeResult(1); } auto result = TransformTopUniqueConstraint(transformer, column_id_list, constraint_timing); return make_uniq>>(std::move(result)); diff --git a/src/duckdb/src/planner/binder/statement/bind_alter.cpp b/src/duckdb/src/planner/binder/statement/bind_alter.cpp index 93fc10ee4..725915d7a 100644 --- a/src/duckdb/src/planner/binder/statement/bind_alter.cpp +++ b/src/duckdb/src/planner/binder/statement/bind_alter.cpp @@ -40,7 +40,7 @@ BoundStatement Binder::BindAlterAddIndex(BoundStatement &result, CatalogEntry &e auto bound_constraint = BindUniqueConstraint(*constraint_info.constraint, table_info.GetQualifiedName().Name(), column_list); - VerifyConstraintTimingStorageVersion(*constraint_info.constraint, table.ParentCatalog(), table.temporary); + VerifyConstraintCheckModeStorageVersion(*constraint_info.constraint, table.ParentCatalog(), table.temporary); auto &bound_unique = bound_constraint->Cast(); auto &unique_constraint = constraint_info.constraint->Cast(); @@ -157,7 +157,7 @@ BoundStatement Binder::Bind(AlterStatement &stmt) { } if (!entry->temporary) { // We can only alter temporary tables and views in read-only mode. - properties.RegisterDBModify(catalog, context, DatabaseModificationType::ALTER_TABLE); + properties.RegisterDBModify(catalog, context, stmt.info->GetModifications()); } stmt.info->SetQualifiedName(entry->ParentSchema().GetQualifiedName(stmt.info->GetQualifiedName().Name())); diff --git a/src/duckdb/src/planner/binder/statement/bind_create.cpp b/src/duckdb/src/planner/binder/statement/bind_create.cpp index 079242fbf..df25bd1d1 100644 --- a/src/duckdb/src/planner/binder/statement/bind_create.cpp +++ b/src/duckdb/src/planner/binder/statement/bind_create.cpp @@ -317,7 +317,7 @@ SchemaCatalogEntry &Binder::BindSchema(CreateInfo &info) { D_ASSERT(schema_obj.type == CatalogType::SCHEMA_ENTRY); if (!info.temporary) { auto &properties = GetStatementProperties(); - properties.RegisterDBModify(schema_obj.catalog, context, DatabaseModificationType::CREATE_CATALOG_ENTRY); + properties.RegisterDBModify(schema_obj.catalog, context, info.GetModifications()); } return schema_obj; } @@ -849,7 +849,7 @@ BoundStatement Binder::Bind(CreateStatement &stmt) { auto &info = stmt.info->Cast(); BindCreateSchema(info); properties.RegisterDBModify(Catalog::GetCatalog(context, info.SchemaCatalog()), context, - DatabaseModificationType::CREATE_CATALOG_ENTRY); + info.GetModifications()); result.plan = make_uniq(LogicalOperatorType::LOGICAL_CREATE_SCHEMA, std::move(stmt.info)); break; } diff --git a/src/duckdb/src/planner/binder/statement/bind_create_table.cpp b/src/duckdb/src/planner/binder/statement/bind_create_table.cpp index cd8f9988d..70f1777e1 100644 --- a/src/duckdb/src/planner/binder/statement/bind_create_table.cpp +++ b/src/duckdb/src/planner/binder/statement/bind_create_table.cpp @@ -167,27 +167,24 @@ unique_ptr BindCheckConstraint(Binder &binder, const Constraint return std::move(bound_constraint); } -void Binder::VerifyConstraintTimingStorageVersion(const Constraint &constraint, Catalog &catalog, bool temporary) { +void Binder::VerifyConstraintCheckModeStorageVersion(const Constraint &constraint, Catalog &catalog, bool temporary) { if (constraint.type != ConstraintType::UNIQUE || temporary || !catalog.IsDuckCatalog() || catalog.InMemory()) { return; } auto &unique = constraint.Cast(); - if (unique.timing == ConstraintTiming::DEFAULT) { + if (unique.check_mode == ConstraintCheckMode::DEFAULT) { return; } if (StorageManager::Get(catalog).GetStorageVersion() < StorageVersion::V2_0_0) { - throw BinderException("Explicit constraint timing is only supported for storage versions v2.0.0 and higher.\n" - "Use an in-memory database, or ATTACH with (STORAGE_VERSION 'v2.0.0')"); + throw BinderException( + "Explicit constraint check mode is only supported for storage versions v2.0.0 and higher.\n" + "Use an in-memory database, or ATTACH with (STORAGE_VERSION 'v2.0.0')"); } } unique_ptr Binder::BindUniqueConstraint(const Constraint &constraint, const Identifier &table, const ColumnList &columns) { auto &unique = constraint.Cast(); - if (unique.IsDeferred()) { - throw NotImplementedException("Deferred %s constraints are not implemented yet", - unique.IsPrimaryKey() ? "PRIMARY KEY" : "UNIQUE"); - } // Resolve the columns. vector indexes; @@ -472,12 +469,23 @@ static void FindForeignKeyIndexes(const ColumnList &columns, const vector> &constraints, ForeignKeyConstraint &fk) { // find the matching primary key constraint bool found_constraint = false; + bool found_deferred = false; // if no columns are defined, we will automatically try to bind to the primary key bool find_primary_key = fk.pk_columns.empty(); + if (!find_primary_key && fk.pk_columns.size() != fk.fk_columns.size()) { + throw BinderException( + "Failed to create foreign key: number of referencing (%s) and referenced columns (%s) differ", + StringUtil::Join(fk.fk_columns, ","), StringUtil::Join(fk.pk_columns, ",")); + } for (auto &constr : constraints) { if (constr->type != ConstraintType::UNIQUE) { continue; @@ -503,26 +511,26 @@ static void FindMatchingPrimaryKeyColumns(const ColumnList &columns, const vecto "Failed to create foreign key: number of referencing (%s) and referenced columns (%s) differ", fk_name_str, pk_name_str); } + if (unique.IsDeferred()) { + ThrowDeferredReferencedKey(); + } fk.pk_columns = pk_names; return; } - if (pk_names.size() != fk.fk_columns.size()) { - // the number of referencing and referenced columns for foreign keys must be the same + if (pk_names != fk.pk_columns) { continue; } - bool equals = true; - for (idx_t i = 0; i < fk.pk_columns.size(); i++) { - if (fk.pk_columns[i] != pk_names[i]) { - equals = false; - break; - } - } - if (!equals) { + if (unique.IsDeferred()) { + // keep looking for a matching constraint that is not deferred + found_deferred = true; continue; } // found match return; } + if (found_deferred) { + ThrowDeferredReferencedKey(); + } // no match found! examine why if (!found_constraint) { // no unique constraint or primary key @@ -720,7 +728,7 @@ unique_ptr Binder::BindCreateTableInfo(unique_ptrGetQualifiedName().Path().front()); - properties.RegisterDBModify(catalog, context, DatabaseModificationType::DROP_CATALOG_ENTRY); + properties.RegisterDBModify(catalog, context, stmt.info->GetModifications()); break; } case CatalogType::VIEW_ENTRY: @@ -101,7 +101,7 @@ BoundStatement Binder::Bind(DropStatement &stmt) { stmt.info->SetQualifiedName(entry->ParentSchema().GetQualifiedName(stmt.info->GetQualifiedName().Name())); if (!entry->temporary) { // we can only drop temporary schema entries in read-only mode - properties.RegisterDBModify(entry->ParentCatalog(), context, DatabaseModificationType::DROP_CATALOG_ENTRY); + properties.RegisterDBModify(entry->ParentCatalog(), context, stmt.info->GetModifications()); } break; } diff --git a/src/duckdb/src/planner/binder/statement/bind_insert.cpp b/src/duckdb/src/planner/binder/statement/bind_insert.cpp index 0f20c2eca..35a35caae 100644 --- a/src/duckdb/src/planner/binder/statement/bind_insert.cpp +++ b/src/duckdb/src/planner/binder/statement/bind_insert.cpp @@ -367,6 +367,11 @@ unique_ptr Binder::GenerateMergeInto(InsertQueryNode &node, if (!index.is_unique) { continue; } + if (index.check_mode == ConstraintCheckMode::DEFERRED) { + // Without a conflict target, every constraint is a conflict target. + throw BinderException("DEFERRED PRIMARY KEY or UNIQUE constraints cannot be ON CONFLICT targets, " + "specify the ON CONFLICT columns of a constraint that is not DEFERRED"); + } vector> and_children; auto &indexed_columns = index.column_set; @@ -441,17 +446,22 @@ unique_ptr Binder::GenerateMergeInto(InsertQueryNode &node, } } bool index_references_columns = false; + bool deferred_index_matches = false; for (auto &index : storage_info.index_info) { - if (!index.is_unique) { + if (!index.is_unique || on_conflict_filter != index.column_set) { continue; } - bool index_matches = on_conflict_filter == index.column_set; - if (index_matches) { - index_references_columns = true; - break; + if (index.check_mode == ConstraintCheckMode::DEFERRED) { + deferred_index_matches = true; + continue; } + index_references_columns = true; + break; } if (!index_references_columns) { + if (deferred_index_matches) { + throw BinderException("DEFERRED PRIMARY KEY or UNIQUE constraints cannot be ON CONFLICT targets"); + } // Same as before, this is essentially a no-op, turning this into a DO THROW instead // But since this makes no logical sense, it's probably better to throw an error throw BinderException("The specified columns as conflict target are not referenced by a UNIQUE/PRIMARY KEY " diff --git a/src/duckdb/src/planner/expression_binder/index_binder.cpp b/src/duckdb/src/planner/expression_binder/index_binder.cpp index f5c2341f0..f3a3d7350 100644 --- a/src/duckdb/src/planner/expression_binder/index_binder.cpp +++ b/src/duckdb/src/planner/expression_binder/index_binder.cpp @@ -7,6 +7,7 @@ #include "duckdb/planner/column_binding.hpp" #include "duckdb/execution/index/bound_index.hpp" #include "duckdb/execution/index/unbound_index.hpp" +#include "duckdb/main/client_context.hpp" #include "duckdb/main/config.hpp" #include "duckdb/main/database.hpp" #include "duckdb/planner/operator/logical_get.hpp" diff --git a/src/duckdb/src/storage/checkpoint/table_data_writer.cpp b/src/duckdb/src/storage/checkpoint/table_data_writer.cpp index 70ff127bf..824c3cf48 100644 --- a/src/duckdb/src/storage/checkpoint/table_data_writer.cpp +++ b/src/duckdb/src/storage/checkpoint/table_data_writer.cpp @@ -1,5 +1,6 @@ #include "duckdb/storage/checkpoint/table_data_writer.hpp" -#include "duckdb/planner/binder.hpp" +#include "duckdb/common/shared_ptr_ipp.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" #include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" @@ -9,11 +10,12 @@ #include "duckdb/main/database.hpp" #include "duckdb/main/settings.hpp" #include "duckdb/parallel/task_scheduler.hpp" +#include "duckdb/planner/binder.hpp" #include "duckdb/planner/parsed_data/bound_create_table_info.hpp" #include "duckdb/storage/checkpoint/table_data_reader.hpp" +#include "duckdb/storage/index_storage_info.hpp" #include "duckdb/storage/metadata/metadata_reader.hpp" -#include "duckdb/storage/table/column_checkpoint_state.hpp" -#include "duckdb/storage/table/data_table_info.hpp" +#include "duckdb/storage/table/table_index_list.hpp" #include "duckdb/storage/table/table_statistics.hpp" #include "duckdb/storage/data_table.hpp" @@ -90,12 +92,18 @@ void SingleFileTableDataWriter::WriteUnchangedTable(MetaBlockPointer pointer, existing_next_row_id = next_row_id; } +unique_ptr SingleFileTableDataWriter::GetTableIndexWriter(StorageVersion version) { + return make_uniq(checkpoint_manager, version); +} + void SingleFileTableDataWriter::FlushPartialBlocks() { checkpoint_manager.partial_block_manager.FlushPartialBlocks(); } void SingleFileTableDataWriter::FinalizeTable(const TableStatistics &global_stats, DataTableInfo &info, - RowGroupCollection &collection, Serializer &serializer) { + RowGroupCollection &collection, + const vector> &index_infos, + Serializer &serializer) { MetaBlockPointer pointer; idx_t total_rows; idx_t next_row_id; @@ -189,20 +197,10 @@ void SingleFileTableDataWriter::FinalizeTable(const TableStatistics &global_stat serializer.WriteProperty(101, "table_pointer", pointer); serializer.WriteProperty(102, "total_rows", total_rows); - // prior: ser version 3 - auto v1_0_0_storage = StorageManager::IsPriorToVersion( - StorageVersion::V1_2_0, serializer.GetOptions().storage_compatibility.storage_version); - IndexSerializationInfo serialization_info; - if (!v1_0_0_storage) { - serialization_info.options.emplace("v1_0_0_storage", v1_0_0_storage); - } - - auto index_storage_infos = info.GetIndexes().SerializeToDisk(context, serialization_info); - if (debug_verify_blocks) { - for (auto &entry : index_storage_infos.ordered_infos) { - for (auto &allocator : entry.get().allocator_infos) { - for (auto &block : allocator.block_pointers) { + for (const auto &index_info : index_infos) { + for (const auto &allocator : index_info->allocator_infos) { + for (const auto &block : allocator.block_pointers) { checkpoint_manager.verify_block_usage_count[block.block_id]++; } } @@ -210,11 +208,11 @@ void SingleFileTableDataWriter::FinalizeTable(const TableStatistics &global_stat } // write empty block pointers for forwards compatibility - vector compat_block_pointers; + const vector compat_block_pointers; serializer.WriteProperty(103, "index_pointers", compat_block_pointers); - serializer.WriteList( - 104, "index_storage_infos", index_storage_infos.ordered_infos.size(), - [&](Serializer::List &list, idx_t i) { list.WriteElement(index_storage_infos.ordered_infos[i].get()); }); + serializer.WriteList(104, "index_storage_infos", index_infos.size(), + [&](Serializer::List &list, idx_t i) { list.WriteElement(*index_infos[i]); }); + if (serializer.ShouldSerialize(StorageVersion::V2_0_0)) { serializer.WriteProperty(105, "next_row_id", next_row_id); } diff --git a/src/duckdb/src/storage/checkpoint/table_index_writer.cpp b/src/duckdb/src/storage/checkpoint/table_index_writer.cpp new file mode 100644 index 000000000..89aba0f93 --- /dev/null +++ b/src/duckdb/src/storage/checkpoint/table_index_writer.cpp @@ -0,0 +1,32 @@ +#include "duckdb/storage/checkpoint/table_index_writer.hpp" +#include "duckdb/storage/partial_block_manager.hpp" +#include "duckdb/storage/checkpoint_manager.hpp" + +namespace duckdb { + +TableIndexWriter::TableIndexWriter(PartialBlockManager &partial_block_manager, StorageVersion version) + : partial_block_manager(partial_block_manager), storage_version(version) { +} + +TableIndexWriter::~TableIndexWriter() { +} + +StorageVersion TableIndexWriter::GetStorageVersion() const { + return storage_version; +} + +SingleFileTableIndexWriter::SingleFileTableIndexWriter(SingleFileCheckpointWriter &checkpoint_manager, + const StorageVersion version) + : TableIndexWriter(checkpoint_manager.index_partial_block_manager, version), + checkpoint_manager(checkpoint_manager) { +} + +PartialBlockManager SingleFileTableIndexWriter::CreatePerIndexPartialBlockManager() { + return checkpoint_manager.CreatePerIndexPartialBlockManager(); +} + +void SingleFileTableIndexWriter::Flush() { + partial_block_manager.FlushPartialBlocks(); +} + +} // namespace duckdb diff --git a/src/duckdb/src/storage/checkpoint_manager.cpp b/src/duckdb/src/storage/checkpoint_manager.cpp index 4ded34ae6..bf5efc78f 100644 --- a/src/duckdb/src/storage/checkpoint_manager.cpp +++ b/src/duckdb/src/storage/checkpoint_manager.cpp @@ -1,6 +1,7 @@ #include "duckdb/storage/checkpoint_manager.hpp" #include "duckdb/catalog/catalog_entry/duck_index_entry.hpp" +#include "duckdb/catalog/catalog_entry/duck_schema_entry.hpp" #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" #include "duckdb/catalog/catalog_entry/index_catalog_entry.hpp" #include "duckdb/catalog/catalog_entry/scalar_macro_catalog_entry.hpp" @@ -33,6 +34,7 @@ #include "duckdb/storage/checkpoint/table_data_writer.hpp" #include "duckdb/storage/metadata/metadata_reader.hpp" #include "duckdb/storage/table/data_table_info.hpp" +#include "duckdb/storage/table/index_entry.hpp" #include "duckdb/transaction/duck_transaction_manager.hpp" #include "duckdb/transaction/duck_transaction.hpp" #include "duckdb/transaction/transaction_manager.hpp" @@ -54,6 +56,9 @@ ActiveCheckpointWrapper::ActiveCheckpointWrapper(optional_ptr con ActiveCheckpointWrapper::~ActiveCheckpointWrapper() { // This happens on failure before we commit the transaction. + if (active) { + transaction_manager.ResetActiveCheckpoint(); + } if (checkpoint_transaction) { transaction_manager.RollbackTransaction(*checkpoint_transaction); checkpoint_transaction = nullptr; @@ -63,21 +68,28 @@ ActiveCheckpointWrapper::~ActiveCheckpointWrapper() { } } -void ActiveCheckpointWrapper::GetCheckpointTransaction(CheckpointOptions &options) { - checkpoint_context->transaction.BeginTransaction(); - checkpoint_context->transaction.SetReadOnly(); - auto &transaction = DuckTransaction::Get(*checkpoint_context, db); - transaction.SetIsCheckpointTransaction(); - checkpoint_transaction = &transaction; - // the checkpoint sees every commit before it started - D_ASSERT(transaction.view.visibility_bound == VisibilityBound::Before(transaction.start_time)); +void ActiveCheckpointWrapper::Begin(CheckpointOptions &options) { + if (checkpoint_context) { + // the start time of the checkpoint transaction defines the visibility for checkpointing + checkpoint_context->transaction.BeginTransaction(); + checkpoint_context->transaction.SetReadOnly(); + auto &transaction = DuckTransaction::Get(*checkpoint_context, db); + transaction.SetIsCheckpointTransaction(); + checkpoint_transaction = &transaction; + // the checkpoint sees every commit before it started + D_ASSERT(transaction.view.visibility_bound == VisibilityBound::Before(transaction.start_time)); + options.visibility_bound = transaction.view.visibility_bound; + } else { + options.visibility_bound = VisibilityBound::Through(transaction_manager.GetLastCommit()); + } options.checkpoint_id = transaction_manager.NextCheckpointId(); - options.visibility_bound = transaction.view.visibility_bound; transaction_manager.SetActiveCheckpoint(options.checkpoint_id.GetIndex()); + active = true; } void ActiveCheckpointWrapper::Commit() { transaction_manager.ResetActiveCheckpoint(); + active = false; if (!checkpoint_transaction) { return; } @@ -85,16 +97,13 @@ void ActiveCheckpointWrapper::Commit() { checkpoint_transaction = nullptr; } -bool ActiveCheckpointWrapper::HasCheckpointContext() const { - return checkpoint_context; -} - void ReorderTableEntries(catalog_entry_vector_t &tables); SingleFileCheckpointWriter::SingleFileCheckpointWriter(QueryContext context, AttachedDatabase &db, BlockManager &block_manager, CheckpointOptions options_p) : CheckpointWriter(db), context(context.GetClientContext()), - partial_block_manager(context, block_manager, PartialBlockType::FULL_CHECKPOINT), options(options_p) { + partial_block_manager(context, block_manager, PartialBlockType::FULL_CHECKPOINT), + index_partial_block_manager(context, block_manager, PartialBlockType::FULL_CHECKPOINT), options(options_p) { } BlockManager &SingleFileCheckpointWriter::GetBlockManager() { @@ -102,6 +111,10 @@ BlockManager &SingleFileCheckpointWriter::GetBlockManager() { return *storage_manager.block_manager; } +PartialBlockManager SingleFileCheckpointWriter::CreatePerIndexPartialBlockManager() { + return PartialBlockManager(QueryContext(context), GetBlockManager(), PartialBlockType::FULL_CHECKPOINT); +} + MetadataWriter &SingleFileCheckpointWriter::GetMetadataWriter() { return *metadata_writer; } @@ -114,19 +127,19 @@ unique_ptr SingleFileCheckpointWriter::GetTableDataWriter(Table return make_uniq(*this, table, *table_metadata_writer); } -static catalog_entry_vector_t GetCatalogEntries(vector> &schemas) { +static catalog_entry_vector_t GetCatalogEntries(vector> &schemas, VisibilityBound bound) { catalog_entry_vector_t entries; for (auto &schema_p : schemas) { auto &schema = schema_p.get(); entries.push_back(schema); - schema.Scan(CatalogType::TYPE_ENTRY, [&](CatalogEntry &entry) { + schema.Scan(CatalogType::TYPE_ENTRY, bound, [&](CatalogEntry &entry) { if (entry.internal) { return; } entries.push_back(entry); }); - schema.Scan(CatalogType::SEQUENCE_ENTRY, [&](CatalogEntry &entry) { + schema.Scan(CatalogType::SEQUENCE_ENTRY, bound, [&](CatalogEntry &entry) { if (entry.internal) { return; } @@ -135,7 +148,7 @@ static catalog_entry_vector_t GetCatalogEntries(vector> views; - schema.Scan(CatalogType::TABLE_ENTRY, [&](CatalogEntry &entry) { + schema.Scan(CatalogType::TABLE_ENTRY, bound, [&](CatalogEntry &entry) { if (entry.internal) { return; } @@ -163,14 +176,14 @@ static catalog_entry_vector_t GetCatalogEntries(vector(); - duck_table.ScanTriggersNonTransactional([&](CatalogEntry &entry) { + duck_table.ScanTriggers(bound, [&](CatalogEntry &entry) { if (!entry.internal) { entries.push_back(entry); } }); } - schema.Scan(CatalogType::SCALAR_FUNCTION_ENTRY, [&](CatalogEntry &entry) { + schema.Scan(CatalogType::SCALAR_FUNCTION_ENTRY, bound, [&](CatalogEntry &entry) { if (entry.internal) { return; } @@ -179,7 +192,7 @@ static catalog_entry_vector_t GetCatalogEntries(vector> schemas; - // we scan the set of committed schemas + vector> schemas; + // write the catalog as of the bound: later commits are in the WAL that outlives this checkpoint auto &catalog = Catalog::GetCatalog(db).Cast(); - catalog.ScanSchemas([&](SchemaCatalogEntry &entry) { schemas.push_back(entry); }); + catalog.ScanSchemas(options.visibility_bound, [&](DuckSchemaEntry &entry) { schemas.push_back(entry); }); D_ASSERT(catalog.IsDuckCatalog()); auto &dependency_manager = *catalog.GetDependencyManager(); - catalog_entries = GetCatalogEntries(schemas); - dependency_manager.ReorderEntries(catalog_entries); + catalog_entries = GetCatalogEntries(schemas, options.visibility_bound); + dependency_manager.ReorderEntries(catalog_entries, options.visibility_bound); // write the actual data into the database @@ -341,17 +354,17 @@ void SingleFileCheckpointWriter::CreateCheckpoint() { // truncate the WAL if (has_wal) { - unique_lock owned_wal_lock; - optional_ptr> wal_lock; - if (!options.wal_lock) { - // not holding the WAL lock yet - grab it - owned_wal_lock = storage_manager.GetWALLock(); - wal_lock = owned_wal_lock; + unique_lock owned_commit_lock; + optional_ptr> commit_lock; + if (!options.commit_lock) { + // not holding the commit lock yet - grab it + owned_commit_lock = storage_manager.GetCommitLock(); + commit_lock = owned_commit_lock; } else { - // we already have the WAL lock - just refer to it - wal_lock = options.wal_lock; + // we already have the commit lock - just refer to it + commit_lock = options.commit_lock; } - storage_manager.WALFinishCheckpoint(*wal_lock); + storage_manager.WALFinishCheckpoint(*commit_lock); } // for any indexes that were appended to while checkpointing, merge the delta back into the main index @@ -649,7 +662,7 @@ void CheckpointReader::ReadIndex(CatalogTransaction transaction, Deserializer &d // Create an unbound index and add it to the table. auto unbound_index = make_uniq(std::move(create_info), std::move(index_storage_info), TableIOManager::Get(data_table), data_table.db); - table_info->GetIndexes().AddIndex(std::move(unbound_index)); + table_info->GetIndexes().AddIndex(std::move(unbound_index), ConstraintCheckMode::DEFAULT); } //===--------------------------------------------------------------------===// @@ -705,7 +718,6 @@ void SingleFileCheckpointWriter::WriteTable(TableCatalogEntry &table, Serializer // FIXME: If we do not have a context, however, the unbound indexes have to be serialized to disk. // Write the table data - auto table_lock = table.GetStorage().GetCheckpointLock(); auto writer = GetTableDataWriter(table); if (writer) { writer->WriteTableData(serializer); diff --git a/src/duckdb/src/storage/data_table.cpp b/src/duckdb/src/storage/data_table.cpp index 2cc3384c3..a6e963fb8 100644 --- a/src/duckdb/src/storage/data_table.cpp +++ b/src/duckdb/src/storage/data_table.cpp @@ -361,7 +361,7 @@ bool DataTable::HasUniqueIndexes() const { } void DataTable::AddIndex(unique_ptr index) { - info->indexes.AddIndex(std::move(index)); + info->indexes.AddIndex(std::move(index), ConstraintCheckMode::DEFAULT); } bool DataTable::HasForeignKeyIndex(const vector &keys, ForeignKeyType type) { @@ -1057,7 +1057,6 @@ void DataTable::AppendLock(DuckTransaction &transaction, TableAppendState &state "a different transaction", GetTableName(), TableModification()); } - state.table_lock = transaction.SharedLockTable(*info); state.row_start = NumericCast(row_groups->GetNextRowId()); state.current_row = state.row_start; auto &transaction_manager = transaction.GetTransactionManager(); @@ -1221,7 +1220,6 @@ void DataTable::RevertAppendInternal(idx_t start_row) { void DataTable::RevertAppend(DuckTransaction &transaction, idx_t start_row, idx_t count) { lock_guard lock(append_lock); - auto table_lock = transaction.SharedLockTable(*info); // revert any appends to indexes if (!info->indexes.Empty()) { @@ -1310,7 +1308,6 @@ void DataTable::VerifyDeleteConstraints(optional_ptr storage, unique_ptr DataTable::InitializeDelete(TableCatalogEntry &table, ClientContext &context, const vector> &bound_constraints) { - auto &transaction = DuckTransaction::Get(context, db); // Bind all indexes. info->BindIndexes(context); @@ -1327,7 +1324,6 @@ unique_ptr DataTable::InitializeDelete(TableCatalogEntry &tabl result->verify_chunk.Initialize(Allocator::Get(context), types); result->constraint_state = make_uniq(table, bound_constraints); } - result->checkpoint_lock = transaction.SharedLockTable(*info); return result; } @@ -1576,16 +1572,20 @@ unique_ptr DataTable::GetSample() { //===--------------------------------------------------------------------===// // Checkpoint //===--------------------------------------------------------------------===// -unique_ptr DataTable::GetCheckpointLock() { - return info->checkpoint_lock.GetExclusiveLock(); -} - void DataTable::Checkpoint(TableDataWriter &writer, Serializer &serializer) { - writer.SetRowGroupCount(info->CheckpointRowGroupCount(writer.GetCheckpointOptions())); - // checkpoint each individual row group TableStatistics global_stats; - row_groups->Checkpoint(writer, global_stats); - row_groups->SetRowGroupAppendMode(RowGroupAppendMode::SUGGEST_NEW); + CollectionCheckpointSnapshot snapshot; + { + lock_guard lock(append_lock); + snapshot = row_groups->SnapshotForCheckpoint(writer); + } + // checkpoint each individual row group + auto result = row_groups->Checkpoint(writer, global_stats, snapshot); + { + lock_guard lock(append_lock); + row_groups->InstallCheckpoint(std::move(result), global_stats); + row_groups->SetRowGroupAppendMode(RowGroupAppendMode::SUGGEST_NEW); + } if (writer.GetRebuildIndexes()) { MetricsTimer timer; auto context = writer.TryGetClientContext(); @@ -1595,14 +1595,16 @@ void DataTable::Checkpoint(TableDataWriter &writer, Serializer &serializer) { RebuildIndexes(); timer.EndTimer(); } - // The row group payload data has been written. Now write: - // sample - // column stats - // row-group pointers - // table pointer - // index data - writer.FinalizeTable(global_stats, *info, *row_groups, serializer); - row_groups->SetStats(global_stats); + + const auto storage_version = serializer.GetOptions().storage_compatibility.storage_version; + const auto index_writer = writer.GetTableIndexWriter(storage_version); + vector> index_infos; + if (index_writer) { + // Only checkpoint indexes when we write to disk + index_infos = info->GetIndexes().Checkpoint(*index_writer); + } + + writer.FinalizeTable(global_stats, *info, *row_groups, index_infos, serializer); } void DataTable::CommitDropColumn(const idx_t column_index, CommitDropState &drop_state) { @@ -1663,7 +1665,8 @@ bool DataTable::ScanColumnSegmentInfo(const QueryContext &context, ColumnSegment // Index Constraint Creation //===--------------------------------------------------------------------===// void DataTable::AddIndex(const ColumnList &columns, const vector &column_indexes, - const IndexConstraintType type, IndexStorageInfo index_info) { + const IndexConstraintType type, IndexStorageInfo index_info, + const ConstraintCheckMode check_mode) { if (!IsMainTable()) { throw TransactionException("Transaction conflict: attempting to add an index to table \"%s\" but it has been " "%s by a different transaction", @@ -1686,7 +1689,7 @@ void DataTable::AddIndex(const ColumnList &columns, const vector & auto &io_manager = TableIOManager::Get(*this); auto art = make_uniq(index_info.name, type, physical_ids, io_manager, std::move(expressions), db, nullptr, index_info); - info->indexes.AddIndex(std::move(art)); + info->indexes.AddIndex(std::move(art), check_mode); } } // namespace duckdb diff --git a/src/duckdb/src/storage/index.cpp b/src/duckdb/src/storage/index.cpp index 48dc01e04..35ce6cb6a 100644 --- a/src/duckdb/src/storage/index.cpp +++ b/src/duckdb/src/storage/index.cpp @@ -1,5 +1,5 @@ #include "duckdb/storage/index.hpp" -#include "duckdb/common/radix.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" namespace duckdb { @@ -10,4 +10,8 @@ Index::Index(const vector &column_ids, TableIOManager &table_io_manage column_id_set.insert(column_ids.begin(), column_ids.end()); } +IndexCheckpointResult Index::Checkpoint(PartialBlockManager &partial_block_manager, const StorageVersion version) { + throw NotImplementedException("Checkpoint not implemented for index type \"%s\"", GetIndexType()); +} + } // namespace duckdb diff --git a/src/duckdb/src/storage/index_entry.cpp b/src/duckdb/src/storage/index_entry.cpp index 6554801da..e1c0c3308 100644 --- a/src/duckdb/src/storage/index_entry.cpp +++ b/src/duckdb/src/storage/index_entry.cpp @@ -1,24 +1,21 @@ #include "duckdb/storage/table/index_entry.hpp" +#include "duckdb/storage/index.hpp" +#include "duckdb/storage/partial_block_manager.hpp" +#include "duckdb/storage/storage_info.hpp" #include "duckdb/storage/table/table_index_list.hpp" +#include "duckdb/storage/data_table.hpp" +#include "duckdb/storage/table/scan_state.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" -#include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" -#include "duckdb/common/types/constraint_conflict_info.hpp" #include "duckdb/common/types/conflict_manager.hpp" -#include "duckdb/common/vector_operations/vector_operations.hpp" -#include "duckdb/storage/table/append_state.hpp" #include "duckdb/execution/index/art/art.hpp" #include "duckdb/execution/index/unbound_index.hpp" -#include "duckdb/main/config.hpp" -#include "duckdb/main/database.hpp" #include "duckdb/planner/expression_binder/index_binder.hpp" -#include "duckdb/storage/data_table.hpp" -#include "duckdb/storage/table/data_table_info.hpp" -#include "duckdb/storage/table/scan_state.hpp" -#include "duckdb/main/attached_database.hpp" namespace duckdb { -IndexEntry::IndexEntry(unique_ptr index_p) : owned_index(std::move(index_p)) { +IndexEntry::IndexEntry(unique_ptr index_p, const ConstraintCheckMode check_mode_p) + : owned_index(std::move(index_p)), check_mode(check_mode_p) { if (owned_index->IsBound()) { bind_state = IndexBindState::BOUND; } else { @@ -104,6 +101,15 @@ void IndexEntry::RevertAppend(DataChunk &chunk, Vector &row_ids) { } void IndexEntry::InitializeLocalIndexes(TableIndexList &delete_indexes, TableIndexList &append_indexes) const { + InitializeLocalIndexesInternal(delete_indexes, &append_indexes); +} + +void IndexEntry::InitializeLocalDeleteIndex(TableIndexList &delete_indexes) const { + InitializeLocalIndexesInternal(delete_indexes, nullptr); +} + +void IndexEntry::InitializeLocalIndexesInternal(TableIndexList &delete_indexes, + optional_ptr append_indexes) const { auto entry_lock = lock.GetSharedLock(); if (owned_index->GetConstraintType() == IndexConstraintType::NONE || !owned_index->IsBound()) { return; @@ -114,8 +120,11 @@ void IndexEntry::InitializeLocalIndexes(TableIndexList &delete_indexes, TableInd } auto constraint_type = bound_index.GetConstraintType(); - delete_indexes.AddIndex(bound_index.CreateEmptyCopy(constraint_type)); - append_indexes.AddIndex(bound_index.CreateEmptyCopy(constraint_type)); + delete_indexes.AddIndex(bound_index.CreateEmptyCopy(constraint_type), ConstraintCheckMode::DEFAULT); + // Deferred constraints have no append index: transaction-local rows are verified when committing. + if (append_indexes && !IsDeferred()) { + append_indexes->AddIndex(bound_index.CreateEmptyCopy(constraint_type), ConstraintCheckMode::DEFAULT); + } } void IndexEntry::AppendToDeleteIndexes(DataChunk &chunk, Vector &row_ids) { @@ -280,9 +289,18 @@ bool IndexEntry::IsUnique() const { return owned_index->IsUnique(); } +bool IndexEntry::IsDeferred() const { + return check_mode == ConstraintCheckMode::DEFERRED; +} + bool IndexEntry::IsForeignKeyIndex(const vector &fk_keys, const ForeignKeyType fk_type) const { auto entry_lock = lock.GetSharedLock(); - if (fk_type == ForeignKeyType::FK_TYPE_PRIMARY_KEY_TABLE ? !owned_index->IsUnique() : !owned_index->IsForeign()) { + if (fk_type == ForeignKeyType::FK_TYPE_PRIMARY_KEY_TABLE) { + // Foreign keys cannot reference a deferred key. + if (!owned_index->IsUnique() || IsDeferred()) { + return false; + } + } else if (!owned_index->IsForeign()) { return false; } const auto &column_ids = owned_index->GetColumnIds(); @@ -456,6 +474,7 @@ IndexInfo IndexEntry::GetStorageInfo() const { result.is_primary = owned_index->IsPrimary(); result.is_unique = owned_index->IsUnique() || result.is_primary; result.is_foreign = owned_index->IsForeign(); + result.check_mode = check_mode; result.column_set = owned_index->GetColumnIdSet(); return result; } @@ -468,19 +487,44 @@ idx_t IndexEntry::GetInMemorySize() const { return owned_index->Cast().GetInMemorySize(); } -IndexStorageInfo IndexEntry::SerializeToDisk(QueryContext context, const case_insensitive_map_t &options) { +IndexCheckpointResult IndexEntry::Checkpoint(TableIndexWriter &index_writer) { auto entry_lock = lock.GetExclusiveLock(); - if (owned_index->IsBound()) { - return owned_index->Cast().SerializeToDisk(context, options); + D_ASSERT(owned_index); + + auto storage_version = index_writer.GetStorageVersion(); + if (owned_index->GetCheckpointMode() == IndexCheckpointMode::DEFERRED) { + if (owned_index->IsBound() && !owned_index->Cast().SupportsDeltaIndexes()) { + throw InternalException("Deferred index checkpointing requires delta index support"); + } + return owned_index->Checkpoint(index_writer.GetSharedPartialBlockManager(), storage_version); + } + + auto partial_block_manager = index_writer.CreatePerIndexPartialBlockManager(); + auto checkpoint = owned_index->Checkpoint(partial_block_manager, storage_version); + partial_block_manager.FlushPartialBlocks(); + + InstallShadowInternal(std::move(checkpoint.shadow_index)); + return checkpoint; +} + +void IndexEntry::InstallShadow(unique_ptr shadow_index) { + auto entry_lock = lock.GetExclusiveLock(); + InstallShadowInternal(std::move(shadow_index)); +} + +void IndexEntry::InstallShadowInternal(unique_ptr shadow_index) { + if (!shadow_index) { + return; } - return owned_index->Cast().CopyStorageInfo(); + D_ASSERT(owned_index && owned_index->IsBound()); + owned_index = std::move(shadow_index); } -IndexStorageInfo IndexEntry::SerializeToWAL(const case_insensitive_map_t &options) { +IndexStorageInfo IndexEntry::SerializeToWAL(const StorageVersion version) { auto entry_lock = lock.GetExclusiveLock(); // We never write an unbound index to the WAL. D_ASSERT(owned_index->IsBound()); - return owned_index->Cast().SerializeToWAL(options); + return owned_index->Cast().SerializeToWAL(version); } void IndexEntry::MergeCheckpointDeltas(const optional_idx checkpoint_id) { diff --git a/src/duckdb/src/storage/local_storage.cpp b/src/duckdb/src/storage/local_storage.cpp index 5331d2ea6..4e962a4fc 100644 --- a/src/duckdb/src/storage/local_storage.cpp +++ b/src/duckdb/src/storage/local_storage.cpp @@ -9,6 +9,7 @@ #include "duckdb/storage/partial_block_manager.hpp" #include "duckdb/storage/storage_manager.hpp" #include "duckdb/storage/table/append_state.hpp" +#include "duckdb/storage/table/column_data.hpp" #include "duckdb/storage/table/data_table_info.hpp" #include "duckdb/storage/table/row_group.hpp" #include "duckdb/storage/table/scan_state.hpp" @@ -621,7 +622,7 @@ void LocalStorage::FlushBulkAppendBlocksAndSync(AttachedDatabase &db) { if (requires_sync) { // the WAL will reference the flushed row groups (flushed just now or already during the // statement, e.g. by batch inserts) - persist them now so that the commit does not have - // to FileSync while holding the WAL lock + // to FileSync while holding the commit lock db.GetStorageManager().GetBlockManager().FileSync(); synced_flushed_blocks = true; } diff --git a/src/duckdb/src/storage/metadata/metadata_reader.cpp b/src/duckdb/src/storage/metadata/metadata_reader.cpp index 50c5ca836..e4a26cc4d 100644 --- a/src/duckdb/src/storage/metadata/metadata_reader.cpp +++ b/src/duckdb/src/storage/metadata/metadata_reader.cpp @@ -1,5 +1,7 @@ #include "duckdb/storage/metadata/metadata_reader.hpp" +#include "duckdb/common/numeric_utils.hpp" + namespace duckdb { MetadataReader::MetadataReader(MetadataManager &manager, MetaBlockPointer pointer, diff --git a/src/duckdb/src/storage/metadata/metadata_writer.cpp b/src/duckdb/src/storage/metadata/metadata_writer.cpp index f2efed59f..8f7fcf34b 100644 --- a/src/duckdb/src/storage/metadata/metadata_writer.cpp +++ b/src/duckdb/src/storage/metadata/metadata_writer.cpp @@ -1,5 +1,7 @@ #include "duckdb/storage/metadata/metadata_writer.hpp" +#include "duckdb/common/numeric_utils.hpp" + namespace duckdb { MetadataWriter::MetadataWriter(MetadataManager &manager, optional_ptr> written_pointers_p) diff --git a/src/duckdb/src/storage/serialization/serialize_constraint.cpp b/src/duckdb/src/storage/serialization/serialize_constraint.cpp index c93dffc38..6132d72f0 100644 --- a/src/duckdb/src/storage/serialization/serialize_constraint.cpp +++ b/src/duckdb/src/storage/serialization/serialize_constraint.cpp @@ -86,7 +86,7 @@ void UniqueConstraint::Serialize(Serializer &serializer) const { serializer.WriteProperty(201, "index", index); serializer.WritePropertyWithDefault>(202, "columns", columns); if (serializer.ShouldSerialize(StorageVersion::V2_0_0)) { - serializer.WritePropertyWithDefault(203, "timing", timing, ConstraintTiming::DEFAULT); + serializer.WritePropertyWithDefault(203, "check_mode", check_mode, ConstraintCheckMode::DEFAULT); } } @@ -95,7 +95,7 @@ unique_ptr UniqueConstraint::Deserialize(Deserializer &deserializer) deserializer.ReadPropertyWithDefault(200, "is_primary_key", result->is_primary_key); deserializer.ReadProperty(201, "index", result->index); deserializer.ReadPropertyWithDefault>(202, "columns", result->columns); - deserializer.ReadPropertyWithExplicitDefault(203, "timing", result->timing, ConstraintTiming::DEFAULT); + deserializer.ReadPropertyWithExplicitDefault(203, "check_mode", result->check_mode, ConstraintCheckMode::DEFAULT); return std::move(result); } diff --git a/src/duckdb/src/storage/storage_manager.cpp b/src/duckdb/src/storage/storage_manager.cpp index 6c649f895..5c8aa286d 100644 --- a/src/duckdb/src/storage/storage_manager.cpp +++ b/src/duckdb/src/storage/storage_manager.cpp @@ -19,6 +19,7 @@ #include "duckdb/storage/table/in_memory_checkpoint.hpp" #include "duckdb/catalog/duck_catalog.hpp" #include "duckdb/catalog/catalog_entry/schema_catalog_entry.hpp" +#include "duckdb/catalog/catalog_entry/duck_schema_entry.hpp" #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" #include "duckdb/transaction/duck_transaction.hpp" #include "duckdb/transaction/duck_transaction_manager.hpp" @@ -251,12 +252,12 @@ bool StorageManager::HasWAL() const { bool StorageManager::WALStartCheckpoint(MetaBlockPointer meta_block, CheckpointOptions &options, ActiveCheckpointWrapper &active_checkpoint) { unique_lock guard; - // Lock ordering: WAL lock -> transaction lock (in GetCheckpointTransaction) - if (!options.wal_lock) { - // not holding the WAL lock yet - grab it - guard = GetWALLock(); + // Lock ordering: commit lock -> transaction lock (in ActiveCheckpointWrapper::Begin) + if (!options.commit_lock) { + // not holding the commit lock yet - grab it + guard = GetCommitLock(); } - // drain under the WAL lock before the checkpoint transaction starts: its snapshot would + // drain under the commit lock before the checkpoint transaction starts: its snapshot would // otherwise be bounded below commits whose WAL this checkpoint deletes auto &transaction_manager = DuckTransactionManager::Get(db); transaction_manager.WaitForDurability(); @@ -266,15 +267,8 @@ bool StorageManager::WALStartCheckpoint(MetaBlockPointer meta_block, CheckpointO options.type = CheckpointType::CONCURRENT_CHECKPOINT; } - if (active_checkpoint.HasCheckpointContext()) { - // While holding the WAL lock, if we have a context then start a checkpoint transaction. - // The start time of this transaction defines the visibility for checkpointing, any new commits are written - // to the next WAL. - active_checkpoint.GetCheckpointTransaction(options); - } else { - options.checkpoint_id = transaction_manager.NextCheckpointId(); - options.visibility_bound = VisibilityBound::Through(transaction_manager.GetLastCommit()); - } + // While holding the commit lock: any new commits are written to the next WAL + active_checkpoint.Begin(options); D_ASSERT(options.checkpoint_id.IsValid()); DUCKDB_LOG(db.GetDatabase(), TransactionLogType, db, "Start Checkpoint", options.checkpoint_id.GetIndex()); @@ -346,8 +340,8 @@ void StorageManager::WALFinishCheckpoint(unique_lock &) { DUCKDB_LOG(db.GetDatabase(), TransactionLogType, db, "Finish Checkpoint"); } -unique_lock StorageManager::GetWALLock() { - return unique_lock(wal_lock); +unique_lock StorageManager::GetCommitLock() { + return unique_lock(commit_lock); } string StorageManager::GetWALPath(const string &suffix) { @@ -812,14 +806,14 @@ void SingleFileStorageManager::Destroy() { if (!load_complete) { return; } - vector> schemas; + vector> schemas; // we scan the set of committed schemas auto &catalog = Catalog::GetCatalog(db).Cast(); - catalog.ScanSchemas([&](SchemaCatalogEntry &entry) { schemas.push_back(entry); }); + catalog.ScanSchemas(VisibilityBound::AllCommitted(), [&](DuckSchemaEntry &entry) { schemas.push_back(entry); }); vector> tables; for (auto &schema : schemas) { - schema.get().Scan(CatalogType::TABLE_ENTRY, [&](CatalogEntry &entry) { + schema.get().Scan(CatalogType::TABLE_ENTRY, VisibilityBound::AllCommitted(), [&](CatalogEntry &entry) { if (entry.internal) { return; } diff --git a/src/duckdb/src/storage/table/in_memory_checkpoint.cpp b/src/duckdb/src/storage/table/in_memory_checkpoint.cpp index b80bea326..a294abd46 100644 --- a/src/duckdb/src/storage/table/in_memory_checkpoint.cpp +++ b/src/duckdb/src/storage/table/in_memory_checkpoint.cpp @@ -1,13 +1,17 @@ #include "duckdb/storage/table/in_memory_checkpoint.hpp" #include "duckdb/main/attached_database.hpp" +#include "duckdb/catalog/catalog_entry/duck_schema_entry.hpp" #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" #include "duckdb/catalog/duck_catalog.hpp" #include "duckdb/common/enums/checkpoint_abort.hpp" #include "duckdb/common/serializer/binary_serializer.hpp" #include "duckdb/main/settings.hpp" #include "duckdb/storage/data_table.hpp" +#include "duckdb/storage/storage_manager.hpp" #include "duckdb/storage/table/column_segment.hpp" +#include "duckdb/storage/table/table_index_list.hpp" +#include "duckdb/transaction/duck_transaction_manager.hpp" namespace duckdb { @@ -22,15 +26,32 @@ InMemoryCheckpointer::InMemoryCheckpointer(QueryContext context, AttachedDatabas } void InMemoryCheckpointer::CreateCheckpoint() { - vector> schemas; - // we scan the set of committed schemas + auto &transaction_manager = DuckTransactionManager::Get(db); + // appends that run alongside the checkpoint start new row groups, index changes go to the checkpoint deltas + ActiveCheckpointWrapper active_checkpoint(context, db, transaction_manager); + { + unique_lock commit_lock; + if (!options.commit_lock) { + // a checkpoint instead of a WAL write already holds the commit lock + commit_lock = storage_manager.GetCommitLock(); + } + if (options.type == CheckpointType::FULL_CHECKPOINT && + transaction_manager.GetLastCommit() >= transaction_manager.LowestVisibilityBound()) { + // an active snapshot still needs state a full checkpoint would vacuum away + options.type = CheckpointType::CONCURRENT_CHECKPOINT; + } + active_checkpoint.Begin(options); + } + + vector> schemas; + // the schemas and tables committed before the checkpoint's bound auto &catalog = Catalog::GetCatalog(db).Cast(); - catalog.ScanSchemas([&](SchemaCatalogEntry &entry) { schemas.push_back(entry); }); + catalog.ScanSchemas(options.visibility_bound, [&](DuckSchemaEntry &entry) { schemas.push_back(entry); }); vector> tables; for (const auto &schema_ref : schemas) { auto &schema = schema_ref.get(); - schema.Scan(CatalogType::TABLE_ENTRY, [&](CatalogEntry &entry) { + schema.Scan(CatalogType::TABLE_ENTRY, options.visibility_bound, [&](CatalogEntry &entry) { if (entry.type == CatalogType::TABLE_ENTRY) { tables.push_back(entry.Cast()); } @@ -47,7 +68,13 @@ void InMemoryCheckpointer::CreateCheckpoint() { throw IOException("In-memory checkpoint aborted because of PRAGMA debug_checkpoint_abort flag"); } } + for (auto &table : tables) { + // index entries added or removed while the checkpoint ran went to delta indexes + auto &index_list = table.get().GetStorage().GetDataTableInfo()->GetIndexes(); + index_list.MergeCheckpointDeltas(options.checkpoint_id); + } storage_manager.SetWALSize(0); + active_checkpoint.Commit(); } MetadataWriter &InMemoryCheckpointer::GetMetadataWriter() { @@ -64,10 +91,7 @@ void InMemoryCheckpointer::WriteTable(TableCatalogEntry &table, Serializer &seri InMemoryTableDataWriter data_writer(*this, table); // Write the table data - auto table_lock = table.GetStorage().GetCheckpointLock(); table.GetStorage().Checkpoint(data_writer, serializer); - // flush any partial blocks BEFORE releasing the table lock - // flushing partial blocks updates where data lives and is not thread-safe partial_block_manager.FlushPartialBlocks(); } @@ -103,7 +127,9 @@ void InMemoryTableDataWriter::WriteUnchangedTable(MetaBlockPointer pointer, } void InMemoryTableDataWriter::FinalizeTable(const TableStatistics &global_stats, DataTableInfo &info, - RowGroupCollection &collection, Serializer &serializer) { + RowGroupCollection &collection, + const vector> &index_infos, + Serializer &serializer) { // nop: no need to write anything } @@ -112,6 +138,10 @@ unique_ptr InMemoryTableDataWriter::GetRowGroupWriter(RowGroup & checkpoint_manager); } +unique_ptr InMemoryTableDataWriter::GetTableIndexWriter(StorageVersion) { + return nullptr; +}; + void InMemoryTableDataWriter::FlushPartialBlocks() { } diff --git a/src/duckdb/src/storage/table/row_group.cpp b/src/duckdb/src/storage/table/row_group.cpp index 7b26acfe7..0f7fb3a9d 100644 --- a/src/duckdb/src/storage/table/row_group.cpp +++ b/src/duckdb/src/storage/table/row_group.cpp @@ -39,15 +39,15 @@ namespace duckdb { RowGroup::RowGroup(RowGroupCollection &collection_p, idx_t count) - : SegmentBase(count), collection(collection_p), version_info(nullptr), deletes_is_loaded(false), + : SegmentBase(count), collection(collection_p), version_info(make_shared_ptr()), allocation_size(0), row_id_is_loaded(false), row_number_is_loaded(false), has_changes(false) { Verify(); } RowGroup::RowGroup(RowGroupCollection &collection_p, RowGroupPointer pointer) - : SegmentBase(pointer.tuple_count), collection(collection_p), version_info(nullptr), - deletes_is_loaded(false), allocation_size(0), row_id_is_loaded(false), row_number_is_loaded(false), - has_changes(false) { + : SegmentBase(pointer.tuple_count), collection(collection_p), + version_info(make_shared_ptr(std::move(pointer.deletes_pointers))), allocation_size(0), + row_id_is_loaded(false), row_number_is_loaded(false), has_changes(false) { // deserialize the columns if (pointer.data_pointers.size() != collection_p.GetTypes().size()) { throw DataCorruptionException("Row group column count is unaligned with table column count. Corrupt file?"); @@ -58,7 +58,6 @@ RowGroup::RowGroup(RowGroupCollection &collection_p, RowGroupPointer pointer) for (idx_t c = 0; c < columns.size(); c++) { this->is_loaded[c] = false; } - this->deletes_pointers = std::move(pointer.deletes_pointers); this->has_metadata_blocks = pointer.has_metadata_blocks; this->extra_metadata_blocks = std::move(pointer.extra_metadata_blocks); this->has_per_column_metadata_blocks = pointer.has_per_column_metadata_blocks; @@ -68,7 +67,7 @@ RowGroup::RowGroup(RowGroupCollection &collection_p, RowGroupPointer pointer) } RowGroup::RowGroup(RowGroupCollection &collection_p, PersistentRowGroupData &data) - : SegmentBase(data.count), collection(collection_p), version_info(nullptr), deletes_is_loaded(false), + : SegmentBase(data.count), collection(collection_p), version_info(make_shared_ptr()), allocation_size(0), row_id_is_loaded(false), row_number_is_loaded(false), has_changes(false) { auto &block_manager = GetBlockManager(); auto &info = GetTableInfo(); @@ -444,10 +443,7 @@ bool RowGroup::InitializeScan(CollectionScanState &state, SegmentNode unique_ptr RowGroup::CreateNewRowGroupCopy(RowGroupCollection &new_collection, idx_t new_column_count) { auto row_group = make_uniq(new_collection, this->count); - row_group->deletes_pointers = deletes_pointers; - row_group->deletes_is_loaded = deletes_is_loaded.load(); - row_group->owned_version_info = owned_version_info; - row_group->version_info = version_info.load(); + row_group->version_info = version_info; row_group->columns.resize(new_column_count); if (is_loaded) { row_group->is_loaded = unique_ptr[]>(new atomic[new_column_count]); @@ -1138,64 +1134,65 @@ void RowGroup::Scan(CollectionScanState &state, DataChunk &result, TableScanType Scan(options, state, result); } -optional_ptr RowGroup::GetVersionInfo() { +//===--------------------------------------------------------------------===// +// RowGroupVersionInfo +//===--------------------------------------------------------------------===// +RowGroupVersionInfo::RowGroupVersionInfo(vector deletes_pointers_p) + : deletes_pointers(std::move(deletes_pointers_p)), deletes_is_loaded(false), version_info(nullptr) { +} + +bool RowGroupVersionInfo::HasUnloadedDeletes() const { + if (deletes_pointers.empty()) { + // no stored deletes at all + return false; + } + // return whether or not the deletes have been loaded + return !deletes_is_loaded; +} + +optional_ptr RowGroupVersionInfo::Get(BlockManager &block_manager) { if (!HasUnloadedDeletes()) { // deletes are loaded - return the version info - return version_info; + return version_info.load(); } - lock_guard lock(row_group_lock); + lock_guard guard(lock); // double-check after obtaining the lock whether or not deletes are still not loaded to avoid double load if (!HasUnloadedDeletes()) { - return version_info; + return version_info.load(); } D_ASSERT(!deletes_pointers.empty()); auto root_delete = deletes_pointers[0]; - auto loaded_info = RowVersionManager::Deserialize(root_delete, GetBlockManager().GetMetadataManager()); - SetVersionInfo(std::move(loaded_info)); - deletes_is_loaded = true; - return version_info; -} - -void RowGroup::SetVersionInfo(shared_ptr version) { - owned_version_info = std::move(version); + owned_version_info = RowVersionManager::Deserialize(root_delete, block_manager.GetMetadataManager()); version_info = owned_version_info.get(); + deletes_is_loaded = true; + return version_info.load(); } -shared_ptr RowGroup::GetOrCreateVersionInfoInternal() { - // version info does not exist - need to create it - lock_guard lock(row_group_lock); +shared_ptr RowGroupVersionInfo::GetOrCreate(BlockManager &block_manager) { + // load the persisted deletes first, so that a new manager does not shadow them + Get(block_manager); + lock_guard guard(lock); if (!owned_version_info) { - auto &buffer_manager = GetBlockManager().GetBufferManager(); - auto new_info = make_shared_ptr(buffer_manager); - SetVersionInfo(std::move(new_info)); + owned_version_info = make_shared_ptr(block_manager.GetBufferManager()); + version_info = owned_version_info.get(); } return owned_version_info; } +optional_ptr RowGroup::GetVersionInfo() { + return version_info->Get(GetBlockManager()); +} + shared_ptr RowGroup::GetOrCreateVersionInfoPtr() { - auto vinfo = GetVersionInfo(); - if (vinfo) { - // version info exists - return it directly - return owned_version_info; - } - return GetOrCreateVersionInfoInternal(); + return version_info->GetOrCreate(GetBlockManager()); } RowVersionManager &RowGroup::GetOrCreateVersionInfo() { - auto vinfo = GetVersionInfo(); - if (vinfo) { - // version info exists - return it directly - return *vinfo; - } - return *GetOrCreateVersionInfoInternal(); + return *GetOrCreateVersionInfoPtr(); } optional_ptr RowGroup::GetVersionInfoIfLoaded() const { - if (!HasUnloadedDeletes()) { - // deletes are loaded - return the version info - return version_info; - } - return nullptr; + return version_info->GetIfLoaded(); } idx_t RowGroup::GetSelVector(ScanOptions options, idx_t vector_idx, SelectionVector &sel_vector, idx_t max_count) { @@ -1508,8 +1505,7 @@ vector RowGroup::WriteToDisk(RowGroupWriteInfo &info, auto &row_group = row_groups[row_group_idx].get(); auto result_row_group = make_shared_ptr(row_group.GetCollection(), row_group.count); result_row_group->columns = std::move(result_columns[row_group_idx]); - result_row_group->version_info = row_group.version_info.load(); - result_row_group->owned_version_info = row_group.owned_version_info; + result_row_group->version_info = row_group.version_info; row_group_write_data.result_row_group = std::move(result_row_group); } @@ -1550,12 +1546,7 @@ idx_t RowGroup::GetVisibleRowCount(TransactionData transaction, idx_t start_vect } bool RowGroup::HasUnloadedDeletes() const { - if (deletes_pointers.empty()) { - // no stored deletes at all - return false; - } - // return whether or not the deletes have been loaded - return !deletes_is_loaded; + return version_info->HasUnloadedDeletes(); } PerColumnMetadataBlocks RowGroup::ComputePerColumnMetadataBlocks() const { @@ -1633,7 +1624,7 @@ bool RowGroup::HasUnchangedColumns() const { RowGroupWriteData RowGroup::WriteToDisk(RowGroupWriter &writer) { bool can_reuse_metadata = CanReuseMetadata(writer); - if (can_reuse_metadata && !HasChanges()) { + if (can_reuse_metadata && !HasChanges(writer.GetCheckpointOptions().visibility_bound)) { RowGroupWriteData result; result.write_action = RowGroupWriteAction::REUSE_EXISTING_ROW_GROUP_METADATA; if (GetCollection().SupportsPerColumnWrites()) { @@ -1679,10 +1670,8 @@ RowGroupWriteData RowGroup::WriteToDisk(RowGroupWriter &writer) { auto result_row_group = make_shared_ptr(GetCollection(), this->count); result_row_group->columns.resize(GetColumnCount()); result_row_group->column_pointers.resize(GetColumnCount()); - result_row_group->deletes_pointers = deletes_pointers; - result_row_group->deletes_is_loaded = deletes_is_loaded.load(); - result_row_group->owned_version_info = owned_version_info; - result_row_group->version_info = version_info.load(); + // the rewritten row group shares the version info: deletes stay visible through both + result_row_group->version_info = version_info; if (is_loaded) { result_row_group->is_loaded = unique_ptr[]>(new atomic[GetColumnCount()]); for (idx_t c = 0; c < GetColumnCount(); c++) { @@ -1921,12 +1910,12 @@ RowGroupPointer RowGroup::Checkpoint(RowGroupWriteData write_data, RowGroupWrite return row_group_pointer; } -bool RowGroup::HasChanges() const { +bool RowGroup::HasChanges(VisibilityBound bound) const { if (has_changes) { return true; } - auto version_info_loaded = version_info.load(); - if (version_info_loaded && version_info_loaded->HasUnserializedChanges()) { + auto version_info_loaded = version_info->GetIfLoaded(); + if (version_info_loaded && version_info_loaded->HasUnserializedChanges(bound)) { // we have deletes return true; } @@ -1981,6 +1970,7 @@ vector RowGroup::CheckpointDeletes(RowGroupWriter &writer) { // deletes were not loaded so they cannot be changed // re-use them as-is auto &manager = *writer.GetMetadataManager(); + auto &deletes_pointers = version_info->GetDeletesPointers(); manager.ClearModifiedBlocks(deletes_pointers); return deletes_pointers; } @@ -2054,7 +2044,7 @@ struct DuckDBPartitionRowGroup : public PartitionRowGroup { } bool HasPendingWrites() override { - return row_group->HasChanges(); + return row_group->HasChanges(VisibilityBound::AllCommitted()); } }; diff --git a/src/duckdb/src/storage/table/row_group_collection.cpp b/src/duckdb/src/storage/table/row_group_collection.cpp index 42c532e3e..73064f80b 100644 --- a/src/duckdb/src/storage/table/row_group_collection.cpp +++ b/src/duckdb/src/storage/table/row_group_collection.cpp @@ -3,6 +3,7 @@ #include "duckdb/transaction/commit_state.hpp" #include "duckdb/common/serializer/binary_deserializer.hpp" +#include "duckdb/common/thread.hpp" #include "duckdb/common/vector/flat_vector.hpp" #include "duckdb/execution/expression_executor.hpp" #include "duckdb/execution/index/art/art.hpp" @@ -1088,10 +1089,9 @@ void RowGroupCollection::UpdateColumn(TransactionData transaction, DuckTableEntr //===--------------------------------------------------------------------===// struct CollectionCheckpointState { CollectionCheckpointState(RowGroupCollection &collection, TableDataWriter &writer, TableStatistics &global_stats, - RowGroupSegmentTree &row_groups) + RowGroupSegmentTree &row_groups, idx_t segment_count) : collection(collection), writer(writer), executor(writer.CreateTaskExecutor()), global_stats(global_stats), row_groups(row_groups) { - auto segment_count = row_groups.GetSegmentCount(); writers.resize(segment_count); write_data.resize(segment_count); dropped_segments = make_uniq_array(segment_count); @@ -1496,7 +1496,12 @@ void RowGroupCollection::InitializeVacuumState(CollectionCheckpointState &checkp // we record a gap as seen; if we later see a row group with live rows, then we know rowids have to be shifted. bool rowid_gap_seen = false; vector committed_counts; + idx_t visited = 0; for (auto &entry : checkpoint_state.row_groups.SegmentNodes()) { + if (visited++ >= num_row_groups) { + // appended after the snapshot + break; + } auto &row_group = entry.GetNode(); auto row_group_num_rows = row_group.GetCommittedRowCount(); if (legacy_vacuum_with_stable_row_ids) { @@ -1709,10 +1714,27 @@ unique_ptr RowGroupCollection::GetCheckpointTask(CollectionCheck return make_uniq(checkpoint_state, segment_idx); } -void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &global_stats) { - auto row_groups = GetRowGroups(); +CollectionCheckpointSnapshot RowGroupCollection::SnapshotForCheckpoint(TableDataWriter &writer) const { + // appends resume once the append lock is released: record the number of row groups now + CollectionCheckpointSnapshot snapshot; + snapshot.row_groups = GetRowGroups(); + snapshot.segment_count = snapshot.row_groups->GetSegmentCount(); + snapshot.row_group_count = info->CheckpointRowGroupCount(writer.GetCheckpointOptions()); + writer.SetRowGroupCount(snapshot.row_group_count); + return snapshot; +} - CollectionCheckpointState checkpoint_state(*this, writer, global_stats, *row_groups); +CollectionCheckpointResult RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &global_stats, + const CollectionCheckpointSnapshot &snapshot) { + auto &row_groups = snapshot.row_groups; + // row groups past this count were appended after the checkpoint started and are taken over at install time + idx_t checkpointed_row_group_count = snapshot.segment_count; + if (snapshot.row_group_count.IsValid()) { + // appends that saw this checkpoint before the snapshot already started new row groups: those stay in the WAL + checkpointed_row_group_count = + MinValue(checkpointed_row_group_count, snapshot.row_group_count.GetIndex()); + } + CollectionCheckpointState checkpoint_state(*this, writer, global_stats, *row_groups, checkpointed_row_group_count); VacuumState vacuum_state; InitializeVacuumState(checkpoint_state, vacuum_state, writer.GetRowGroupCount()); @@ -1723,7 +1745,7 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl // schedule tasks idx_t total_vacuum_tasks = 0; auto max_vacuum_tasks = Settings::Get(writer.GetDatabase()); - for (idx_t segment_idx = 0; segment_idx < checkpoint_state.SegmentCount(); segment_idx++) { + for (idx_t segment_idx = 0; segment_idx < checkpointed_row_group_count; segment_idx++) { auto vacuum_tasks = ScheduleVacuumTasks(checkpoint_state, vacuum_state, segment_idx, total_vacuum_tasks < max_vacuum_tasks); if (vacuum_tasks) { @@ -1762,11 +1784,16 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl // all tasks have been successfully scheduled - execute tasks until we are done checkpoint_state.executor->WorkOnTasks(); + auto scan_sleep_ms = Settings::Get(writer.GetDatabase()); + if (scan_sleep_ms > 0) { + ThreadUtil::SleepMs(scan_sleep_ms); + } + // no errors - finalize the row groups // if the table already exists on disk - check if all row groups have stayed the same if (Settings::Get(writer.GetDatabase()) && metadata_pointer.IsValid()) { bool table_has_changes = false; - for (idx_t segment_idx = 0; segment_idx < checkpoint_state.SegmentCount(); segment_idx++) { + for (idx_t segment_idx = 0; segment_idx < checkpointed_row_group_count; segment_idx++) { if (checkpoint_state.SegmentIsDropped(segment_idx)) { table_has_changes = true; break; @@ -1782,7 +1809,7 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl // we can directly re-use the metadata pointer // mark all blocks associated with row groups as still being in-use auto &metadata_manager = writer.GetMetadataManager(); - for (idx_t segment_idx = 0; segment_idx < checkpoint_state.SegmentCount(); segment_idx++) { + for (idx_t segment_idx = 0; segment_idx < checkpointed_row_group_count; segment_idx++) { auto entry = checkpoint_state.GetSegment(segment_idx); auto &row_group = entry->GetNode(); metadata_manager.ClearModifiedBlocks(row_group.GetColumnStartPointers()); @@ -1793,26 +1820,39 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl auto row_group_writer = checkpoint_state.writer.GetRowGroupWriter(row_group); row_group.CheckpointDeletes(*row_group_writer); } - writer.WriteUnchangedTable(metadata_pointer, metadata_pointers, total_rows.load(), next_row_id.load()); + // the totals as of the snapshot: rows appended since stay in the WAL + idx_t checkpoint_total_rows = 0; + idx_t checkpoint_next_row_id = 0; + auto base_row_id = row_groups->GetBaseRowId(); + for (idx_t segment_idx = 0; segment_idx < checkpointed_row_group_count; segment_idx++) { + auto entry = checkpoint_state.GetSegment(segment_idx); + idx_t count = entry->GetNode().count; + checkpoint_total_rows += count; + checkpoint_next_row_id = + MaxValue(checkpoint_next_row_id, entry->GetRowStart() + count - base_row_id); + } + writer.WriteUnchangedTable(metadata_pointer, metadata_pointers, checkpoint_total_rows, + checkpoint_next_row_id); // copy over existing stats into the global stats CopyStats(global_stats); - return; + return CollectionCheckpointResult(); } } // not all segments have stayed the same - we need to make a new segment tree with the new set of segments auto new_row_groups = make_shared_ptr(*this, row_groups->GetBaseRowId()); - auto l = new_row_groups->Lock(); - // initialize new empty stats + // initialize new empty stats (before locking the new tree: ALTER takes the statistics lock before a tree lock) global_stats.InitializeEmpty(stats); + auto l = new_row_groups->Lock(); + idx_t new_total_rows = 0; idx_t new_next_row_id = 0; auto base_row_id = row_groups->GetBaseRowId(); auto can_persist_rowid_gaps = writer.CanPersistRowIdGaps(); unordered_set columns_with_incomplete_stats; - for (idx_t segment_idx = 0; segment_idx < checkpoint_state.SegmentCount(); segment_idx++) { + for (idx_t segment_idx = 0; segment_idx < checkpointed_row_group_count; segment_idx++) { auto entry = checkpoint_state.GetSegment(segment_idx); if (!entry) { // row group was vacuumed/dropped - skip @@ -2036,35 +2076,98 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl } } } + l.Release(); if (!columns_with_incomplete_stats.empty()) { // for any columns that have incomplete stats we need to merge in the previous global stats to ensure the stats // are correct — use EXPAND_BOUNDS so additive stats (e.g. total_string_length) are invalidated rather than // double-counted (the collection stats include contributions from all row groups, including those already // merged) + // the two statistics locks are never nested (see InstallCheckpoint) + vector live_column_stats; + { + auto stats_lock = stats.GetLock(); + for (auto &column_idx : columns_with_incomplete_stats) { + live_column_stats.push_back(stats.GetStats(*stats_lock, column_idx).Statistics().Copy()); + } + } auto lock = global_stats.GetLock(); + idx_t stats_idx = 0; for (auto &column_idx : columns_with_incomplete_stats) { - auto stats_lock = stats.GetLock(); - auto &column_stats = stats.GetStats(*stats_lock, column_idx); - global_stats.MergeStats(*lock, column_idx, column_stats.Statistics(), StatsMergeType::EXPAND_BOUNDS); + global_stats.MergeStats(*lock, column_idx, live_column_stats[stats_idx++], StatsMergeType::EXPAND_BOUNDS); } } - l.Release(); // flush any partial blocks BEFORE updating the row group pointer // flushing partial blocks updates where data lives // this cannot be done after other threads start scanning the row groups // so this HAS to happen before we call "SetRowGroups" to update the row groups writer.FlushPartialBlocks(); - // override the row group segment tree - total_rows = new_total_rows; - next_row_id = new_next_row_id; - D_ASSERT(next_row_id.load() >= total_rows.load()); - SetRowGroups(std::move(new_row_groups)); - Verify(); // Rebuild indexes if the REBUILD strategy was chosen (legacy vacuum_rebuild_indexes path) and rowids changed. if (vacuum_state.index_strategy == VacuumIndexStrategy::REBUILD && writer.RowIdsChanged()) { writer.SetRebuildIndexes(); } + + CollectionCheckpointResult result; + result.row_groups = std::move(new_row_groups); + result.total_rows = new_total_rows; + result.next_row_id = new_next_row_id; + result.checkpointed_row_group_count = checkpointed_row_group_count; + return result; +} + +void RowGroupCollection::InstallCheckpoint(CollectionCheckpointResult result, TableStatistics &checkpoint_stats) { + if (!result.row_groups) { + // the table was unchanged: the live statistics already describe it + return; + } + auto &new_row_groups = result.row_groups; + vector> appended_row_groups; + { + // take over the row groups appended while the checkpoint was running + auto live_row_groups = GetRowGroups(); + // lock order as in Checkpoint: the new tree, then the one it was written from + auto new_lock = new_row_groups->Lock(); + auto live_lock = live_row_groups->Lock(); + idx_t live_count = live_row_groups->GetSegmentCount(live_lock); + for (idx_t segment_idx = result.checkpointed_row_group_count; segment_idx < live_count; segment_idx++) { + auto entry = live_row_groups->GetSegmentByIndex(live_lock, UnsafeNumericCast(segment_idx)); + auto row_start = entry->GetRowStart(); + // appended row groups start past the checkpointed ones + D_ASSERT(row_start >= new_row_groups->GetBaseRowId() + result.next_row_id); + result.total_rows += entry->GetNode().count; + new_row_groups->AppendSegment(new_lock, entry->ReferenceNode(), row_start); + appended_row_groups.push_back(entry->GetNode()); + } + if (live_count > result.checkpointed_row_group_count) { + // the appends and reverts that ran meanwhile kept next_row_id up to date + result.next_row_id = next_row_id.load(); + } + } + { + // the live statistics become those of the checkpoint plus those of the row groups appended meanwhile + // the two statistics locks are never nested: ALTER takes the live one before a tree lock + vector checkpoint_column_stats; + { + auto checkpoint_lock = checkpoint_stats.GetLock(); + for (idx_t column_idx = 0; column_idx < types.size(); column_idx++) { + checkpoint_column_stats.push_back( + checkpoint_stats.GetStats(*checkpoint_lock, column_idx).Statistics().Copy()); + } + } + auto lock = stats.GetLock(); + for (idx_t column_idx = 0; column_idx < types.size(); column_idx++) { + auto &column_stats = stats.GetStats(*lock, column_idx).Statistics(); + column_stats = std::move(checkpoint_column_stats[column_idx]); + for (auto &row_group : appended_row_groups) { + column_stats.Merge(*row_group.get().GetStatistics(column_idx)); + } + } + } + total_rows = result.total_rows; + next_row_id = result.next_row_id; + D_ASSERT(next_row_id.load() >= total_rows.load()); + SetRowGroups(std::move(new_row_groups)); + Verify(); } //===--------------------------------------------------------------------===// @@ -2340,10 +2443,6 @@ void RowGroupCollection::VerifyNewConstraint(const QueryContext &context, DuckTr // Statistics //===---------------------------------------------------------------r-----===// -void RowGroupCollection::SetStats(TableStatistics &new_stats) { - stats.SetStats(new_stats); -} - void RowGroupCollection::CopyStats(TableStatistics &other_stats) { stats.CopyStats(other_stats); } diff --git a/src/duckdb/src/storage/table/row_version_manager.cpp b/src/duckdb/src/storage/table/row_version_manager.cpp index 9f9f1f637..01f74d1a4 100644 --- a/src/duckdb/src/storage/table/row_version_manager.cpp +++ b/src/duckdb/src/storage/table/row_version_manager.cpp @@ -204,8 +204,16 @@ idx_t RowVersionManager::DeleteRows(idx_t vector_idx, transaction_t transaction_ void RowVersionManager::CommitDelete(idx_t vector_idx, transaction_t commit_id, const DeleteInfo &info) { lock_guard lock(version_lock); needs_compression_check = true; - if (!uncheckpointed_delete_commit.IsValid() || commit_id > uncheckpointed_delete_commit.GetIndex()) { - uncheckpointed_delete_commit = commit_id; + if (commit_id != NOT_DELETED_ID) { + // a reverted delete changes nothing a checkpoint writes + if (!newest_uncheckpointed_delete_commit.IsValid() || + commit_id > newest_uncheckpointed_delete_commit.GetIndex()) { + newest_uncheckpointed_delete_commit = commit_id; + } + if (!oldest_uncheckpointed_delete_commit.IsValid() || + commit_id < oldest_uncheckpointed_delete_commit.GetIndex()) { + oldest_uncheckpointed_delete_commit = commit_id; + } } GetVectorInfo(vector_idx).CommitDelete(commit_id, info); } @@ -244,8 +252,9 @@ vector RowVersionManager::Checkpoint(RowGroupWriter &writer) { lock_guard lock(version_lock); auto &manager = *writer.GetMetadataManager(); auto options = writer.GetCheckpointOptions(); - if (!uncheckpointed_delete_commit.IsValid()) { - // we can write the current pointer as-is + if (!oldest_uncheckpointed_delete_commit.IsValid() || + oldest_uncheckpointed_delete_commit.GetIndex() >= options.visibility_bound) { + // nothing below the bound changed since the last checkpoint; a delete above it is written by the next one // ensure the blocks we are pointing to are not marked as free manager.ClearModifiedBlocks(storage_pointers); // return the current set of pointers @@ -279,10 +288,12 @@ vector RowVersionManager::Checkpoint(RowGroupWriter &writer) { metadata_writer.Flush(); } - if (uncheckpointed_delete_commit.IsValid() && uncheckpointed_delete_commit.GetIndex() < options.visibility_bound) { + if (newest_uncheckpointed_delete_commit.IsValid() && + newest_uncheckpointed_delete_commit.GetIndex() < options.visibility_bound) { // the last checkpointed id was either before or on the transaction we are checkpointing // nothing to checkpoint in future commits until more deletes appear - uncheckpointed_delete_commit = optional_idx(); + newest_uncheckpointed_delete_commit = optional_idx(); + oldest_uncheckpointed_delete_commit = optional_idx(); } return storage_pointers; } @@ -313,13 +324,14 @@ shared_ptr RowVersionManager::Deserialize(MetaBlockPointer de } version_info->vector_info[vector_index] = std::move(info); } - version_info->uncheckpointed_delete_commit = optional_idx(); + version_info->newest_uncheckpointed_delete_commit = optional_idx(); + version_info->oldest_uncheckpointed_delete_commit = optional_idx(); return version_info; } -bool RowVersionManager::HasUnserializedChanges() { +bool RowVersionManager::HasUnserializedChanges(VisibilityBound bound) { lock_guard lock(version_lock); - return uncheckpointed_delete_commit.IsValid(); + return oldest_uncheckpointed_delete_commit.IsValid() && oldest_uncheckpointed_delete_commit.GetIndex() < bound; } bool RowVersionManager::HasDeletes() { @@ -344,7 +356,7 @@ bool RowVersionManager::HasUncommittedChanges() { vector RowVersionManager::GetStoragePointers() { lock_guard lock(version_lock); - D_ASSERT(!uncheckpointed_delete_commit.IsValid()); + D_ASSERT(!newest_uncheckpointed_delete_commit.IsValid()); return storage_pointers; } diff --git a/src/duckdb/src/storage/table/table_statistics.cpp b/src/duckdb/src/storage/table/table_statistics.cpp index 38ecd9a62..3208cdef5 100644 --- a/src/duckdb/src/storage/table/table_statistics.cpp +++ b/src/duckdb/src/storage/table/table_statistics.cpp @@ -28,8 +28,11 @@ void TableStatistics::Initialize(const vector &types, PersistentTab void TableStatistics::InitializeEmpty(const TableStatistics &other) { D_ASSERT(Empty()); D_ASSERT(!table_sample); + D_ASSERT(other.stats_lock); stats_lock = make_shared_ptr(); + // concurrent appends merge into the other statistics + lock_guard other_lock(*other.stats_lock); if (other.table_sample) { D_ASSERT(other.table_sample->type == SampleType::RESERVOIR_SAMPLE); auto &res = other.table_sample->Cast(); @@ -193,12 +196,6 @@ void TableStatistics::DestroyTableSample(TableStatisticsLock &lock) const { } } -void TableStatistics::SetStats(TableStatistics &other) { - TableStatisticsLock lock(*stats_lock); - column_stats = std::move(other.column_stats); - table_sample = std::move(other.table_sample); -} - unique_ptr TableStatistics::CopyStats(const StorageIndex &index) { lock_guard l(*stats_lock); diff --git a/src/duckdb/src/storage/table/update_segment.cpp b/src/duckdb/src/storage/table/update_segment.cpp index 0298b996d..73118deab 100644 --- a/src/duckdb/src/storage/table/update_segment.cpp +++ b/src/duckdb/src/storage/table/update_segment.cpp @@ -11,12 +11,12 @@ namespace duckdb { -static UpdateSegment::initialize_update_function_t GetInitializeUpdateFunction(PhysicalType type); static UpdateSegment::fetch_update_function_t GetFetchUpdateFunction(PhysicalType type); static UpdateSegment::fetch_committed_function_t GetFetchCommittedFunction(PhysicalType type); static UpdateSegment::fetch_committed_range_function_t GetFetchCommittedRangeFunction(PhysicalType type); static UpdateSegment::merge_update_function_t GetMergeUpdateFunction(PhysicalType type); +static UpdateSegment::save_undo_function_t GetSaveUndoFunction(PhysicalType type); static UpdateSegment::rollback_update_function_t GetRollbackUpdateFunction(PhysicalType type); static UpdateSegment::statistics_update_function_t GetStatisticsUpdateFunction(PhysicalType type); static UpdateSegment::fetch_rows_function_t GetFetchRowsFunction(PhysicalType type); @@ -28,12 +28,12 @@ UpdateSegment::UpdateSegment(ColumnData &column_data) this->type_size = GetTypeIdSize(physical_type); - this->initialize_update_function = GetInitializeUpdateFunction(physical_type); this->fetch_update_function = GetFetchUpdateFunction(physical_type); this->fetch_committed_function = GetFetchCommittedFunction(physical_type); this->fetch_committed_range = GetFetchCommittedRangeFunction(physical_type); this->fetch_rows_function = GetFetchRowsFunction(physical_type); this->merge_update_function = GetMergeUpdateFunction(physical_type); + this->save_undo_function = GetSaveUndoFunction(physical_type); this->rollback_update_function = GetRollbackUpdateFunction(physical_type); this->statistics_update_function = GetStatisticsUpdateFunction(physical_type); this->get_effective_updates = GetEffectiveUpdatesFunction(physical_type); @@ -68,8 +68,13 @@ string UpdateInfo::ToString() { string result = "Update Info [" + type.ToString() + ", Count: " + to_string(N) + ", Transaction Id: " + to_string(version_number.load()) + "]\n"; auto tuples = GetTuples(); + idx_t value_idx = 0; for (idx_t i = 0; i < N; i++) { - result += to_string(tuples[i]) + ": " + GetValue(i).ToString() + "\n"; + if (value_mask_offset != 0 && !GetValueMask().RowIsValid(i)) { + result += to_string(tuples[i]) + ": (base)\n"; + continue; + } + result += to_string(tuples[i]) + ": " + GetValue(value_idx++).ToString() + "\n"; } if (HasNext()) { auto next_pin = next.Pin(); @@ -82,8 +87,19 @@ sel_t *UpdateInfo::GetTuples() { return reinterpret_cast(data_ptr_cast(this) + sizeof(UpdateInfo)); } +static idx_t GetValueMaskOffset(idx_t capacity) { + return AlignValue(sizeof(UpdateInfo) + sizeof(sel_t) * capacity); +} + +static idx_t GetTransactionValuesOffset(idx_t capacity) { + return GetValueMaskOffset(capacity) + ValidityMask::ValidityMaskSize(capacity); +} + data_ptr_t UpdateInfo::GetValues() { - return reinterpret_cast(data_ptr_cast(this) + sizeof(UpdateInfo) + sizeof(sel_t) * max); + if (value_mask_offset != 0) { + return data_ptr_cast(this) + GetTransactionValuesOffset(max); + } + return data_ptr_cast(this) + sizeof(UpdateInfo) + sizeof(sel_t) * max; } UpdateInfo &UpdateInfo::Get(UndoBufferReference &entry) { @@ -99,14 +115,19 @@ bool UpdateInfo::HasNext() const { return next.entry; } -idx_t UpdateInfo::GetAllocSize(idx_t type_size) { - return AlignValue(sizeof(UpdateInfo) + (sizeof(sel_t) + type_size) * STANDARD_VECTOR_SIZE); +ValidityMask UpdateInfo::GetValueMask() { + D_ASSERT(value_mask_offset != 0); + return ValidityMask(reinterpret_cast(data_ptr_cast(this) + value_mask_offset), max); } idx_t UpdateInfo::GetAllocSize(idx_t type_size, idx_t capacity) { return AlignValue(sizeof(UpdateInfo) + (sizeof(sel_t) + type_size) * capacity); } +idx_t UpdateInfo::GetAllocSize(idx_t type_size, idx_t capacity, idx_t value_capacity) { + return AlignValue(GetTransactionValuesOffset(capacity) + type_size * value_capacity); +} + idx_t UpdateInfo::GetCompactCapacity(idx_t count) { // Round up to next power of two, with a minimum of 8, capped at STANDARD_VECTOR_SIZE if (count <= 8) { @@ -128,21 +149,22 @@ void UpdateInfo::Initialize(UpdateInfo &info, DuckTableEntry &table_entry, trans info.next = UndoBufferPointer(); info.N = 0; info.version_number = transaction_id; + info.value_mask_offset = 0; + info.value_max = UnsafeNumericCast(capacity); } void UpdateInfo::Initialize(UpdateInfo &info, DuckTableEntry &table_entry, transaction_t transaction_id, - idx_t row_group_start) { - info.max = STANDARD_VECTOR_SIZE; - info.row_group_start = row_group_start; - info.version_number = transaction_id; - info.table = &table_entry; - info.segment = nullptr; - info.prev.entry = nullptr; - info.next.entry = nullptr; + idx_t row_group_start, idx_t capacity, idx_t value_capacity) { + D_ASSERT(value_capacity <= capacity); + Initialize(info, table_entry, transaction_id, row_group_start, capacity); + info.value_mask_offset = GetValueMaskOffset(capacity); + info.value_max = UnsafeNumericCast(value_capacity); } void UpdateInfo::Verify() { #ifdef DEBUG + D_ASSERT(N <= max); + D_ASSERT(value_mask_offset == 0 || GetValueMask().CountValid(N) <= value_max); auto tuples = GetTuples(); for (idx_t i = 1; i < N; i++) { D_ASSERT(tuples[i] > tuples[i - 1] && tuples[i] < STANDARD_VECTOR_SIZE); @@ -161,12 +183,6 @@ static void MergeValidityInfo(UpdateInfo ¤t, ValidityMask &result_mask) { } } -static void UpdateMergeValidity(const SnapshotView &view, UpdateInfo &info, Vector &result) { - auto &result_mask = FlatVector::ValidityMutable(result); - UpdateInfo::UpdatesForTransaction(info, view, - [&](UpdateInfo ¤t) { MergeValidityInfo(current, result_mask); }); -} - template static void MergeUpdateInfo(UpdateInfo ¤t, T *result_data) { auto tuples = current.GetTuples(); @@ -183,11 +199,123 @@ static void MergeUpdateInfo(UpdateInfo ¤t, T *result_data) { } } +template +struct UpdateDataResult { + explicit UpdateDataResult(Vector &result) : data(FlatVector::GetDataMutable(result)) { + } + T Get(idx_t idx) const { + return data[idx]; + } + void Set(idx_t idx, T value) { + data[idx] = value; + } + void Apply(UpdateInfo &info) { + MergeUpdateInfo(info, data); + } + + T *data; +}; + +struct UpdateValidityResult { + explicit UpdateValidityResult(Vector &result) : mask(FlatVector::ValidityMutable(result)) { + } + bool Get(idx_t idx) const { + return mask.RowIsValid(idx); + } + void Set(idx_t idx, bool value) { + mask.Set(idx, value); + } + void Apply(UpdateInfo &info) { + MergeValidityInfo(info, mask); + } + + ValidityMask &mask; +}; + +//! Apply the stored values of an update info to the tuples that are valid in the apply mask +template +static void ApplyUpdateInfo(UpdateInfo &info, const ValidityMask &apply_mask, RESULT &result) { + bool has_value_mask = info.value_mask_offset != 0; + ValidityMask value_mask; + if (has_value_mask) { + value_mask = info.GetValueMask(); + } + if (info.N == STANDARD_VECTOR_SIZE && (!has_value_mask || value_mask.CheckAllValid(info.N))) { + // the update info contains every tuple of the vector with a value + if (apply_mask.CheckAllInvalid(STANDARD_VECTOR_SIZE)) { + return; + } + if (apply_mask.CheckAllValid(STANDARD_VECTOR_SIZE)) { + result.Apply(info); + return; + } + } + auto tuples = info.GetTuples(); + auto info_data = info.GetData(); + idx_t value_idx = 0; + for (idx_t i = 0; i < info.N; i++) { + if (has_value_mask && !value_mask.RowIsValid(i)) { + continue; + } + auto value = info_data[value_idx++]; + if (apply_mask.RowIsValid(tuples[i])) { + result.Set(tuples[i], value); + } + } +} + +template +static void FetchUpdatesInternal(const SnapshotView &view, UpdateInfo &root, RESULT &result) { + vector invisible; + UpdateInfo::GetInvisibleUpdates(root, view, invisible); + if (invisible.empty()) { + // the root holds the latest values + result.Apply(root); + return; + } + // a tuple without a stored value in an invisible update was first updated by that update: it keeps its base value + validity_t apply_bits[ValidityMask::STANDARD_ENTRY_COUNT]; + memset(apply_bits, 0xFF, sizeof(apply_bits)); + ValidityMask apply_mask(apply_bits, STANDARD_VECTOR_SIZE); + bool any_keep_base = false; + for (auto &pin : invisible) { + auto &info = UpdateInfo::Get(pin); + auto value_mask = info.GetValueMask(); + if (value_mask.CheckAllValid(info.N)) { + continue; + } + any_keep_base = true; + auto tuples = info.GetTuples(); + for (idx_t i = 0; i < info.N; i++) { + if (!value_mask.RowIsValid(i)) { + apply_mask.SetInvalid(tuples[i]); + } + } + } + if (!any_keep_base) { + // all values from before the invisible updates are stored in the update infos + result.Apply(root); + for (auto &pin : invisible) { + result.Apply(UpdateInfo::Get(pin)); + } + return; + } + ApplyUpdateInfo(root, apply_mask, result); + // undo the invisible updates from newest to oldest + for (auto &pin : invisible) { + ApplyUpdateInfo(UpdateInfo::Get(pin), apply_mask, result); + } +} + +static void UpdateMergeValidity(const SnapshotView &view, UpdateInfo &info, Vector &result) { + UpdateValidityResult validity_result(result); + FetchUpdatesInternal(view, info, validity_result); +} + template static void UpdateMergeFetch(const SnapshotView &view, UpdateInfo &info, Vector &result) { - auto result_data = FlatVector::GetDataMutable(result); - UpdateInfo::UpdatesForTransaction(info, view, - [&](UpdateInfo ¤t) { MergeUpdateInfo(current, result_data); }); + UpdateDataResult data_result(result); + FetchUpdatesInternal(view, info, data_result); } static UpdateSegment::fetch_update_function_t GetFetchUpdateFunction(PhysicalType type) { @@ -457,37 +585,55 @@ static bool FindUpdatedTuple(UpdateInfo ¤t, idx_t row_idx, idx_t &update_i return false; } -static void FetchRowsValidity(const SnapshotView &view, UpdateInfo &info, const idx_t *offsets, +template +static void FetchRowsInternal(const SnapshotView &view, UpdateInfo &root, const idx_t *offsets, const SelectionVector &sel, idx_t fetch_offset, idx_t count, idx_t vector_offset, - Vector &result, idx_t result_offset) { - auto &result_mask = FlatVector::ValidityMutable(result); - UpdateInfo::UpdatesForTransaction(info, view, [&](UpdateInfo ¤t) { - auto info_data = current.GetData(); - for (idx_t idx = 0; idx < count; idx++) { - const idx_t row_idx = offsets[sel.get_index(fetch_offset + idx)] - vector_offset; - idx_t update_idx; - if (FindUpdatedTuple(current, row_idx, update_idx)) { - result_mask.Set(result_offset + fetch_offset + idx, info_data[update_idx]); + RESULT &result, idx_t result_offset) { + vector invisible; + UpdateInfo::GetInvisibleUpdates(root, view, invisible); + auto root_data = root.GetData(); + for (idx_t idx = 0; idx < count; idx++) { + const idx_t row_idx = offsets[sel.get_index(fetch_offset + idx)] - vector_offset; + const idx_t result_idx = result_offset + fetch_offset + idx; + idx_t update_idx; + if (!FindUpdatedTuple(root, row_idx, update_idx)) { + // every update in the chain is also present in the root + continue; + } + auto base_value = result.Get(result_idx); + auto value = root_data[update_idx]; + // undo the invisible updates from newest to oldest + for (auto &pin : invisible) { + auto &info = UpdateInfo::Get(pin); + if (!FindUpdatedTuple(info, row_idx, update_idx)) { + continue; + } + auto value_mask = info.GetValueMask(); + if (!value_mask.RowIsValid(update_idx)) { + value = base_value; + } else { + // values are only stored for the valid tuples + value = info.GetData()[value_mask.CountValid(update_idx)]; } } - }); + result.Set(result_idx, value); + } +} + +static void FetchRowsValidity(const SnapshotView &view, UpdateInfo &info, const idx_t *offsets, + const SelectionVector &sel, idx_t fetch_offset, idx_t count, idx_t vector_offset, + Vector &result, idx_t result_offset) { + UpdateValidityResult validity_result(result); + FetchRowsInternal(view, info, offsets, sel, fetch_offset, count, vector_offset, validity_result, + result_offset); } template static void TemplatedFetchRows(const SnapshotView &view, UpdateInfo &info, const idx_t *offsets, const SelectionVector &sel, idx_t fetch_offset, idx_t count, idx_t vector_offset, Vector &result, idx_t result_offset) { - auto result_data = FlatVector::GetDataMutable(result); - UpdateInfo::UpdatesForTransaction(info, view, [&](UpdateInfo ¤t) { - auto info_data = current.GetData(); - for (idx_t idx = 0; idx < count; idx++) { - const idx_t row_idx = offsets[sel.get_index(fetch_offset + idx)] - vector_offset; - idx_t update_idx; - if (FindUpdatedTuple(current, row_idx, update_idx)) { - result_data[result_offset + fetch_offset + idx] = info_data[update_idx]; - } - } - }); + UpdateDataResult data_result(result); + FetchRowsInternal(view, info, offsets, sel, fetch_offset, count, vector_offset, data_result, result_offset); } static UpdateSegment::fetch_rows_function_t GetFetchRowsFunction(PhysicalType type) { @@ -568,20 +714,30 @@ void UpdateSegment::FetchRows(TransactionData transaction, const idx_t *offsets, // Rollback update //===--------------------------------------------------------------------===// template -static void RollbackUpdate(UpdateInfo &base_info, UpdateInfo &rollback_info) { - auto base_data = base_info.GetData(); - auto base_tuples = base_info.GetTuples(); +static void RollbackUpdate(UpdateInfo &root, UpdateInfo &rollback_info) { + auto root_data = root.GetData(); + auto root_tuples = root.GetTuples(); auto rollback_data = rollback_info.GetData(); auto rollback_tuples = rollback_info.GetTuples(); - idx_t base_offset = 0; - for (idx_t i = 0; i < rollback_info.N; i++) { - auto id = rollback_tuples[i]; - while (base_tuples[base_offset] < id) { - base_offset++; - D_ASSERT(base_offset < base_info.N); + auto value_mask = rollback_info.GetValueMask(); + idx_t rollback_idx = 0; + idx_t rollback_value_idx = 0; + idx_t result_count = 0; + for (idx_t i = 0; i < root.N; i++) { + auto id = root_tuples[i]; + auto value = root_data[i]; + if (rollback_idx < rollback_info.N && rollback_tuples[rollback_idx] == id) { + if (!value_mask.RowIsValid(rollback_idx++)) { + // the tuple had no update before: remove it from the root + continue; + } + value = rollback_data[rollback_value_idx++]; } - base_data[base_offset] = rollback_data[i]; + root_tuples[result_count] = id; + root_data[result_count++] = value; } + D_ASSERT(rollback_idx == rollback_info.N); + root.N = UnsafeNumericCast(result_count); } static UpdateSegment::rollback_update_function_t GetRollbackUpdateFunction(PhysicalType type) { @@ -626,7 +782,11 @@ void UpdateSegment::RollbackUpdate(UpdateInfo &info) { // obtain an exclusive lock auto lock_handle = lock.GetExclusiveLock(); - // move the data from the UpdateInfo back into the base info + if (info.N == 0) { + // this update info was replaced by a larger one, or has no updates + return; + } + // move the data from the UpdateInfo back into the root auto entry = GetUpdateNode(*lock_handle, info.vector_index); if (!entry.IsSet()) { return; @@ -701,358 +861,297 @@ static void CheckForConflicts(UndoBufferPointer next_ptr, TransactionData transa } //===--------------------------------------------------------------------===// -// Initialize update info +// Merge update info //===--------------------------------------------------------------------===// -void UpdateSegment::InitializeUpdateInfo(UpdateInfo &info, row_t *ids, const SelectionVector &sel, idx_t count, - idx_t vector_index, idx_t vector_offset) { - info.segment = this; - info.vector_index = vector_index; - info.prev = UndoBufferPointer(); - info.next = UndoBufferPointer(); - - // set up the tuple ids - info.N = UnsafeNumericCast(count); - auto tuples = info.GetTuples(); - for (idx_t i = 0; i < count; i++) { - auto idx = sel.get_index(i); - auto id = ids[idx]; - D_ASSERT(idx_t(id) >= vector_offset && idx_t(id) < vector_offset + STANDARD_VECTOR_SIZE); - tuples[i] = NumericCast(NumericCast(id) - vector_offset); - }; -} - -static void InitializeUpdateValidity(UpdateInfo &base_info, Vector &base_data, UpdateInfo &update_info, - UnifiedVectorFormat &update, const SelectionVector &sel) { - auto &update_mask = update.validity; - auto tuple_data = update_info.GetData(); - - if (update_mask.CanHaveNull()) { - for (idx_t i = 0; i < update_info.N; i++) { - auto idx = update.sel->get_index(sel.get_index(i)); - tuple_data[i] = update_mask.RowIsValidUnsafe(idx); - } - } else { - for (idx_t i = 0; i < update_info.N; i++) { - tuple_data[i] = true; +//! Merge "count" new tuples (sorted by id) into the info - new values replace existing values of the same tuple +template +static void MergeIntoUpdateInfo(UpdateInfo &info, idx_t count, FETCH_ID &&fetch_id, FETCH_VALUE &&fetch_value) { + auto info_data = info.GetData(); + auto info_tuples = info.GetTuples(); + if (info.N == 0) { + // no existing updates: write the new tuples directly + D_ASSERT(count <= info.max); + for (idx_t i = 0; i < count; i++) { + info_tuples[i] = fetch_id(i); + info_data[i] = fetch_value(i); } + info.N = UnsafeNumericCast(count); + return; } - - auto &base_mask = FlatVector::ValidityMutable(base_data); - auto base_tuple_data = base_info.GetData(); - auto base_tuples = base_info.GetTuples(); - if (base_mask.CanHaveNull()) { - for (idx_t i = 0; i < base_info.N; i++) { - base_tuple_data[i] = base_mask.RowIsValidUnsafe(base_tuples[i]); - } - } else { - for (idx_t i = 0; i < base_info.N; i++) { - base_tuple_data[i] = true; + // merge into temporary arrays, and copy them back afterwards + T result_values[STANDARD_VECTOR_SIZE]; + sel_t result_ids[STANDARD_VECTOR_SIZE]; + idx_t info_idx = 0; + idx_t new_idx = 0; + idx_t result_count = 0; + while (info_idx < info.N && new_idx < count) { + auto new_id = fetch_id(new_idx); + auto old_id = info_tuples[info_idx]; + if (new_id <= old_id) { + if (new_id == old_id) { + // the new value replaces the old value + info_idx++; + } + result_ids[result_count] = new_id; + result_values[result_count++] = fetch_value(new_idx++); + } else { + result_ids[result_count] = old_id; + result_values[result_count++] = info_data[info_idx++]; } } -} - -struct UpdateSelectElement { - template - static T Operation(UpdateSegment &segment, T element) { - return element; + for (; info_idx < info.N; info_idx++) { + result_ids[result_count] = info_tuples[info_idx]; + result_values[result_count++] = info_data[info_idx]; } -}; - -template <> -string_t UpdateSelectElement::Operation(UpdateSegment &segment, string_t element) { - return segment.GetStringHeap().AddBlob(element); + for (; new_idx < count; new_idx++) { + result_ids[result_count] = fetch_id(new_idx); + result_values[result_count++] = fetch_value(new_idx); + } + D_ASSERT(result_count <= info.max); + info.N = UnsafeNumericCast(result_count); + memcpy(info_data, result_values, result_count * sizeof(T)); + memcpy(info_tuples, result_ids, result_count * sizeof(sel_t)); } -template -static T GetUpdateNullPadding() { - return T(); +static void VerifyUpdateIds(row_t *ids, idx_t count, const SelectionVector &sel, idx_t vector_offset) { +#ifdef DEBUG + // the ids should be sorted, unique and part of the same vector + for (idx_t i = 0; i < count; i++) { + auto id = ids[sel.get_index(i)]; + D_ASSERT(id >= row_t(vector_offset) && id < row_t(vector_offset + STANDARD_VECTOR_SIZE)); + D_ASSERT(i == 0 || id > ids[sel.get_index(i - 1)]); + } +#endif } -template <> -string_t GetUpdateNullPadding() { - return string_t(uint32_t(0)); +static void MergeValidityLoop(UpdateInfo &info, UnifiedVectorFormat &update, row_t *ids, idx_t count, + const SelectionVector &sel, idx_t vector_offset) { + VerifyUpdateIds(ids, count, sel, vector_offset); + auto &update_validity = update.validity; + MergeIntoUpdateInfo( + info, count, + [&](idx_t i) { + return UnsafeNumericCast(UnsafeNumericCast(ids[sel.get_index(i)]) - vector_offset); + }, + [&](idx_t i) { return update_validity.RowIsValid(update.sel->get_index(sel.get_index(i))); }); } template -static void InitializeUpdateData(UpdateInfo &base_info, Vector &base_data, UpdateInfo &update_info, - UnifiedVectorFormat &update, const SelectionVector &sel) { - auto update_data = update.GetData(update); - auto tuple_data = update_info.GetData(); - - for (idx_t i = 0; i < update_info.N; i++) { - auto idx = update.sel->get_index(sel.get_index(i)); - tuple_data[i] = update_data[idx]; - } - - auto base_array_data = FlatVector::GetData(base_data); - auto &base_validity = FlatVector::ValidityMutable(base_data); - auto base_tuple_data = base_info.GetData(); - auto base_tuples = base_info.GetTuples(); - for (idx_t i = 0; i < base_info.N; i++) { - auto base_idx = base_tuples[i]; - if (!base_validity.RowIsValid(base_idx)) { - base_tuple_data[i] = GetUpdateNullPadding(); - continue; - } - base_tuple_data[i] = UpdateSelectElement::Operation(*base_info.segment, base_array_data[base_idx]); - } +static void MergeUpdateLoop(UpdateInfo &info, UnifiedVectorFormat &update, row_t *ids, idx_t count, + const SelectionVector &sel, idx_t vector_offset) { + VerifyUpdateIds(ids, count, sel, vector_offset); + auto update_data = UnifiedVectorFormat::GetData(update); + MergeIntoUpdateInfo( + info, count, + [&](idx_t i) { + return UnsafeNumericCast(UnsafeNumericCast(ids[sel.get_index(i)]) - vector_offset); + }, + [&](idx_t i) { return update_data[update.sel->get_index(sel.get_index(i))]; }); } -static UpdateSegment::initialize_update_function_t GetInitializeUpdateFunction(PhysicalType type) { +static UpdateSegment::merge_update_function_t GetMergeUpdateFunction(PhysicalType type) { switch (type) { case PhysicalType::BIT: - return InitializeUpdateValidity; + return MergeValidityLoop; case PhysicalType::BOOL: case PhysicalType::INT8: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::INT16: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::INT32: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::INT64: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::UINT8: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::UINT16: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::UINT32: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::UINT64: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::INT128: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::UINT128: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::FLOAT: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::DOUBLE: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::INTERVAL: - return InitializeUpdateData; + return MergeUpdateLoop; case PhysicalType::VARCHAR: - return InitializeUpdateData; + return MergeUpdateLoop; default: - throw NotImplementedException("Unimplemented type for update segment"); + throw NotImplementedException("Unimplemented type for uncompressed segment"); } } -//===--------------------------------------------------------------------===// -// Merge update info -//===--------------------------------------------------------------------===// -template -static idx_t MergeLoop(row_t a[], sel_t b[], idx_t acount, idx_t bcount, idx_t aoffset, F1 merge, F2 pick_a, F3 pick_b, - const SelectionVector &asel) { - idx_t aidx = 0, bidx = 0; - idx_t count = 0; - while (aidx < acount && bidx < bcount) { - auto a_index = asel.get_index(aidx); - auto a_id = UnsafeNumericCast(a[a_index]) - aoffset; - auto b_id = b[bidx]; - if (a_id == b_id) { - merge(a_id, a_index, bidx, count); - aidx++; - bidx++; - count++; - } else if (a_id < b_id) { - pick_a(a_id, a_index, count); - aidx++; - count++; - } else { - pick_b(b_id, bidx, count); - bidx++; - count++; - } - } - for (; aidx < acount; aidx++) { - auto a_index = asel.get_index(aidx); - pick_a(UnsafeNumericCast(a[a_index]) - aoffset, a_index, count); - count++; +//! Writes a validity mask in order, one entry at a time +struct ValidityAppender { + explicit ValidityAppender(validity_t *data_p) : data(data_p) { } - for (; bidx < bcount; bidx++) { - pick_b(b[bidx], bidx, count); + + void Append(bool valid) { + current |= validity_t(valid) << (count % ValidityMask::BITS_PER_VALUE); count++; + if (count % ValidityMask::BITS_PER_VALUE == 0) { + data[count / ValidityMask::BITS_PER_VALUE - 1] = current; + current = 0; + } } - return count; -} - -struct ExtractStandardEntry { - template - static T Extract(const V *data, idx_t entry) { - return data[entry]; + void Finalize() { + if (count % ValidityMask::BITS_PER_VALUE != 0) { + data[count / ValidityMask::BITS_PER_VALUE] = current; + } } -}; -struct ExtractValidityEntry { - template - static T Extract(const V *data, idx_t entry) { - return data->RowIsValid(entry); - } + validity_t *data; + validity_t current = 0; + idx_t count = 0; }; -template -static void MergeUpdateLoopInternal(UpdateInfo &base_info, V *base_table_data, UpdateInfo &update_info, - const SelectionVector &update_vector_sel, const V *update_vector_data, row_t *ids, - idx_t count, const SelectionVector &sel, idx_t row_group_start, - const ValidityMask *base_table_validity = nullptr) { - auto base_id = row_group_start + base_info.vector_index * STANDARD_VECTOR_SIZE; -#ifdef DEBUG - // all of these should be sorted, otherwise the below algorithm does not work - for (idx_t i = 1; i < count; i++) { - auto prev_idx = sel.get_index(i - 1); - auto idx = sel.get_index(i); - D_ASSERT(ids[idx] > ids[prev_idx] && ids[idx] >= row_t(base_id) && - ids[idx] < row_t(base_id + STANDARD_VECTOR_SIZE)); - } -#endif - - // we have a new batch of updates (update, ids, count) - // we already have existing updates (base_info) - // and potentially, this transaction already has updates present (update_info) - // we need to merge these all together so that the latest updates get merged into base_info - // and the "old" values (fetched from EITHER base_info OR from base_data) get placed into update_info - auto base_info_data = base_info.GetData(); - auto base_tuples = base_info.GetTuples(); - auto update_info_data = update_info.GetData(); - auto update_tuples = update_info.GetTuples(); - - // we first do the merging of the old values - // what we are trying to do here is update the "update_info" of this transaction with all the old data we require - // this means we need to merge (1) any previously updated values (stored in update_info->tuples) - // together with (2) - // to simplify this, we create new arrays here - // we memcpy these over afterwards - T result_values[STANDARD_VECTOR_SIZE]; - sel_t result_ids[STANDARD_VECTOR_SIZE]; - - idx_t base_info_offset = 0; - idx_t update_info_offset = 0; - idx_t result_offset = 0; +//! Count the tuples and values the update info of the transaction requires after adding the new tuples +static void CountUndoValues(optional_ptr node, UpdateInfo &root, row_t *ids, idx_t count, + const SelectionVector &sel, idx_t vector_offset, idx_t &tuple_count, idx_t &value_count) { + idx_t node_count = node ? node->N : 0; + auto node_tuples = node ? node->GetTuples() : nullptr; + auto root_tuples = root.GetTuples(); + tuple_count = node_count; + value_count = node ? node->GetValueMask().CountValid(node_count) : 0; + idx_t node_idx = 0; + idx_t root_idx = 0; for (idx_t i = 0; i < count; i++) { - auto idx = sel.get_index(i); - // we have to merge the info for "ids[i]" - auto update_id = UnsafeNumericCast(ids[idx]) - base_id; - - while (update_info_offset < update_info.N && update_tuples[update_info_offset] < update_id) { - // old id comes before the current id: write it - result_values[result_offset] = update_info_data[update_info_offset]; - result_ids[result_offset++] = update_tuples[update_info_offset]; - update_info_offset++; + auto id = UnsafeNumericCast(UnsafeNumericCast(ids[sel.get_index(i)]) - vector_offset); + while (node_idx < node_count && node_tuples[node_idx] < id) { + node_idx++; } - // write the new id - if (update_info_offset < update_info.N && update_tuples[update_info_offset] == update_id) { - // we have an id that is equivalent in the current update info: write the update info - result_values[result_offset] = update_info_data[update_info_offset]; - result_ids[result_offset++] = update_tuples[update_info_offset]; - update_info_offset++; + if (node_idx < node_count && node_tuples[node_idx] == id) { continue; } - - /// now check if we have the current update_id in the base_info, or if we should fetch it from the base data - while (base_info_offset < base_info.N && base_tuples[base_info_offset] < update_id) { - base_info_offset++; + while (root_idx < root.N && root_tuples[root_idx] < id) { + root_idx++; } - if (base_info_offset < base_info.N && base_tuples[base_info_offset] == update_id) { - // it is! we have to move the tuple from base_info->ids[base_info_offset] to update_info - result_values[result_offset] = base_info_data[base_info_offset]; - } else if (base_table_validity && !base_table_validity->RowIsValid(update_id)) { - result_values[result_offset] = GetUpdateNullPadding(); - } else { - // it is not! we have to move base_table_data[update_id] to update_info - result_values[result_offset] = UpdateSelectElement::Operation( - *base_info.segment, OP::template Extract(base_table_data, update_id)); + tuple_count++; + if (root_idx < root.N && root_tuples[root_idx] == id) { + value_count++; } - result_ids[result_offset++] = UnsafeNumericCast(update_id); - } - // write any remaining entries from the old updates - while (update_info_offset < update_info.N) { - result_values[result_offset] = update_info_data[update_info_offset]; - result_ids[result_offset++] = update_tuples[update_info_offset]; - update_info_offset++; - } - // now copy them back - update_info.N = UnsafeNumericCast(result_offset); - memcpy(update_info_data, result_values, result_offset * sizeof(T)); - memcpy(update_tuples, result_ids, result_offset * sizeof(sel_t)); - - // now we merge the new values into the base_info - result_offset = 0; - auto pick_new = [&](idx_t id, idx_t aidx, idx_t count) { - result_values[result_offset] = - OP::template Extract(update_vector_data, update_vector_sel.get_index(aidx)); - result_ids[result_offset] = UnsafeNumericCast(id); - result_offset++; - }; - auto pick_old = [&](idx_t id, idx_t bidx, idx_t count) { - result_values[result_offset] = base_info_data[bidx]; - result_ids[result_offset] = UnsafeNumericCast(id); - result_offset++; - }; - // now we perform a merge of the new ids with the old ids - auto merge = [&](idx_t id, idx_t aidx, idx_t bidx, idx_t count) { - pick_new(id, aidx, count); - }; - MergeLoop(ids, base_tuples, count, base_info.N, base_id, merge, pick_new, pick_old, sel); - - base_info.N = UnsafeNumericCast(result_offset); - memcpy(base_info_data, result_values, result_offset * sizeof(T)); - memcpy(base_tuples, result_ids, result_offset * sizeof(sel_t)); -} - -static void MergeValidityLoop(UpdateInfo &base_info, Vector &base_data, UpdateInfo &update_info, - UnifiedVectorFormat &update, row_t *ids, idx_t count, const SelectionVector &sel, - idx_t row_group_start) { - auto &base_validity = FlatVector::ValidityMutable(base_data); - auto &update_validity = update.validity; - MergeUpdateLoopInternal( - base_info, &base_validity, update_info, *update.sel, &update_validity, ids, count, sel, row_group_start); + } } +//! Merge the tuples in the update info of the transaction with the new tuples into the output arrays +//! For tuples that are not yet in the root, the value before the update is the base value: these are flagged template -static void MergeUpdateLoop(UpdateInfo &base_info, Vector &base_data, UpdateInfo &update_info, - UnifiedVectorFormat &update, row_t *ids, idx_t count, const SelectionVector &sel, - idx_t row_group_start) { - auto base_table_data = FlatVector::GetDataMutable(base_data); - auto update_vector_data = update.GetData(update); - auto &base_validity = FlatVector::Validity(base_data); - MergeUpdateLoopInternal(base_info, base_table_data, update_info, *update.sel, update_vector_data, ids, count, - sel, row_group_start, &base_validity); +static idx_t MergeUndoValues(UpdateInfo &node, UpdateInfo &root, row_t *ids, idx_t count, const SelectionVector &sel, + idx_t vector_offset, sel_t *out_tuples, T *out_data, validity_t *out_mask) { + auto node_tuples = node.GetTuples(); + auto node_data = node.GetData(); + auto node_mask = node.GetValueMask(); + auto root_tuples = root.GetTuples(); + auto root_data = root.GetData(); + + ValidityAppender mask_appender(out_mask); + idx_t node_idx = 0; + idx_t node_value_idx = 0; + idx_t root_idx = 0; + idx_t result_count = 0; + idx_t result_value_count = 0; + auto copy_node_entry = [&]() { + bool has_value = node_mask.RowIsValid(node_idx); + if (has_value) { + out_data[result_value_count++] = node_data[node_value_idx++]; + } + out_tuples[result_count++] = node_tuples[node_idx++]; + mask_appender.Append(has_value); + }; + for (idx_t i = 0; i < count; i++) { + auto id = UnsafeNumericCast(UnsafeNumericCast(ids[sel.get_index(i)]) - vector_offset); + while (node_idx < node.N && node_tuples[node_idx] < id) { + copy_node_entry(); + } + if (node_idx < node.N && node_tuples[node_idx] == id) { + // this transaction updated the tuple before - the value from before that update is already saved + copy_node_entry(); + continue; + } + while (root_idx < root.N && root_tuples[root_idx] < id) { + root_idx++; + } + // tuples that are not in the root have no saved value - their value before the update is the base value + bool in_root = root_idx < root.N && root_tuples[root_idx] == id; + if (in_root) { + out_data[result_value_count++] = root_data[root_idx]; + } + out_tuples[result_count++] = id; + mask_appender.Append(in_root); + } + while (node_idx < node.N) { + copy_node_entry(); + } + mask_appender.Finalize(); + return result_count; } -static UpdateSegment::merge_update_function_t GetMergeUpdateFunction(PhysicalType type) { +//! Save the values from before the update - the target is either the source, or a larger update info replacing it +template +static void SaveUndoValues(UpdateInfo &source, UpdateInfo &root, row_t *ids, idx_t count, const SelectionVector &sel, + idx_t vector_offset, UpdateInfo &target) { + idx_t result_count; + if (&source != &target || source.N == 0) { + // we are not reading from the target: write directly into it + result_count = MergeUndoValues(source, root, ids, count, sel, vector_offset, target.GetTuples(), + target.GetData(), target.GetValueMask().GetData()); + } else { + // merge into temporary arrays, and copy them back afterwards + T result_values[STANDARD_VECTOR_SIZE]; + sel_t result_tuples[STANDARD_VECTOR_SIZE]; + validity_t result_mask[ValidityMask::STANDARD_ENTRY_COUNT]; + result_count = + MergeUndoValues(source, root, ids, count, sel, vector_offset, result_tuples, result_values, result_mask); + auto value_count = ValidityMask(result_mask, STANDARD_VECTOR_SIZE).CountValid(result_count); + memcpy(target.GetTuples(), result_tuples, result_count * sizeof(sel_t)); + memcpy(target.GetData(), result_values, value_count * sizeof(T)); + memcpy(target.GetValueMask().GetData(), result_mask, ValidityMask::ValidityMaskSize(result_count)); + } + target.N = UnsafeNumericCast(result_count); + D_ASSERT(target.N <= target.max); +} + +static UpdateSegment::save_undo_function_t GetSaveUndoFunction(PhysicalType type) { switch (type) { case PhysicalType::BIT: - return MergeValidityLoop; + return SaveUndoValues; case PhysicalType::BOOL: case PhysicalType::INT8: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::INT16: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::INT32: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::INT64: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::UINT8: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::UINT16: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::UINT32: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::UINT64: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::INT128: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::UINT128: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::FLOAT: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::DOUBLE: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::INTERVAL: - return MergeUpdateLoop; + return SaveUndoValues; case PhysicalType::VARCHAR: - return MergeUpdateLoop; + return SaveUndoValues; default: - throw NotImplementedException("Unimplemented type for uncompressed segment"); + throw NotImplementedException("Unimplemented type for update segment"); } } @@ -1328,14 +1427,6 @@ static idx_t SortSelectionVector(SelectionVector &sel, idx_t count, row_t *ids) return pos; } -UpdateInfo *CreateEmptyUpdateInfo(TransactionData transaction, DuckTableEntry &table_entry, idx_t type_size, - idx_t count, unsafe_unique_array &data, idx_t row_group_start) { - data = make_unsafe_uniq_array_uninitialized(UpdateInfo::GetAllocSize(type_size)); - auto update_info = reinterpret_cast(data.get()); - UpdateInfo::Initialize(*update_info, table_entry, transaction.GetTransactionId(), row_group_start); - return update_info; -} - void UpdateSegment::InitializeUpdateInfo(idx_t vector_idx) { // create the versions for this segment, if there are none yet if (!root) { @@ -1370,6 +1461,8 @@ void UpdateSegment::ReallocateRootInfoIfNeeded(UpdateInfo ¤t_info, idx_t u new_info.max = UnsafeNumericCast(new_capacity); new_info.prev = current_info.prev; new_info.next = current_info.next; + new_info.value_mask_offset = 0; + new_info.value_max = UnsafeNumericCast(new_capacity); memcpy(new_info.GetTuples(), current_info.GetTuples(), sizeof(sel_t) * current_info.N); memcpy(new_info.GetValues(), current_info.GetValues(), type_size * current_info.N); @@ -1439,100 +1532,111 @@ void UpdateSegment::Update(TransactionData transaction, DuckTableEntry &table_en D_ASSERT(idx_t(first_id) >= row_group_start); - if (root->info[vector_index].IsSet()) { - // there is already a version here, check if there are any conflicts and search for the node that belongs to - // this transaction in the version chain - auto root_pointer = root->info[vector_index]; - auto root_pin = root_pointer.Pin(); - - UndoBufferReference node_ref; - CheckForConflicts(UpdateInfo::Get(root_pin).next, transaction, ids, sel, count, - UnsafeNumericCast(vector_offset), node_ref); + if (!root->info[vector_index].IsSet()) { + // there are no updates for this vector yet: create the root update info + idx_t compact_capacity = UpdateInfo::GetCompactCapacity(count); + auto handle = root->allocator.Allocate(UpdateInfo::GetAllocSize(type_size, compact_capacity)); + auto &root_info = UpdateInfo::Get(handle); + UpdateInfo::Initialize(root_info, table_entry, MAX_COMMIT_ID, row_group_start, compact_capacity); + root_info.segment = this; + root_info.vector_index = vector_index; + root_info.column_index = column_index; + root->info[vector_index] = handle.GetBufferPointer(); + } + // make room in the root before modifying anything + { + auto root_pin = root->info[vector_index].Pin(); ReallocateRootInfoIfNeeded(UpdateInfo::Get(root_pin), count, vector_index); - root_pointer = root->info[vector_index]; - root_pin = root_pointer.Pin(); - auto &base_info = UpdateInfo::Get(root_pin); - - // there are no conflicts - continue with the update - unsafe_unique_array update_info_data; + } + auto root_pointer = root->info[vector_index]; + auto root_pin = root_pointer.Pin(); + auto &root_info = UpdateInfo::Get(root_pin); + if (transaction.transaction) { + // check if there are any conflicts and search for the update info that belongs to this transaction + UndoBufferReference node_ref; + CheckForConflicts(root_info.next, transaction, ids, sel, count, UnsafeNumericCast(vector_offset), + node_ref); optional_ptr node; - if (!node_ref.IsSet()) { - // no updates made yet by this transaction: initially the update info to empty - if (transaction.transaction) { - auto &dtransaction = transaction.transaction->Cast(); - node_ref = dtransaction.CreateUpdateInfo(table_entry, type_size, count, row_group_start); - node = &UpdateInfo::Get(node_ref); + if (node_ref.IsSet()) { + node = &UpdateInfo::Get(node_ref); + node->Verify(); + } + // determine the tuples and values the update info of this transaction requires + // we only count them with a merge walk if they cannot be derived or bounded cheaply + bool save_required = true; + bool fits = false; + idx_t tuple_count = 0; + idx_t value_count = 0; + if (node) { + if (node->N == STANDARD_VECTOR_SIZE) { + // this transaction updated every tuple of the vector before - their values before it are saved + save_required = false; } else { - node = CreateEmptyUpdateInfo(transaction, table_entry, type_size, count, update_info_data, - row_group_start); - } - node->segment = this; - node->vector_index = vector_index; - node->N = 0; - node->column_index = column_index; - - // insert the new node into the chain - node->next = base_info.next; - if (node->next.IsSet()) { - auto next_pin = node->next.Pin(); - auto &next_info = UpdateInfo::Get(next_pin); - next_info.prev = node_ref.GetBufferPointer(); + auto node_value_count = node->GetValueMask().CountValid(node->N); + fits = node->N + count <= node->max && node_value_count + count <= node->value_max; } - node->prev = root_pointer; - base_info.next = transaction.transaction ? node_ref.GetBufferPointer() : UndoBufferPointer(); + } else if (root_info.N == 0 || root_info.N == STANDARD_VECTOR_SIZE) { + // either none or all of the tuples have a value in the root + tuple_count = count; + value_count = root_info.N == 0 ? 0 : count; } else { - // we already had updates made to this transaction - node = &UpdateInfo::Get(node_ref); + CountUndoValues(node, root_info, ids, count, sel, vector_offset, tuple_count, value_count); } - base_info.Verify(); - node->Verify(); - - // now we are going to perform the merge - merge_update_function(base_info, base_data, *node, update_format, ids, count, sel, row_group_start); - - base_info.Verify(); - node->Verify(); - } else { - // there is no version info yet: create the top level update info and fill it with the updates - // allocate space for the UpdateInfo in the allocator (compact: sized to actual count) - idx_t compact_capacity = UpdateInfo::GetCompactCapacity(count); - idx_t alloc_size = UpdateInfo::GetAllocSize(type_size, compact_capacity); - auto handle = root->allocator.Allocate(alloc_size); - auto &update_info = UpdateInfo::Get(handle); - UpdateInfo::Initialize(update_info, table_entry, MAX_COMMIT_ID, row_group_start, compact_capacity); - update_info.column_index = column_index; - - InitializeUpdateInfo(update_info, ids, sel, count, vector_index, vector_offset); - - // now create the transaction level update info in the undo log - unsafe_unique_array update_info_data; - UndoBufferReference node_ref; - optional_ptr transaction_node; - if (transaction.transaction) { - node_ref = transaction.transaction->CreateUpdateInfo(table_entry, type_size, count, row_group_start); - transaction_node = &UpdateInfo::Get(node_ref); + if (save_required && node && !fits) { + CountUndoValues(node, root_info, ids, count, sel, vector_offset, tuple_count, value_count); + if (tuple_count == node->N) { + // this transaction updated all of these tuples before - their values before it are saved + save_required = false; + } + fits = tuple_count <= node->max && value_count <= node->value_max; + } + if (!save_required) { + // the update info of this transaction does not change + } else if (fits) { + // the values from before the update fit into the update info of this transaction + save_undo_function(*node, root_info, ids, count, sel, vector_offset, *node); + node->Verify(); } else { - transaction_node = - CreateEmptyUpdateInfo(transaction, table_entry, type_size, count, update_info_data, row_group_start); + // create a new update info that fits all values from before the updates of this transaction + idx_t capacity = UpdateInfo::GetCompactCapacity(tuple_count); + idx_t value_capacity = value_count == 0 ? 0 : UpdateInfo::GetCompactCapacity(value_count); + if (node) { + capacity = MaxValue(capacity, node->max); + value_capacity = MaxValue(value_capacity, node->value_max); + } + auto new_ref = transaction.transaction->CreateUpdateInfo(table_entry, type_size, capacity, value_capacity, + row_group_start); + auto &new_node = UpdateInfo::Get(new_ref); + new_node.segment = this; + new_node.vector_index = vector_index; + new_node.column_index = column_index; + save_undo_function(node ? *node : new_node, root_info, ids, count, sel, vector_offset, new_node); + new_node.Verify(); + + // insert the new update info into the chain - replacing the old one (if any) + auto new_pointer = new_ref.GetBufferPointer(); + new_node.prev = node ? node->prev : root_pointer; + new_node.next = node ? node->next : root_info.next; + { + auto prev_pin = new_node.prev.Pin(); + UpdateInfo::Get(prev_pin).next = new_pointer; + } + if (new_node.next.IsSet()) { + auto next_pin = new_node.next.Pin(); + UpdateInfo::Get(next_pin).prev = new_pointer; + } + if (node) { + // the old update info stays in the undo buffer, but no longer contains any updates + node->N = 0; + node->prev = UndoBufferPointer(); + node->next = UndoBufferPointer(); + } } - - InitializeUpdateInfo(*transaction_node, ids, sel, count, vector_index, vector_offset); - - // we write the updates in the update node data, and write the updates in the info - initialize_update_function(*transaction_node, base_data, update_info, update_format, sel); - - update_info.next = transaction.transaction ? node_ref.GetBufferPointer() : UndoBufferPointer(); - update_info.prev = UndoBufferPointer(); - transaction_node->next = UndoBufferPointer(); - transaction_node->prev = handle.GetBufferPointer(); - transaction_node->column_index = column_index; - - transaction_node->Verify(); - update_info.Verify(); - - root->info[vector_index] = handle.GetBufferPointer(); } + // write the new values into the root + merge_update_function(root_info, update_format, ids, count, sel, vector_offset); + root_info.Verify(); } bool UpdateSegment::HasUpdates() const { diff --git a/src/duckdb/src/storage/table_index_list.cpp b/src/duckdb/src/storage/table_index_list.cpp index 1ef7e78eb..a83fc23e8 100644 --- a/src/duckdb/src/storage/table_index_list.cpp +++ b/src/duckdb/src/storage/table_index_list.cpp @@ -1,21 +1,29 @@ #include "duckdb/storage/table/table_index_list.hpp" -#include "duckdb/planner/binder.hpp" -#include "duckdb/catalog/catalog.hpp" +#include "duckdb/common/assert.hpp" +#include "duckdb/common/shared_ptr_ipp.hpp" +#include "duckdb/common/typedefs.hpp" +#include "duckdb/common/unique_ptr.hpp" +#include "duckdb/storage/checkpoint/table_index_writer.hpp" #include "duckdb/catalog/catalog_entry/duck_table_entry.hpp" +#include "duckdb/catalog/catalog.hpp" +#include "duckdb/planner/binder.hpp" #include "duckdb/common/types/constraint_conflict_info.hpp" #include "duckdb/common/types/conflict_manager.hpp" #include "duckdb/common/vector_operations/vector_operations.hpp" +#include "duckdb/storage/index_storage_info.hpp" +#include "duckdb/storage/storage_info.hpp" #include "duckdb/storage/table/append_state.hpp" #include "duckdb/execution/index/art/art.hpp" #include "duckdb/execution/index/unbound_index.hpp" #include "duckdb/main/config.hpp" -#include "duckdb/main/database.hpp" #include "duckdb/planner/expression_binder/index_binder.hpp" #include "duckdb/storage/data_table.hpp" #include "duckdb/storage/table/data_table_info.hpp" +#include "duckdb/common/serializer/serializer.hpp" #include "duckdb/storage/table/scan_state.hpp" #include "duckdb/main/attached_database.hpp" +#include namespace duckdb { @@ -82,14 +90,15 @@ TableIndexList::~TableIndexList() { } } -void TableIndexList::AddIndex(unique_ptr index) { +shared_ptr TableIndexList::AddIndex(unique_ptr index, const ConstraintCheckMode check_mode) { D_ASSERT(index); annotated_lock_guard lock(index_entries_lock); - auto index_entry = make_shared_ptr(std::move(index)); + auto index_entry = make_shared_ptr(std::move(index), check_mode); if (index_entry->GetBindState() != IndexBindState::BOUND) { unbound_count++; } - index_entries.push_back(std::move(index_entry)); + index_entries.push_back(index_entry); + return index_entry; } void TableIndexList::InitializeLocalIndexes(TableIndexList &delete_indexes, TableIndexList &append_indexes) const { @@ -206,7 +215,7 @@ void TableIndexList::VerifyUniqueIndexes(optional_ptr dele annotated_lock_guard lock(index_entries_lock); if (!manager) { for (const auto &entry : index_entries) { - if (!entry->IsUnique() || entry->GetIndexType() != ART::TYPE_NAME) { + if (!entry->IsUnique() || entry->IsDeferred() || entry->GetIndexType() != ART::TYPE_NAME) { continue; } auto delete_entry = delete_indexes ? delete_indexes->FindEntry(entry->GetName()) : nullptr; @@ -219,7 +228,8 @@ void TableIndexList::VerifyUniqueIndexes(optional_ptr dele const auto &conflict_info = manager->GetConflictInfo(); for (const auto &entry : index_entries) { auto index_info = entry->GetStorageInfo(); - if (!index_info.is_unique || entry->GetIndexType() != ART::TYPE_NAME || + if (!index_info.is_unique || index_info.check_mode == ConstraintCheckMode::DEFERRED || + entry->GetIndexType() != ART::TYPE_NAME || !conflict_info.ConflictTargetMatches(index_info.is_unique, index_info.column_set)) { continue; } @@ -239,7 +249,8 @@ void TableIndexList::VerifyUniqueIndexes(optional_ptr dele // Scan the other indexes and throw if there are any conflicts. manager->SetMode(ConflictManagerMode::THROW); for (const auto &entry : index_entries) { - if (!entry->IsUnique() || entry->GetIndexType() != ART::TYPE_NAME || manager->IndexMatches(entry->GetName())) { + if (!entry->IsUnique() || entry->IsDeferred() || entry->GetIndexType() != ART::TYPE_NAME || + manager->IndexMatches(entry->GetName())) { continue; } auto delete_entry = delete_indexes ? delete_indexes->FindEntry(entry->GetName()) : nullptr; @@ -487,12 +498,35 @@ unordered_set TableIndexList::GetIndexedColumns() const { return column_ids; } +vector> TableIndexList::Checkpoint(TableIndexWriter &writer) { + annotated_lock_guard lock(index_entries_lock); + + vector> shadows; + vector> infos; + shadows.reserve(index_entries.size()); + infos.reserve(index_entries.size()); + + for (const auto &entry : index_entries) { + auto checkpoint = entry->Checkpoint(writer); + shadows.push_back(std::move(checkpoint.shadow_index)); + infos.push_back(std::move(checkpoint.storage_info)); + } + + writer.Flush(); + + for (idx_t i = 0; i < index_entries.size(); i++) { + index_entries[i]->InstallShadow(std::move(shadows[i])); + } + + return infos; +} + vector> TableIndexList::GetConflictTargetColumns(const ConflictInfo &conflict_info) const { annotated_lock_guard lock(index_entries_lock); vector> result; for (const auto &entry : index_entries) { auto index_info = entry->GetStorageInfo(); - if (!index_info.is_unique || + if (!index_info.is_unique || index_info.check_mode == ConstraintCheckMode::DEFERRED || !conflict_info.ConflictTargetMatches(index_info.is_unique, index_info.column_set)) { continue; } @@ -515,28 +549,11 @@ unordered_set TableIndexList::GetUniqueIndexColumns() const { return result; } -IndexSerializationResult TableIndexList::SerializeToDisk(QueryContext context, const IndexSerializationInfo &info) { - annotated_lock_guard lock(index_entries_lock); - - IndexSerializationResult result; - - result.owned_infos.reserve(index_entries.size()); - for (const auto &entry : index_entries) { - auto storage_info = entry->SerializeToDisk(context, info.options); - D_ASSERT(!storage_info.name.empty()); - result.owned_infos.push_back(std::move(storage_info)); - result.ordered_infos.push_back(result.owned_infos.back()); - } - - return result; -} - -unique_ptr TableIndexList::SerializeToWAL(const Identifier &name, - const case_insensitive_map_t &options) { +unique_ptr TableIndexList::SerializeToWAL(const Identifier &name, const StorageVersion version) { annotated_lock_guard lock(index_entries_lock); for (const auto &entry : index_entries) { if (entry->GetName() == name) { - return make_uniq(entry->SerializeToWAL(options)); + return make_uniq(entry->SerializeToWAL(version)); } } return nullptr; diff --git a/src/duckdb/src/storage/wal_replay.cpp b/src/duckdb/src/storage/wal_replay.cpp index 2e5d404ab..a804fdd3a 100644 --- a/src/duckdb/src/storage/wal_replay.cpp +++ b/src/duckdb/src/storage/wal_replay.cpp @@ -66,8 +66,10 @@ class ReplayState { WALReplayState replay_state; struct ReplayIndexInfo { - ReplayIndexInfo(TableIndexList &index_list, unique_ptr index, idx_t table_oid, optional_idx index_oid) - : index_list(index_list), index(std::move(index)), table_oid(table_oid), index_oid(index_oid) { + ReplayIndexInfo(TableIndexList &index_list, unique_ptr index, idx_t table_oid, optional_idx index_oid, + ConstraintCheckMode check_mode) + : index_list(index_list), index(std::move(index)), table_oid(table_oid), index_oid(index_oid), + check_mode(check_mode) { } reference index_list; @@ -78,6 +80,8 @@ class ReplayState { //! Invalid for constraint-backed indexes (i.e., UNIQUE): they have no separate catalog entry and cannot be //! targeted by DROP INDEX. optional_idx index_oid; + //! The check mode of the constraint enforced by the index. + ConstraintCheckMode check_mode; }; vector replay_index_infos; }; @@ -627,7 +631,7 @@ unique_ptr WriteAheadLogReplayer::ReplayLog(unique_ptrGetIndexes(); state.replay_index_infos.emplace_back(table_index_list, std::move(index_instance), table.oid, - /*index_oid=*/optional_idx()); + /*index_oid=*/optional_idx(), unique_info.check_mode); catalog.Alter(context, alter_info); } @@ -1259,7 +1263,8 @@ void WriteAheadLogDeserializer::ReplayCreateIndex() { auto unbound_index = make_uniq(std::move(create_info), std::move(index_info), io_manager, db); auto &table_index_list = storage.GetDataTableInfo()->GetIndexes(); - state.replay_index_infos.emplace_back(table_index_list, std::move(unbound_index), table.oid, index_entry->oid); + state.replay_index_infos.emplace_back(table_index_list, std::move(unbound_index), table.oid, index_entry->oid, + ConstraintCheckMode::DEFAULT); } void WriteAheadLogDeserializer::ReplayDropIndex() { diff --git a/src/duckdb/src/storage/write_ahead_log.cpp b/src/duckdb/src/storage/write_ahead_log.cpp index c48e43b94..2abd21975 100644 --- a/src/duckdb/src/storage/write_ahead_log.cpp +++ b/src/duckdb/src/storage/write_ahead_log.cpp @@ -389,18 +389,12 @@ void WriteAheadLog::WriteDropTableMacro(const TableMacroCatalogEntry &entry) { void SerializeIndex(AttachedDatabase &db, WriteAheadLogSerializer &serializer, TableIndexList &list, const Identifier &name) { - case_insensitive_map_t options; auto storage_version = db.GetStorageManager().GetStorageVersion(); - // Before: serialization version 3 - auto v1_0_0_storage = StorageManager::IsPriorToVersion(StorageVersion::V1_2_0, storage_version); - if (!v1_0_0_storage) { - options["v1_0_0_storage"] = v1_0_0_storage; - } - - auto info = list.SerializeToWAL(name, options); + auto info = list.SerializeToWAL(name, storage_version); if (!info) { return; } + serializer.WriteProperty(102, "index_storage_info", *info); serializer.WriteList(103, "index_storage", info->buffers.size(), [&](Serializer::List &list, idx_t i) { auto &buffers = info->buffers[i]; diff --git a/src/duckdb/src/transaction/duck_transaction.cpp b/src/duckdb/src/transaction/duck_transaction.cpp index cbae8ed87..9d90a09a1 100644 --- a/src/duckdb/src/transaction/duck_transaction.cpp +++ b/src/duckdb/src/transaction/duck_transaction.cpp @@ -139,12 +139,12 @@ void DuckTransaction::PushAppend(DuckTableEntry &table_entry, idx_t start_row, i append_info->count = row_count; } -UndoBufferReference DuckTransaction::CreateUpdateInfo(DuckTableEntry &table_entry, idx_t type_size, idx_t entries, - idx_t row_group_start) { - idx_t alloc_size = UpdateInfo::GetAllocSize(type_size); +UndoBufferReference DuckTransaction::CreateUpdateInfo(DuckTableEntry &table_entry, idx_t type_size, idx_t capacity, + idx_t value_capacity, idx_t row_group_start) { + idx_t alloc_size = UpdateInfo::GetAllocSize(type_size, capacity, value_capacity); auto undo_entry = undo_buffer.CreateEntry(UndoFlags::UPDATE_TUPLE, alloc_size); auto &update_info = UpdateInfo::Get(undo_entry); - UpdateInfo::Initialize(update_info, table_entry, GetTransactionId(), row_group_start); + UpdateInfo::Initialize(update_info, table_entry, GetTransactionId(), row_group_start, capacity, value_capacity); return undo_entry; } @@ -227,20 +227,45 @@ ErrorData DuckTransaction::PreFlushOptimisticBlocks(AttachedDatabase &db) noexce return error; } -ErrorData DuckTransaction::WriteToWAL(ClientContext &context, AttachedDatabase &db, - unique_ptr &commit_state) noexcept { - ErrorData error_data; +static void RevertCommitState(unique_ptr &commit_state) noexcept { try { - D_ASSERT(ShouldWriteToWAL(db)); - auto &storage_manager = db.GetStorageManager(); - auto wal = storage_manager.GetWAL(); - commit_state = storage_manager.GenStorageCommitState(*wal); + commit_state->RevertCommit(); + commit_state.reset(); + } catch (std::exception &) { + // Ignore this error. If we fail to RevertCommit(), just return the original exception + } +} +ErrorData DuckTransaction::AppendLocalStorage(ClientContext &context, AttachedDatabase &db, + unique_ptr &commit_state) noexcept { + ErrorData error_data; + try { + if (ShouldWriteToWAL(db)) { + auto &storage_manager = db.GetStorageManager(); + commit_state = storage_manager.GenStorageCommitState(*storage_manager.GetWAL()); + } auto &profiler = *context.client_data->profiler; auto commit_timer = profiler.StartTimer(); storage->Commit(commit_state.get()); commit_timer.EndTimer(); + } catch (std::exception &ex) { + // Call RevertCommit() outside this try-catch as it itself may throw + error_data = ErrorData(ex); + } + if (commit_state && error_data.HasError()) { + RevertCommitState(commit_state); + } + return error_data; +} +ErrorData DuckTransaction::WriteToWAL(ClientContext &context, AttachedDatabase &db, + unique_ptr &commit_state) noexcept { + ErrorData error_data; + try { + // the append may have consumed the last local change: do not ask ShouldWriteToWAL again here + D_ASSERT(commit_state); + auto wal = db.GetStorageManager().GetWAL(); + auto &profiler = *context.client_data->profiler; auto wal_timer = profiler.StartTimer(); undo_buffer.WriteToWAL(*wal, commit_state.get()); wal_timer.EndTimer(); @@ -248,21 +273,13 @@ ErrorData DuckTransaction::WriteToWAL(ClientContext &context, AttachedDatabase & // no FileSync is required here: any optimistically written blocks that the WAL references // have already been synced by FlushBulkAppendBlocksAndSync, before the commit locks were taken D_ASSERT(!commit_state->HasRowGroupData() || storage->SyncedFlushedBlocks()); - } catch (std::exception &ex) { // Call RevertCommit() outside this try-catch as it itself may throw error_data = ErrorData(ex); } - if (commit_state && error_data.HasError()) { - try { - commit_state->RevertCommit(); - commit_state.reset(); - } catch (std::exception &) { - // Ignore this error. If we fail to RevertCommit(), just return the original exception - } + RevertCommitState(commit_state); } - return error_data; } @@ -354,8 +371,8 @@ void DuckTransaction::SetModifications(DatabaseModificationType type) { require_write_lock = require_write_lock || type.AlterTable(); require_write_lock = require_write_lock || type.CreateCatalogEntry(); require_write_lock = require_write_lock || type.DropCatalogEntry(); - require_write_lock = require_write_lock || type.Sequence(); require_write_lock = require_write_lock || type.CreateIndex(); + // not SEQUENCE: a checkpoint writes a sequence's current state, and WAL replay keeps the most used one if (require_write_lock) { // obtain a shared checkpoint lock to prevent concurrent checkpoints while this transaction is running @@ -381,26 +398,4 @@ unique_ptr DuckTransaction::TryGetCheckpointLock() { } } -shared_ptr DuckTransaction::SharedLockTable(DataTableInfo &info) { - unique_lock transaction_lock(active_locks_lock); - auto entry = active_locks.find(info); - if (entry == active_locks.end()) { - entry = active_locks.insert(entry, make_pair(std::ref(info), make_uniq())); - } - auto &active_table_lock = *entry->second; - transaction_lock.unlock(); // release transaction-level lock before acquiring table-level lock - lock_guard table_lock(active_table_lock.checkpoint_lock_mutex); - auto checkpoint_lock = active_table_lock.checkpoint_lock.lock(); - // check if it is expired (or has never been acquired yet) - if (checkpoint_lock) { - // not expired - return it - return checkpoint_lock; - } - // no existing lock - obtain it - checkpoint_lock = make_shared_ptr(info.GetSharedLock()); - // store it for future reference - active_table_lock.checkpoint_lock = checkpoint_lock; - return checkpoint_lock; -} - } // namespace duckdb diff --git a/src/duckdb/src/transaction/duck_transaction_manager.cpp b/src/duckdb/src/transaction/duck_transaction_manager.cpp index 99fca626e..c961129c3 100644 --- a/src/duckdb/src/transaction/duck_transaction_manager.cpp +++ b/src/duckdb/src/transaction/duck_transaction_manager.cpp @@ -111,6 +111,7 @@ Transaction &DuckTransactionManager::StartTransaction(ClientContext &context) { } void DuckTransactionManager::SetActiveCheckpoint(idx_t checkpoint_id) { + // called under the commit lock: a commit's flush and its commit or revert are entirely before or after this active_checkpoint = checkpoint_id; } @@ -356,7 +357,8 @@ ErrorData DuckTransactionManager::CommitTransaction(ClientContext &context, Tran unique_ptr lock; auto undo_properties = transaction.GetUndoProperties(); auto checkpoint_decision = CanCheckpoint(transaction, lock, undo_properties); - unique_lock held_wal_lock; + // orders this commit's append and commit or revert against checkpoints; read-only transactions commit without it + unique_lock held_commit_lock; unique_ptr commit_state; optional_ptr commit_wal; bool skip_wal_write_due_to_checkpoint = false; @@ -374,27 +376,26 @@ ErrorData DuckTransactionManager::CommitTransaction(ClientContext &context, Tran skip_wal_write_due_to_checkpoint = true; } } - bool should_write_to_wal = !error.HasError() && transaction.ShouldWriteToWAL(db); - if (should_write_to_wal) { - auto &storage_manager = db.GetStorageManager().Cast(); - // if we are committing changes and we are not doing a "checkpoint instead of WAL write" - // we need to write to the WAL to make the changes durable - // since WAL writes can take a long time - we grab the WAL lock here and unlock the transaction lock - // read-only transactions can bypass this branch and start/commit while the WAL write is happening - // unlock the transaction lock while we write to the WAL - // note: we can only drop the transaction lock if we are NOT checkpointing - // if we are checkpointing, we have already made certain decisions (e.g. the CheckpointType) + bool has_changes = !error.HasError() && transaction.ChangesMade() && db.HasStorageManager(); + bool should_write_to_wal = has_changes && transaction.ShouldWriteToWAL(db); + if (has_changes) { + // appending the local storage and writing the WAL can take long: other transactions run meanwhile + // the appended rows stay invisible until the commit below + // note: if we are checkpointing, we have already made certain decisions (e.g. the CheckpointType) t_lock.unlock(); - // grab the WAL lock and hold it until the entire commit is finished - held_wal_lock = storage_manager.GetWALLock(); + // grab the commit lock and hold it until the entire commit is finished + held_commit_lock = db.GetStorageManager().GetCommitLock(); - // Commit the changes to the WAL. if (!skip_wal_write_due_to_checkpoint) { - error = transaction.WriteToWAL(context, db, commit_state); - wal_written = true; + error = transaction.AppendLocalStorage(context, db, commit_state); + if (!error.HasError() && should_write_to_wal) { + // Commit the changes to the WAL. + error = transaction.WriteToWAL(context, db, commit_state); + wal_written = true; + } } - // after we finish writing to the WAL we grab the transaction lock again + // after we finish writing we grab the transaction lock again t_lock.lock(); } if (!error.HasError() && checkpoint_decision.can_checkpoint) { @@ -404,11 +405,14 @@ ErrorData DuckTransactionManager::CommitTransaction(ClientContext &context, Tran if (should_write_to_wal && skip_wal_write_due_to_checkpoint && !checkpoint_decision.can_checkpoint) { // we have not written to the WAL but we have now realized we can't checkpoint after all // in order to commit we need backpeddle and write to the WAL after all - D_ASSERT(held_wal_lock.owns_lock()); + D_ASSERT(held_commit_lock.owns_lock()); // unlock the transaction lock while we are writing to the WAL t_lock.unlock(); - error = transaction.WriteToWAL(context, db, commit_state); - wal_written = true; + error = transaction.AppendLocalStorage(context, db, commit_state); + if (!error.HasError()) { + error = transaction.WriteToWAL(context, db, commit_state); + wal_written = true; + } t_lock.lock(); skip_wal_write_due_to_checkpoint = false; } @@ -489,10 +493,10 @@ ErrorData DuckTransactionManager::CommitTransaction(ClientContext &context, Tran // We do not need to hold the transaction lock during cleanup of transactions, // as they (1) have been removed, or (2) enter cleanup_info. t_lock.unlock(); - // if we have skipped the WAL write due to checkpoint, we keep the WAL lock while checkpointing + // if we have skipped the WAL write due to checkpoint, we keep the commit lock while checkpointing // this prevents any concurrent transactions from happening during this time - if (!skip_wal_write_due_to_checkpoint && held_wal_lock.owns_lock()) { - held_wal_lock.unlock(); + if (!skip_wal_write_due_to_checkpoint && held_commit_lock.owns_lock()) { + held_commit_lock.unlock(); } if (commit_wal) { @@ -554,7 +558,7 @@ ErrorData DuckTransactionManager::CommitTransaction(ClientContext &context, Tran CheckpointOptions options; options.action = CheckpointAction::ALWAYS_CHECKPOINT; options.type = checkpoint_decision.type; - options.wal_lock = held_wal_lock.owns_lock() ? &held_wal_lock : nullptr; + options.commit_lock = held_commit_lock.owns_lock() ? &held_commit_lock : nullptr; auto &storage_manager = db.GetStorageManager(); try { storage_manager.CreateCheckpoint(context, options); diff --git a/src/duckdb/src/transaction/wal_write_state.cpp b/src/duckdb/src/transaction/wal_write_state.cpp index f1bbe9a1c..6c125e5d5 100644 --- a/src/duckdb/src/transaction/wal_write_state.cpp +++ b/src/duckdb/src/transaction/wal_write_state.cpp @@ -292,7 +292,8 @@ void WALWriteState::CommitEntry(UndoFlags type, data_ptr_t data) { case UndoFlags::UPDATE_TUPLE: { // update: auto info = reinterpret_cast(data); - if (!info->segment->column_data.GetTableInfo().IsTemporary()) { + // update infos without updates were replaced by a larger update info + if (info->N > 0 && !info->segment->column_data.GetTableInfo().IsTemporary()) { WriteUpdate(*info); } break; diff --git a/src/duckdb/ub_src_optimizer.cpp b/src/duckdb/ub_src_optimizer.cpp index 9cff26665..665569589 100644 --- a/src/duckdb/ub_src_optimizer.cpp +++ b/src/duckdb/ub_src_optimizer.cpp @@ -80,6 +80,8 @@ #include "src/optimizer/remove_duplicate_groups.cpp" +#include "src/optimizer/remove_redundant_order_keys.cpp" + #include "src/optimizer/remove_unused_columns.cpp" #include "src/optimizer/row_group_pruner.cpp" diff --git a/src/duckdb/ub_src_storage_checkpoint.cpp b/src/duckdb/ub_src_storage_checkpoint.cpp index d7b8c0a40..bb243a6b6 100644 --- a/src/duckdb/ub_src_storage_checkpoint.cpp +++ b/src/duckdb/ub_src_storage_checkpoint.cpp @@ -4,5 +4,7 @@ #include "src/storage/checkpoint/table_data_writer.cpp" +#include "src/storage/checkpoint/table_index_writer.cpp" + #include "src/storage/checkpoint/write_overflow_strings_to_disk.cpp"