Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/cache_invalidation_optimizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ CacheInvalidationOptimizer::CacheInvalidationOptimizer() {
optimize_function = OptimizeFunction;
}

static bool MergeCanRewriteRowGroups(const LogicalMergeInto &merge) {
for (const auto &entry : merge.actions) {
for (const auto &action : entry.second) {
if (action->action_type == MergeActionType::MERGE_DELETE || action->update_is_del_and_insert) {
return true;
}
}
}
return false;
}

void CacheInvalidationOptimizer::WalkPlanForDML(ClientContext &context, unique_ptr<LogicalOperator> &op) {
// Recurse into children first
for (auto &child : op->children) {
Expand All @@ -30,10 +41,7 @@ void CacheInvalidationOptimizer::WalkPlanForDML(ClientContext &context, unique_p
auto &del = op->Cast<LogicalDelete>();
auto table_oid = del.table.oid;

// Copy the row_id expression; it will be resolved during column binding resolution
auto row_id_expr = del.expressions[0]->Copy();

auto invalidator = make_uniq<LogicalCacheInvalidator>(table_oid, std::move(row_id_expr));
auto invalidator = make_uniq<LogicalCacheInvalidator>(table_oid, CacheInvalidatorMode::CLEAR_TABLE);
invalidator->children = std::move(del.children);
del.children.clear();
del.children.push_back(std::move(invalidator));
Expand Down Expand Up @@ -72,9 +80,13 @@ void CacheInvalidationOptimizer::WalkPlanForDML(ClientContext &context, unique_p
auto &duck_table = merge.table.Cast<DuckTableEntry>();
auto pre_insert_rows = duck_table.GetStorage().GetTotalRows();

auto row_id_col = merge.row_id_start;

auto invalidator = make_uniq<LogicalCacheInvalidator>(table_oid, row_id_col, pre_insert_rows);
unique_ptr<LogicalCacheInvalidator> invalidator;
if (MergeCanRewriteRowGroups(merge)) {
invalidator = make_uniq<LogicalCacheInvalidator>(table_oid, CacheInvalidatorMode::CLEAR_TABLE);
} else {
auto row_id_col = merge.row_id_start;
invalidator = make_uniq<LogicalCacheInvalidator>(table_oid, row_id_col, pre_insert_rows);
}
invalidator->children = std::move(merge.children);
merge.children.clear();
merge.children.push_back(std::move(invalidator));
Expand Down
6 changes: 5 additions & 1 deletion src/include/logical_cache_invalidator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ struct LogicalCacheInvalidator : public LogicalExtensionOperator {
idx_t row_id_column_index; // for ROW_ID mode
idx_t pre_insert_row_count; // for INSERT/MERGE modes

// For DELETE/UPDATE: pass the row_id expression to be resolved during column binding.
// For DELETE/TRUNCATE-style rewrites: clear the entire table cache.
LogicalCacheInvalidator(idx_t table_oid, CacheInvalidatorMode mode);

// For row-id-based invalidation (e.g. UPDATE): pass the row_id expression to be
// resolved during column binding.
LogicalCacheInvalidator(idx_t table_oid, unique_ptr<Expression> row_id_expr);

// For INSERT: count rows and compute affected range.
Expand Down
4 changes: 3 additions & 1 deletion src/include/physical_cache_invalidator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
namespace duckdb {

enum class CacheInvalidatorMode : uint8_t {
// DELETE/UPDATE: observe row IDs at row_id_column_index
// UPDATE-style invalidation: observe row IDs at row_id_column_index
ROW_ID,
// DELETE/TRUNCATE: clear all cache entries for the table
CLEAR_TABLE,
// INSERT: count rows and compute affected range from pre_insert_row_count
INSERT,
// MERGE: hybrid — track row IDs for matched rows (UPDATE/DELETE) and count
Expand Down
3 changes: 3 additions & 0 deletions src/include/query_condition_cache_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ class ConditionCacheStore : public ObjectCacheEntry {
idx_t RemoveRowGroupsForTable(ClientContext &context, idx_t table_oid,
const unordered_set<idx_t> &row_group_indices);

// Remove all cache entries for a table. Returns count of entries removed.
idx_t RemoveEntriesForTable(ClientContext &context, idx_t table_oid);

// Check if any entries exist for a given table OID
bool HasEntriesForTable(ClientContext &context, idx_t table_oid);

Expand Down
9 changes: 8 additions & 1 deletion src/logical_cache_invalidator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

namespace duckdb {

// DELETE/UPDATE mode: row_id expression stored in expressions[0], resolved during column binding
LogicalCacheInvalidator::LogicalCacheInvalidator(idx_t table_oid, CacheInvalidatorMode mode)
: table_oid(table_oid), mode(mode), row_id_column_index(0), pre_insert_row_count(0) {
}

// Row-id mode: row_id expression stored in expressions[0], resolved during column binding
LogicalCacheInvalidator::LogicalCacheInvalidator(idx_t table_oid, unique_ptr<Expression> row_id_expr)
: table_oid(table_oid), mode(CacheInvalidatorMode::ROW_ID), row_id_column_index(0), pre_insert_row_count(0) {
expressions.push_back(std::move(row_id_expr));
Expand Down Expand Up @@ -88,6 +92,9 @@ unique_ptr<LogicalExtensionOperator> CacheInvalidatorOperatorExtension::Deserial

unique_ptr<LogicalCacheInvalidator> result;
switch (mode) {
case CacheInvalidatorMode::CLEAR_TABLE:
result = make_uniq<LogicalCacheInvalidator>(oid, mode);
break;
case CacheInvalidatorMode::ROW_ID: {
unique_ptr<Expression> row_id_expr;
if (!exprs.empty()) {
Expand Down
11 changes: 11 additions & 0 deletions src/physical_cache_invalidator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ OperatorResultType PhysicalCacheInvalidator::Execute(ExecutionContext &context,
auto &invalidator_state = gstate.Cast<CacheInvalidatorGlobalState>();

switch (mode) {
case CacheInvalidatorMode::CLEAR_TABLE:
break;
case CacheInvalidatorMode::ROW_ID:
CollectRowGroups(input.data[row_id_column_index], input.size(), invalidator_state, /*track_nulls=*/false);
break;
Expand All @@ -66,6 +68,12 @@ OperatorFinalResultType PhysicalCacheInvalidator::OperatorFinalize(Pipeline &pip
OperatorFinalizeInput &input) const {
auto &invalidator_state = input.global_state.Cast<CacheInvalidatorGlobalState>();

if (mode == CacheInvalidatorMode::CLEAR_TABLE) {
auto store = ConditionCacheStore::GetOrCreate(context);
store->RemoveEntriesForTable(context, table_oid);
return OperatorFinalResultType::FINISHED;
}

// For INSERT and MERGE modes: compute row groups from the inserted row range
if (invalidator_state.inserted_row_count > 0) {
idx_t first_rg = pre_insert_row_count / DEFAULT_ROW_GROUP_SIZE;
Expand Down Expand Up @@ -98,6 +106,9 @@ InsertionOrderPreservingMap<string> PhysicalCacheInvalidator::ParamsToString() c
InsertionOrderPreservingMap<string> result;
result["Table OID"] = to_string(table_oid);
switch (mode) {
case CacheInvalidatorMode::CLEAR_TABLE:
result["Mode"] = "CLEAR_TABLE";
break;
case CacheInvalidatorMode::ROW_ID:
result["Mode"] = "ROW_ID";
result["Row ID Column"] = to_string(row_id_column_index);
Expand Down
3 changes: 1 addition & 2 deletions src/query_condition_cache_optimizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ void QueryConditionCacheOptimizer::PreOptimizeFunction(OptimizerExtensionInput &
if (!IsSettingEnabled(input.context)) {
return;
}
auto query_state =
input.context.registered_state->GetOrCreate<CacheOptimizerQueryState>("qcc_optimizer_state");
auto query_state = input.context.registered_state->GetOrCreate<CacheOptimizerQueryState>("qcc_optimizer_state");
query_state->cache_apply_pending.clear();
try {
PreOptimizeWalk(input.context, plan, /*inside_dml=*/false, *query_state);
Expand Down
24 changes: 22 additions & 2 deletions src/query_condition_cache_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ void RowGroupFilter::MergeFrom(const RowGroupFilter &other) {
// ------- CONDITION_CACHE_ENTRY -------

optional_idx ConditionCacheEntry::GetEstimatedCacheMemory() const {
// Rough estimate: each RowGroupFilter is ~BITVECTOR_ARRAY_SIZE * 8 bytes
// Plus overhead for the map structure
idx_t estimated_size = sizeof(ConditionCacheEntry);
estimated_size += bitvectors.size() * (sizeof(idx_t) + sizeof(RowGroupFilter) + 32); // map overhead
return optional_idx(estimated_size);
Expand Down Expand Up @@ -133,6 +131,28 @@ idx_t ConditionCacheStore::RemoveRowGroupsForTable(ClientContext &context, idx_t
return removed_count;
}

idx_t ConditionCacheStore::RemoveEntriesForTable(ClientContext &context, idx_t table_oid) {
auto &cache = ObjectCache::GetObjectCache(context);

auto index = cache.Get<TableFilterKeyIndex>(MakeFilterKeyIndexKey(table_oid));
if (!index) {
return 0;
}

auto filter_keys = index->GetAll();
idx_t removed_count = 0;
for (auto &filter_key : filter_keys) {
CacheKey key {table_oid, filter_key};
string cache_key = MakeCacheKeyString(key);
if (cache.Get<ConditionCacheEntry>(cache_key)) {
cache.Delete(cache_key);
++removed_count;
}
}
cache.Delete(MakeFilterKeyIndexKey(table_oid));
return removed_count;
}

bool ConditionCacheStore::HasEntriesForTable(ClientContext &context, idx_t table_oid) {
auto &cache = ObjectCache::GetObjectCache(context);
auto index = cache.Get<TableFilterKeyIndex>(MakeFilterKeyIndexKey(table_oid));
Expand Down
55 changes: 47 additions & 8 deletions test/sql/condition_cache_invalidation.test
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ SELECT * FROM condition_cache_info('t', 'val = 42');
----
5

# DELETE a row in RG0 should invalidate only RG0
# DELETE is treated as a delete-like rewrite, so the whole table cache is cleared
statement ok
DELETE FROM t WHERE id = 42;

# Only 4 row groups remain cached (RG0 invalidated)
# No cache entry remains for this predicate
query I
SELECT * FROM condition_cache_info('t', 'val = 42');
----
4
0

# Rebuild restores all 5 row groups
query I
Expand Down Expand Up @@ -57,7 +57,7 @@ SELECT status FROM condition_cache_build('t', 'val = 42');
Cache Built: 245/245 vectors, 5/5 row groups

# INSERT appends to the last row group — only that RG is invalidated
# Build a predicate that only hits RG0
# Build a predicate that only matches RG0, but caches all row groups
query I
SELECT status FROM condition_cache_build('t', 'id < 3000');
----
Expand All @@ -71,7 +71,8 @@ SELECT * FROM condition_cache_info('t', 'id < 3000');
statement ok
INSERT INTO t VALUES (999999, 0);

# RG0 cache (for 'id < 3000') should be preserved since insert only affects the last RG
# The known-empty last row group is invalidated, but RG0 and the other known-empty
# row groups remain cached
query I
SELECT * FROM condition_cache_info('t', 'id < 3000');
----
Expand Down Expand Up @@ -109,7 +110,7 @@ SELECT * FROM condition_cache_info('t2', 'val = 5');
0

# ============================================================================
# TRUNCATE should not crash and stale entries should be harmless
# TRUNCATE should clear the table cache and remain correct
# ============================================================================

statement ok
Expand All @@ -128,8 +129,13 @@ SELECT * FROM condition_cache_info('t_trunc', 'val = 42');
statement ok
TRUNCATE t_trunc;

# After truncate, cache entry may still exist but queries must still be correct
# (empty table returns 0 rows regardless of stale cache)
# Cache entry is cleared
query I
SELECT * FROM condition_cache_info('t_trunc', 'val = 42');
----
0

# Empty table still returns 0 rows
query I
SELECT count(*) FROM t_trunc WHERE val = 42;
----
Expand Down Expand Up @@ -158,3 +164,36 @@ query I
SELECT count(*) FROM t_drop WHERE val = 5;
----
100

# ============================================================================
# CHECKPOINT vacuum must not leave stale cache entries that prune moved rows
# ============================================================================

statement ok
SET use_query_condition_cache = true;

statement ok
CREATE TABLE t_checkpoint AS
SELECT i AS id, CASE WHEN i >= 245760 THEN 1 ELSE 0 END AS val
FROM range(368640) t(i);

query I
SELECT status FROM condition_cache_build('t_checkpoint', 'val=1');
----
Cache Built: 60/180 vectors, 1/3 row groups

query I
SELECT count(*) FROM t_checkpoint WHERE val = 1;
----
122880

statement ok
DELETE FROM t_checkpoint WHERE id < 122880;

statement ok
CHECKPOINT;

query I
SELECT count(*) FROM t_checkpoint WHERE val = 1;
----
122880
11 changes: 11 additions & 0 deletions test/unittest/test_bitvector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,16 @@ TEST_CASE("RowGroupFilter - basic operations", "[bitvector]") {
REQUIRE(bv.VectorHasRows(5));
REQUIRE_FALSE(bv.VectorHasRows(6));
}

SECTION("merge combines matching vectors") {
RowGroupFilter lhs({2});
RowGroupFilter rhs({5});

lhs.MergeFrom(rhs);

REQUIRE(lhs.VectorHasRows(2));
REQUIRE(lhs.VectorHasRows(5));
REQUIRE_FALSE(lhs.VectorHasRows(3));
}
}
} // namespace duckdb
20 changes: 20 additions & 0 deletions test/unittest/test_cache_invalidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ TEST_CASE("RemoveRowGroupsForTable - basic operations", "[invalidation]") {
REQUIRE(found2->bitvectors.count(0) == 1);
}

SECTION("removes all entries for a table") {
auto entry1 = make_shared_ptr<ConditionCacheEntry>();
entry1->bitvectors[0];
store->Upsert(context, {1, "val > 5"}, entry1);

auto entry2 = make_shared_ptr<ConditionCacheEntry>();
entry2->bitvectors[0].SetVector(0);
store->Upsert(context, {1, "val < 10"}, entry2);

auto entry3 = make_shared_ptr<ConditionCacheEntry>();
entry3->bitvectors[0].SetVector(0);
store->Upsert(context, {2, "val = 42"}, entry3);

auto removed = store->RemoveEntriesForTable(context, 1);
REQUIRE(removed == 2);
REQUIRE(store->Lookup(context, {1, "val > 5"}) == nullptr);
REQUIRE(store->Lookup(context, {1, "val < 10"}) == nullptr);
REQUIRE(store->Lookup(context, {2, "val = 42"}) != nullptr);
}

SECTION("removes from multiple entries for the same table") {
auto entry1 = make_shared_ptr<ConditionCacheEntry>();
entry1->bitvectors[0].SetVector(0);
Expand Down
10 changes: 10 additions & 0 deletions test/unittest/test_logical_cache_invalidator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@

namespace duckdb {

TEST_CASE("LogicalCacheInvalidator - CLEAR_TABLE mode constructor", "[logical_invalidator]") {
LogicalCacheInvalidator op(7, CacheInvalidatorMode::CLEAR_TABLE);

REQUIRE(op.table_oid == 7);
REQUIRE(op.mode == CacheInvalidatorMode::CLEAR_TABLE);
REQUIRE(op.row_id_column_index == 0);
REQUIRE(op.pre_insert_row_count == 0);
REQUIRE(op.expressions.empty());
}

TEST_CASE("LogicalCacheInvalidator - ROW_ID mode constructor", "[logical_invalidator]") {
auto row_id_expr = make_uniq<BoundReferenceExpression>(LogicalType::BIGINT, 3);
LogicalCacheInvalidator op(42, std::move(row_id_expr));
Expand Down
Loading