schema: add per-table large_data_guardrails_enabled flag

Add a per-table large_data_guardrails_enabled flag controlled via the CQL
table property WITH large_data_guardrails_enabled = true|false.

Store the flag as a boolean column in system_schema_ext.scylla_tables.
Only write a live cell when enabled; when disabled (the default), omit
the cell entirely so that old nodes that don't know this column can
still read the SSTable during rolling upgrade or rollback.  When the
property transitions from true to false via ALTER TABLE, a tombstone is
written in make_update_table_mutations to override the previous live
cell — this is safe because the CQL feature gate ensures all nodes are
upgraded before the property can be set to true.

Gate the CQL property behind the LARGE_DATA_GUARDRAILS cluster feature:
attempting to set large_data_guardrails_enabled = true before all nodes
advertise the feature raises a ConfigurationException.
This commit is contained in:
Taras Veretilnyk
2026-05-14 12:42:32 +02:00
parent f7ffc64703
commit 5a0974e781
8 changed files with 55 additions and 1 deletions

View File

@@ -60,6 +60,7 @@ const sstring cf_prop_defs::COMPACTION_ENABLED_KEY = "enabled";
const sstring cf_prop_defs::KW_TABLETS = "tablets";
const sstring cf_prop_defs::KW_STORAGE_ENGINE = "storage_engine";
const sstring cf_prop_defs::KW_LARGE_DATA_GUARDRAILS_ENABLED = "large_data_guardrails_enabled";
schema::extensions_map cf_prop_defs::make_schema_extensions(const db::extensions& exts) const {
schema::extensions_map er;
@@ -109,6 +110,7 @@ void cf_prop_defs::validate(const data_dictionary::database db, sstring ks_name,
KW_COMPRESSION, KW_CRC_CHECK_CHANCE, KW_ID, KW_PAXOSGRACESECONDS,
KW_SYNCHRONOUS_UPDATES, KW_TABLETS,
KW_STORAGE_ENGINE,
KW_LARGE_DATA_GUARDRAILS_ENABLED,
});
static std::set<sstring> obsolete_keywords({
sstring("index_interval"),
@@ -210,6 +212,12 @@ void cf_prop_defs::validate(const data_dictionary::database db, sstring ks_name,
throw exceptions::configuration_exception(format("Illegal value for '{}'", KW_STORAGE_ENGINE));
}
}
if (has_property(KW_LARGE_DATA_GUARDRAILS_ENABLED) && get_boolean(KW_LARGE_DATA_GUARDRAILS_ENABLED, false)) {
if (!db.features().large_data_guardrails) {
throw exceptions::configuration_exception("large_data_guardrails_enabled cannot be used until all nodes in the cluster enable this feature");
}
}
}
std::map<sstring, sstring> cf_prop_defs::get_compaction_type_options() const {
@@ -417,6 +425,9 @@ void cf_prop_defs::apply_to_builder(schema_builder& builder, schema::extensions_
builder.set_logstor();
}
}
if (has_property(KW_LARGE_DATA_GUARDRAILS_ENABLED)) {
builder.set_large_data_guardrails_enabled(get_boolean(KW_LARGE_DATA_GUARDRAILS_ENABLED, false));
}
}
void cf_prop_defs::validate_minimum_int(const sstring& field, int32_t minimum_value, int32_t default_value) const

View File

@@ -65,6 +65,7 @@ public:
static const sstring KW_TABLETS;
static const sstring KW_STORAGE_ENGINE;
static const sstring KW_LARGE_DATA_GUARDRAILS_ENABLED;
// FIXME: In origin the following consts are in CFMetaData.
static constexpr int32_t DEFAULT_DEFAULT_TIME_TO_LIVE = 0;

View File

@@ -338,6 +338,7 @@ schema_ptr scylla_tables(schema_features features) {
sb.with_column("tablets", map_type_impl::get_instance(utf8_type, utf8_type, false));
sb.with_column("storage_engine", utf8_type);
sb.with_column("large_data_guardrails_enabled", boolean_type);
sb.with_hash_version();
s = sb.build();
@@ -1702,6 +1703,17 @@ mutation make_scylla_tables_mutation(schema_ptr table, api::timestamp_type times
if (table->logstor_enabled()) {
m.set_clustered_cell(ckey, "storage_engine", "logstor", timestamp);
}
// Write the large_data_guardrails_enabled column only when enabled.
// When disabled (the default), omit the cell entirely so that old nodes
// that don't know this column can still read the SSTable during rolling
// upgrade or rollback. The CQL validation gate ensures that 'true' can
// only be set once all nodes support the column, so it is safe to write
// the live cell at that point.
if (table->large_data_guardrails_enabled()) {
auto& guardrails_cdef = *scylla_tables()->get_column_definition("large_data_guardrails_enabled");
m.set_clustered_cell(ckey, guardrails_cdef,
atomic_cell::make_live(*boolean_type, timestamp, boolean_type->decompose(true)));
}
// In-memory tables are deprecated since scylla-2024.1.0
// FIXME: delete the column when there's no live version supporting it anymore.
// Writing it here breaks upgrade rollback to versions that do not support the in_memory schema_feature
@@ -1935,6 +1947,23 @@ utils::chunked_vector<mutation> make_update_table_mutations(service::storage_pro
utils::chunked_vector<mutation> mutations;
add_table_or_view_to_schema_mutation(new_table, timestamp, false, mutations);
make_update_indices_mutations(sp, old_table, new_table, timestamp, mutations);
// When large_data_guardrails_enabled transitions from true to false,
// write a tombstone to override the previous live cell in scylla_tables.
// make_scylla_tables_mutation only writes a live cell when enabled, so
// the tombstone must be added here where we have the old schema.
// This is safe because the CQL feature gate ensures all nodes are
// upgraded before the property can be set to true.
if (old_table->large_data_guardrails_enabled() && !new_table->large_data_guardrails_enabled()) {
schema_ptr s = tables();
auto pkey = partition_key::from_singular(*s, new_table->ks_name());
auto ckey = clustering_key::from_singular(*s, new_table->cf_name());
mutation m(scylla_tables(), pkey);
auto& guardrails_cdef = *scylla_tables()->get_column_definition("large_data_guardrails_enabled");
m.set_clustered_cell(ckey, guardrails_cdef, atomic_cell::make_dead(timestamp, gc_clock::now()));
mutations.emplace_back(std::move(m));
}
make_update_columns_mutations(std::move(old_table), std::move(new_table), timestamp, mutations);
warn(unimplemented::cause::TRIGGERS);
@@ -2194,6 +2223,8 @@ static void prepare_builder_from_scylla_tables_row(const schema_ctxt& ctxt, sche
throw std::invalid_argument(format("Invalid value for storage_engine: {}", *storage_engine));
}
}
auto guardrails_enabled = table_row.get<bool>("large_data_guardrails_enabled");
builder.set_large_data_guardrails_enabled(guardrails_enabled.value_or(false));
}
schema_ptr create_table_from_mutations(const schema_ctxt& ctxt, schema_mutations sm, const data_dictionary::user_types_storage& user_types, schema_ptr cdc_schema, std::optional<table_schema_version> version)

View File

@@ -181,6 +181,7 @@ public:
gms::feature writetime_ttl_individual_element { *this, "WRITETIME_TTL_INDIVIDUAL_ELEMENT"sv };
gms::feature arbitrary_tablet_boundaries { *this, "ARBITRARY_TABLET_BOUNDARIES"sv };
gms::feature large_data_virtual_tables { *this, "LARGE_DATA_VIRTUAL_TABLES"sv };
gms::feature large_data_guardrails { *this, "LARGE_DATA_GUARDRAILS"sv };
gms::feature keyspace_multi_rf_change { *this, "KEYSPACE_MULTI_RF_CHANGE"sv };
public:

View File

@@ -711,6 +711,7 @@ table_schema_version schema::calculate_digest(const schema::raw_schema& r) {
}
feed_hash(h, r._props.tablet_options);
feed_hash(h, r._large_data_guardrails_enabled);
return table_schema_version(utils::UUID_gen::get_name_UUID(h.finalize()));
}

View File

@@ -612,6 +612,7 @@ private:
// schema digest. It is also not set locally on a schema tables.
std::reference_wrapper<const dht::static_sharder> _sharder;
bool _in_memory = false;
bool _large_data_guardrails_enabled = false;
std::optional<raw_view_info> _view_info;
user_properties _props;
@@ -814,6 +815,10 @@ public:
return _raw._per_partition_rate_limit_options;
}
bool large_data_guardrails_enabled() const {
return _raw._large_data_guardrails_enabled;
}
const ::speculative_retry& speculative_retry() const {
return _raw._props.speculative_retry;
}

View File

@@ -251,6 +251,10 @@ public:
_raw._in_memory = in_memory;
return *this;
}
schema_builder& set_large_data_guardrails_enabled(bool enabled) {
_raw._large_data_guardrails_enabled = enabled;
return *this;
}
schema_builder& set_tablet_options(std::map<sstring, sstring>&& hints);

View File

@@ -676,7 +676,7 @@ SEASTAR_TEST_CASE(test_system_schema_version_is_stable) {
// If you changed the schema of system.batchlog then this is expected to fail.
// Just replace expected version with the new version.
BOOST_REQUIRE_EQUAL(s->version(), table_schema_version(utils::UUID("c3f984e4-f886-3616-bb80-f8c68ed93595")));
BOOST_REQUIRE_EQUAL(s->version(), table_schema_version(utils::UUID("95ec5e20-12f2-361a-90b3-662f8999400b")));
});
}