From 5a0974e781dad95ded2c261cdf62dda97747eda0 Mon Sep 17 00:00:00 2001 From: Taras Veretilnyk Date: Thu, 14 May 2026 12:42:32 +0200 Subject: [PATCH] schema: add per-table large_data_guardrails_enabled flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cql3/statements/cf_prop_defs.cc | 11 +++++++++++ cql3/statements/cf_prop_defs.hh | 1 + db/schema_tables.cc | 31 +++++++++++++++++++++++++++++++ gms/feature_service.hh | 1 + schema/schema.cc | 1 + schema/schema.hh | 5 +++++ schema/schema_builder.hh | 4 ++++ test/boost/schema_change_test.cc | 2 +- 8 files changed, 55 insertions(+), 1 deletion(-) diff --git a/cql3/statements/cf_prop_defs.cc b/cql3/statements/cf_prop_defs.cc index fb055de7d9..5d336c51a9 100644 --- a/cql3/statements/cf_prop_defs.cc +++ b/cql3/statements/cf_prop_defs.cc @@ -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 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 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 diff --git a/cql3/statements/cf_prop_defs.hh b/cql3/statements/cf_prop_defs.hh index b1d236b6d9..9157f755f0 100644 --- a/cql3/statements/cf_prop_defs.hh +++ b/cql3/statements/cf_prop_defs.hh @@ -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; diff --git a/db/schema_tables.cc b/db/schema_tables.cc index e3d2dccc4b..acc310ed1a 100644 --- a/db/schema_tables.cc +++ b/db/schema_tables.cc @@ -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 make_update_table_mutations(service::storage_pro utils::chunked_vector 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("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 version) diff --git a/gms/feature_service.hh b/gms/feature_service.hh index 0a7c1be7ad..8fb2ac024b 100644 --- a/gms/feature_service.hh +++ b/gms/feature_service.hh @@ -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: diff --git a/schema/schema.cc b/schema/schema.cc index e21d318ee5..6bd426d258 100644 --- a/schema/schema.cc +++ b/schema/schema.cc @@ -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())); } diff --git a/schema/schema.hh b/schema/schema.hh index 1f127ba681..63d50f24ab 100644 --- a/schema/schema.hh +++ b/schema/schema.hh @@ -612,6 +612,7 @@ private: // schema digest. It is also not set locally on a schema tables. std::reference_wrapper _sharder; bool _in_memory = false; + bool _large_data_guardrails_enabled = false; std::optional _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; } diff --git a/schema/schema_builder.hh b/schema/schema_builder.hh index 7ee1eb8c22..6464a23e2a 100644 --- a/schema/schema_builder.hh +++ b/schema/schema_builder.hh @@ -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&& hints); diff --git a/test/boost/schema_change_test.cc b/test/boost/schema_change_test.cc index ca6d739076..4c963c1e50 100644 --- a/test/boost/schema_change_test.cc +++ b/test/boost/schema_change_test.cc @@ -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"))); }); }