Files
greptimedb/tests-integration/tests/table_ddl_event.rs
T
Lei, HUANG 7a2479496f chore!: gate soft-drop table behind the enterprise feature (#8747)
* chore: gate soft-drop table behind the enterprise feature

Soft-drop table becomes an enterprise-only feature:

- metasrv rejects gc.experimental_soft_drop.enable=true at startup in
  non-enterprise builds, and ddl_soft_drop_enabled is hard-disabled
  without the enterprise feature as a second line of defense
- the UNDROP TABLE parser/AST/statement variant, ADMIN purge_table()
  registration, and information_schema.recycle_bin registration are
  compiled out unless the enterprise feature is enabled
- common-meta procedures, tombstone keys, and DdlTask serde stay
  unconditional for persisted-procedure recovery and wire compatibility
- the [gc.experimental_soft_drop] section is removed from the OSS
  example config and generated docs (moving to the enterprise repo)
- the soft-drop sqlness cases and their CI job are removed from OSS
  (moving to the enterprise repo); affected information_schema .result
  files are regenerated

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* refactor: limit unused_variables allow to non-enterprise builds

Addresses review comment: apply the allow via cfg_attr so enterprise
builds still catch accidental unused variables in register_admin_only.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* refactor: include the config key in the soft-drop enterprise gate error

Addresses review comment: name gc.experimental_soft_drop.enable in the
startup validation error so users can locate the setting quickly when
it is set via env vars or layered config.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test: limit unused_mut allow to non-enterprise builds

Addresses review comment: apply the allow via cfg_attr so enterprise
builds still catch unused mut in the table_ddl_event test setup.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* feat: reject soft-drop DDL submissions in non-enterprise builds

Addresses review comment: clients could bypass the SQL-level gates by
submitting DdlTask::UndropTable or DdlTask::PurgeDroppedTable directly
to the procedure service. Reject fresh submissions at the DdlManager
boundary in non-enterprise builds while keeping the procedure loaders
registered for crash recovery and wire compatibility.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test: stop --enable-gc from enabling soft drop in the sqlness template

Addresses review comment: the metasrv test template rendered
[gc.experimental_soft_drop] enable = true under the generic --enable-gc
flag, which non-enterprise metasrv now rejects at startup, making the
documented --enable-gc mode unusable in OSS. Keep the flag scoped to
plain GC; enterprise soft-drop coverage moves to the enterprise repo.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix: gate fresh soft-drop procedures

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test: gate soft-drop fallback coverage

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix: gate soft-drop procedure implementation

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* refactor: gate drop table soft-drop behavior

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* refactor: gate expired soft-drop gc behavior

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* ci: test enterprise table ddl lifecycle

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* chore: mark purge_table as enterprise licensed

The purge_table module is compiled only with the enterprise feature, so
apply the Enterprise License header and register it with both license
header configurations.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* chore: mark recycle_bin as enterprise licensed

The recycle_bin module is compiled only with the enterprise feature, so
apply the Enterprise License header and register it with both license
header configurations.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* chore: mark soft-drop procedure sources as enterprise licensed

The purge and undrop procedure implementations plus the recycle-bin test
module compile only with the enterprise feature. Apply the Enterprise
License header and register them with both license configurations.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

---------

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>
2026-08-06 15:10:54 +08:00

466 lines
15 KiB
Rust

// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
#[cfg(feature = "enterprise")]
use std::time::Duration;
use client::OutputData;
use common_event_recorder::event_table::{
CATALOG_NAME_COLUMN, PAYLOAD_COLUMN, PHYSICAL_TABLE_ID_COLUMN, PROCEDURE_ID_COLUMN,
PROCEDURE_TRIGGER_COLUMN, SCHEMA_NAME_COLUMN, TABLE_ID_COLUMN, TABLE_NAME_COLUMN, TYPE_COLUMN,
};
use common_test_util::temp_dir::create_temp_dir;
use frontend::instance::Instance;
use meta_srv::gc::GcSchedulerOptions;
use mito2::gc::GcConfig;
use servers::query_handler::sql::SqlQueryHandler;
use session::context::QueryContext;
use tests_integration::cluster::GreptimeDbClusterBuilder;
use crate::event_recorder_test_util::{
assert_eventually_eq, assert_single_event, find_eventually_string, find_eventually_u32,
};
const EVENTS_TABLE: &str = "greptime_private.events";
const TABLE: &str = "table_ddl_events";
const PHYSICAL_TABLE: &str = "table_ddl_events_phy";
const LOGICAL_TABLE: &str = "table_ddl_events_logical";
#[tokio::test(flavor = "multi_thread")]
async fn test_table_ddl_procedure_events() {
common_telemetry::init_default_ut_logging();
// Arrange: use a distributed cluster to submit Undrop and Purge directly
// through the metasrv DDL manager and exercise logical-table task grouping.
let home_dir = create_temp_dir("table_ddl_procedure_events");
#[cfg_attr(not(feature = "enterprise"), allow(unused_mut))]
let mut gc_options = GcSchedulerOptions {
enable: true,
..Default::default()
};
// Soft drop is enterprise-only; non-enterprise builds run this test with
// plain GC and skip the undrop/purge assertions below.
#[cfg(feature = "enterprise")]
{
gc_options.experimental_soft_drop.enable = true;
gc_options.experimental_soft_drop.retention = Duration::from_secs(1);
}
let cluster = GreptimeDbClusterBuilder::new("table_ddl_procedure_events")
.await
.with_datanodes(1)
.with_metasrv_gc_config(gc_options)
.with_datanode_gc_config(GcConfig {
enable: true,
..Default::default()
})
.with_shared_home_dir(Arc::new(home_dir))
.build(true)
.await;
let frontend = cluster.fe_instance().clone();
// Act / Assert: Create Table retains its rich submitted row and records the
// created table ID when it completes.
run_sql(
&frontend,
&format!(
"CREATE TABLE {TABLE} (host STRING PRIMARY KEY, ts TIMESTAMP TIME INDEX, val DOUBLE)"
),
)
.await;
let table_id = find_table_id(&frontend, TABLE).await;
let create_table_procedure_id = submitted_procedure_id(&frontend, "create_table", TABLE).await;
assert_named_submitted_event(
&frontend,
"create_table",
&create_table_procedure_id,
"\
+--------------+------------------+
| type | name |
+--------------+------------------+
| create_table | table_ddl_events |
+--------------+------------------+",
)
.await;
assert_id_terminal_event(
&frontend,
"create_table",
&create_table_procedure_id,
table_id,
"Succeeded",
"\
+--------------+
| type |
+--------------+
| create_table |
+--------------+",
)
.await;
// Act / Assert: Alter and Truncate have a rich submitted row and lightweight
// terminal lifecycle row.
run_sql(
&frontend,
&format!("ALTER TABLE {TABLE} ADD COLUMN extra STRING"),
)
.await;
let alter_table_procedure_id = submitted_procedure_id(&frontend, "alter_table", TABLE).await;
assert_named_submitted_event(
&frontend,
"alter_table",
&alter_table_procedure_id,
"\
+-------------+------------------+
| type | name |
+-------------+------------------+
| alter_table | table_ddl_events |
+-------------+------------------+",
)
.await;
assert_lightweight_terminal_event(
&frontend,
"alter_table",
&alter_table_procedure_id,
"Succeeded",
)
.await;
run_sql(
&frontend,
&format!("INSERT INTO {TABLE} VALUES ('a', 0, 1, 'b')"),
)
.await;
run_sql(&frontend, &format!("TRUNCATE TABLE {TABLE}")).await;
let truncate_table_procedure_id =
submitted_procedure_id(&frontend, "truncate_table", TABLE).await;
assert_named_submitted_event(
&frontend,
"truncate_table",
&truncate_table_procedure_id,
"\
+----------------+------------------+
| type | name |
+----------------+------------------+
| truncate_table | table_ddl_events |
+----------------+------------------+",
)
.await;
assert_lightweight_terminal_event(
&frontend,
"truncate_table",
&truncate_table_procedure_id,
"Succeeded",
)
.await;
// Act / Assert: Drop records the table name, while Undrop and Purge use the
// table ID as their submitted locator.
run_sql(&frontend, &format!("DROP TABLE {TABLE}")).await;
let drop_table_procedure_id = submitted_procedure_id(&frontend, "drop_table", TABLE).await;
assert_named_submitted_event(
&frontend,
"drop_table",
&drop_table_procedure_id,
"\
+------------+------------------+
| type | name |
+------------+------------------+
| drop_table | table_ddl_events |
+------------+------------------+",
)
.await;
assert_lightweight_terminal_event(
&frontend,
"drop_table",
&drop_table_procedure_id,
"Succeeded",
)
.await;
#[cfg(feature = "enterprise")]
{
let (undrop_table_procedure_id, _) = cluster
.metasrv
.ddl_manager()
.submit_undrop_table_task(common_meta::rpc::ddl::UndropTableTask { table_id })
.await
.unwrap();
let undrop_table_procedure_id = undrop_table_procedure_id.to_string();
assert_id_submitted_event(
&frontend,
"undrop_table",
&undrop_table_procedure_id,
"\
+--------------+
| type |
+--------------+
| undrop_table |
+--------------+",
)
.await;
assert_lightweight_terminal_event(
&frontend,
"undrop_table",
&undrop_table_procedure_id,
"Succeeded",
)
.await;
run_sql(&frontend, &format!("DROP TABLE {TABLE}")).await;
let (purge_table_procedure_id, _) = cluster
.metasrv
.ddl_manager()
.submit_purge_dropped_table_task(common_meta::rpc::ddl::PurgeDroppedTableTask {
table_id,
})
.await
.unwrap();
let purge_table_procedure_id = purge_table_procedure_id.to_string();
assert_id_submitted_event(
&frontend,
"purge_dropped_table",
&purge_table_procedure_id,
"\
+---------------------+
| type |
+---------------------+
| purge_dropped_table |
+---------------------+",
)
.await;
assert_lightweight_terminal_event(
&frontend,
"purge_dropped_table",
&purge_table_procedure_id,
"Succeeded",
)
.await;
}
// Arrange / Act: metric logical-table DDL is executed through the distributed
// frontend, which groups logical DDL tasks for the meta procedure manager.
run_sql(
&frontend,
&format!(
"CREATE TABLE {PHYSICAL_TABLE} (ts TIMESTAMP TIME INDEX, val DOUBLE) ENGINE=metric WITH (\"physical_metric_table\" = \"\")"
),
)
.await;
let physical_table_id = find_table_id(&frontend, PHYSICAL_TABLE).await;
run_sql(
&frontend,
&format!(
"CREATE TABLE {LOGICAL_TABLE} (ts TIMESTAMP TIME INDEX, val DOUBLE, host STRING PRIMARY KEY) ENGINE=metric WITH (\"on_physical_table\" = \"{PHYSICAL_TABLE}\")"
),
)
.await;
let logical_table_id = find_table_id(&frontend, LOGICAL_TABLE).await;
let create_logical_tables_procedure_id =
submitted_procedure_id(&frontend, "create_logical_tables", LOGICAL_TABLE).await;
// Assert: logical Create emits one submitted row per logical table and
// preserves the logical and physical table IDs on its terminal row.
assert_named_submitted_event(
&frontend,
"create_logical_tables",
&create_logical_tables_procedure_id,
"\
+-----------------------+--------------------------+
| type | name |
+-----------------------+--------------------------+
| create_logical_tables | table_ddl_events_logical |
+-----------------------+--------------------------+",
)
.await;
assert_logical_terminal_event(
&frontend,
"create_logical_tables",
&create_logical_tables_procedure_id,
logical_table_id,
physical_table_id,
"Succeeded",
"\
+-----------------------+--------------------------+
| type | name |
+-----------------------+--------------------------+
| create_logical_tables | table_ddl_events_logical |
+-----------------------+--------------------------+",
)
.await;
// Act / Assert: logical Alter preserves the one-row-per-logical-table submitted
// contract and emits a lightweight terminal row.
run_sql(
&frontend,
&format!("ALTER TABLE {LOGICAL_TABLE} ADD COLUMN rack STRING PRIMARY KEY"),
)
.await;
let alter_logical_tables_procedure_id =
submitted_procedure_id(&frontend, "alter_logical_tables", LOGICAL_TABLE).await;
assert_named_submitted_event(
&frontend,
"alter_logical_tables",
&alter_logical_tables_procedure_id,
"\
+----------------------+--------------------------+
| type | name |
+----------------------+--------------------------+
| alter_logical_tables | table_ddl_events_logical |
+----------------------+--------------------------+",
)
.await;
assert_lightweight_terminal_event(
&frontend,
"alter_logical_tables",
&alter_logical_tables_procedure_id,
"Succeeded",
)
.await;
}
async fn run_sql(instance: &Arc<Instance>, sql: &str) {
let output = instance
.do_query(sql, QueryContext::arc())
.await
.remove(0)
.unwrap();
assert!(matches!(output.data, OutputData::AffectedRows(_)), "{sql}");
}
async fn find_table_id(instance: &Arc<Instance>, table_name: &str) -> u32 {
find_eventually_u32(
instance,
&format!(
"SELECT table_id FROM information_schema.tables WHERE table_catalog = 'greptime' AND table_schema = 'public' AND table_name = '{table_name}'"
),
"table_id",
)
.await
}
async fn submitted_procedure_id(
instance: &Arc<Instance>,
event_type: &str,
table_name: &str,
) -> String {
let query = format!(
"SELECT {} FROM {EVENTS_TABLE} WHERE {} = '{event_type}' AND {} = '{table_name}' AND json_path_match({}, '$.type == \"Submitted\"') AND json_path_match({}, '$.version == 1') ORDER BY timestamp DESC LIMIT 1",
PROCEDURE_ID_COLUMN.name(),
TYPE_COLUMN.name(),
TABLE_NAME_COLUMN.name(),
PROCEDURE_TRIGGER_COLUMN.name(),
PAYLOAD_COLUMN.name(),
);
find_eventually_string(instance, &query, PROCEDURE_ID_COLUMN.name()).await
}
async fn assert_named_submitted_event(
instance: &Arc<Instance>,
event_type: &str,
procedure_id: &str,
expected: &str,
) {
let query = format!(
"SELECT {}, {} AS name FROM {EVENTS_TABLE} WHERE {} = '{event_type}' AND {} = '{procedure_id}' AND json_path_match({}, '$.type == \"Submitted\"') AND json_path_match({}, '$.version == 1')",
TYPE_COLUMN.name(),
TABLE_NAME_COLUMN.name(),
TYPE_COLUMN.name(),
PROCEDURE_ID_COLUMN.name(),
PROCEDURE_TRIGGER_COLUMN.name(),
PAYLOAD_COLUMN.name(),
);
assert_eventually_eq(instance, &query, expected).await;
}
#[cfg(feature = "enterprise")]
async fn assert_id_submitted_event(
instance: &Arc<Instance>,
event_type: &str,
procedure_id: &str,
expected: &str,
) {
let query = format!(
"SELECT {} FROM {EVENTS_TABLE} WHERE {} = '{event_type}' AND {} = '{procedure_id}' AND json_path_match({}, '$.type == \"Submitted\"') AND json_path_match({}, '$.version == 1')",
TYPE_COLUMN.name(),
TYPE_COLUMN.name(),
PROCEDURE_ID_COLUMN.name(),
PROCEDURE_TRIGGER_COLUMN.name(),
PAYLOAD_COLUMN.name(),
);
assert_eventually_eq(instance, &query, expected).await;
}
async fn assert_id_terminal_event(
instance: &Arc<Instance>,
event_type: &str,
procedure_id: &str,
table_id: u32,
terminal_trigger: &str,
expected: &str,
) {
let query = format!(
"SELECT {} FROM {EVENTS_TABLE} WHERE {} = '{event_type}' AND {} = '{procedure_id}' AND json_path_match({}, '$.type == \"{terminal_trigger}\"') AND {} = {table_id} AND json_is_null({}) AND {} IS NULL AND {} IS NULL AND {} IS NULL",
TYPE_COLUMN.name(),
TYPE_COLUMN.name(),
PROCEDURE_ID_COLUMN.name(),
PROCEDURE_TRIGGER_COLUMN.name(),
TABLE_ID_COLUMN.name(),
PAYLOAD_COLUMN.name(),
CATALOG_NAME_COLUMN.name(),
SCHEMA_NAME_COLUMN.name(),
TABLE_NAME_COLUMN.name(),
);
assert_eventually_eq(instance, &query, expected).await;
}
async fn assert_logical_terminal_event(
instance: &Arc<Instance>,
event_type: &str,
procedure_id: &str,
table_id: u32,
physical_table_id: u32,
terminal_trigger: &str,
expected: &str,
) {
let query = format!(
"SELECT {}, {} AS name FROM {EVENTS_TABLE} WHERE {} = '{event_type}' AND {} = '{procedure_id}' AND json_path_match({}, '$.type == \"{terminal_trigger}\"') AND {} = {table_id} AND {} = {physical_table_id} AND json_is_null({})",
TYPE_COLUMN.name(),
TABLE_NAME_COLUMN.name(),
TYPE_COLUMN.name(),
PROCEDURE_ID_COLUMN.name(),
PROCEDURE_TRIGGER_COLUMN.name(),
TABLE_ID_COLUMN.name(),
PHYSICAL_TABLE_ID_COLUMN.name(),
PAYLOAD_COLUMN.name(),
);
assert_eventually_eq(instance, &query, expected).await;
}
async fn assert_lightweight_terminal_event(
instance: &Arc<Instance>,
event_type: &str,
procedure_id: &str,
terminal_trigger: &str,
) {
let query = format!(
"SELECT count(*) AS event_count FROM {EVENTS_TABLE} WHERE {} = '{event_type}' AND {} = '{procedure_id}' AND json_path_match({}, '$.type == \"{terminal_trigger}\"') AND json_is_null({}) AND {} IS NULL AND {} IS NULL AND {} IS NULL AND {} IS NULL",
TYPE_COLUMN.name(),
PROCEDURE_ID_COLUMN.name(),
PROCEDURE_TRIGGER_COLUMN.name(),
PAYLOAD_COLUMN.name(),
CATALOG_NAME_COLUMN.name(),
SCHEMA_NAME_COLUMN.name(),
TABLE_NAME_COLUMN.name(),
TABLE_ID_COLUMN.name(),
);
assert_single_event(instance, &query).await;
}