diff --git a/influxdb3/tests/cli/api.rs b/influxdb3/tests/cli/api.rs index ba445fe77b1..237821e1d65 100644 --- a/influxdb3/tests/cli/api.rs +++ b/influxdb3/tests/cli/api.rs @@ -255,6 +255,7 @@ impl ShowDatabasesQuery<'_> { pub struct DeleteDatabaseQuery<'a> { server: &'a TestServer, name: String, + hard_delete: Option, } impl TestServer { @@ -262,12 +263,25 @@ impl TestServer { DeleteDatabaseQuery { server: self, name: name.into(), + hard_delete: None, } } } impl DeleteDatabaseQuery<'_> { + pub fn with_hard_delete(mut self, when: impl Into) -> Self { + self.hard_delete = Some(when.into()); + self + } + pub fn run(self) -> Result { + let mut args = vec![self.name.as_str()]; + + if let Some(hard_delete) = &self.hard_delete { + args.push("--hard-delete"); + args.push(hard_delete); + } + self.server.run_with_confirmation( vec![ "delete", @@ -275,7 +289,7 @@ impl DeleteDatabaseQuery<'_> { "--tls-ca", "../testing-certs/rootCA.pem", ], - &[self.name.as_str()], + &args, ) } } @@ -364,6 +378,7 @@ pub struct DeleteTableQuery<'a> { server: &'a TestServer, db_name: String, table_name: String, + hard_delete: Option, } impl TestServer { @@ -376,20 +391,32 @@ impl TestServer { server: self, db_name: db_name.into(), table_name: table_name.into(), + hard_delete: None, } } } impl DeleteTableQuery<'_> { + pub fn with_hard_delete(mut self, when: impl Into) -> Self { + self.hard_delete = Some(when.into()); + self + } + pub fn run(self) -> Result { - let args = vec![ + let mut args = vec![ self.table_name.as_str(), "--database", self.db_name.as_str(), - "--tls-ca", - "../testing-certs/rootCA.pem", ]; + if let Some(hard_delete) = &self.hard_delete { + args.push("--hard-delete"); + args.push(hard_delete); + } + + args.push("--tls-ca"); + args.push("../testing-certs/rootCA.pem"); + self.server .run_with_confirmation(vec!["delete", "table"], &args) } diff --git a/influxdb3/tests/cli/mod.rs b/influxdb3/tests/cli/mod.rs index aacf874cab3..3079f3f61c3 100644 --- a/influxdb3/tests/cli/mod.rs +++ b/influxdb3/tests/cli/mod.rs @@ -325,6 +325,325 @@ async fn test_delete_missing_database() { debug!(err = ?err, "delete missing database"); assert_contains!(&err, "404"); } + +#[test_log::test(tokio::test)] +async fn test_delete_database_with_hard_delete_now() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_now"; + + // Create database first + server + .create_database(db_name) + .run() + .expect("create database"); + + // Delete with hard-delete now + let result = server + .delete_database(db_name) + .with_hard_delete("now") + .run() + .expect("delete database with hard-delete now"); + + assert_contains!( + &result, + format!("Database \"{db_name}\" deleted successfully") + ); + + // Query system.databases to verify hard_deletion_time is set + // Note: deleted databases have their names changed to include the deletion timestamp + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name, deleted, hard_deletion_time FROM system.databases WHERE database_name LIKE '{db_name}-%' AND deleted = true" + )) + .run() + .expect("query system.databases"); + + // The result is already a serde_json::Value + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be set (not null) and close to current time + assert!(data[0]["hard_deletion_time"].is_string()); +} + +#[test_log::test(tokio::test)] +async fn test_delete_database_with_hard_delete_never() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_never"; + + // Create database first + server + .create_database(db_name) + .run() + .expect("create database"); + + // Delete with hard-delete never + let result = server + .delete_database(db_name) + .with_hard_delete("never") + .run() + .expect("delete database with hard-delete never"); + + assert_contains!( + &result, + format!("Database \"{db_name}\" deleted successfully") + ); + + // Query system.databases to verify hard_deletion_time is NULL + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name, deleted, hard_deletion_time FROM system.databases WHERE database_name LIKE '{db_name}-%' AND deleted = true" + )) + .run() + .expect("query system.databases"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be null for "never" + assert!(data[0]["hard_deletion_time"].is_null()); +} + +#[test_log::test(tokio::test)] +async fn test_delete_database_with_hard_delete_default() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_default"; + + // Create database first + server + .create_database(db_name) + .run() + .expect("create database"); + + // Delete with hard-delete default + let result = server + .delete_database(db_name) + .with_hard_delete("default") + .run() + .expect("delete database with hard-delete default"); + + assert_contains!( + &result, + format!("Database \"{db_name}\" deleted successfully") + ); + + // Query system.databases to verify hard_deletion_time is set to a future time + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name, deleted, hard_deletion_time FROM system.databases WHERE database_name LIKE '{db_name}-%' AND deleted = true" + )) + .run() + .expect("query system.databases"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be set and in the future + assert!(data[0]["hard_deletion_time"].is_string()); +} + +#[test_log::test(tokio::test)] +async fn test_delete_database_with_hard_delete_timestamp() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_timestamp"; + + // Create database first + server + .create_database(db_name) + .run() + .expect("create database"); + + // Delete with a specific timestamp + let timestamp = "2025-12-31T23:59:59Z"; + let result = server + .delete_database(db_name) + .with_hard_delete(timestamp) + .run() + .expect("delete database with hard-delete timestamp"); + + assert_contains!( + &result, + format!("Database \"{db_name}\" deleted successfully") + ); + + // Query system.databases to verify hard_deletion_time matches the timestamp + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name, deleted, hard_deletion_time FROM system.databases WHERE database_name LIKE '{db_name}-%' AND deleted = true" + )) + .run() + .expect("query system.databases"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should match our timestamp + assert_eq!(data[0]["hard_deletion_time"], timestamp); +} + +#[test_log::test(tokio::test)] +async fn test_delete_database_without_hard_delete_option() { + let server = TestServer::spawn().await; + let db_name = "test_delete_no_hard_delete"; + + // Create database first + server + .create_database(db_name) + .run() + .expect("create database"); + + // Delete without hard-delete option + let result = server + .delete_database(db_name) + .run() + .expect("delete database without hard-delete option"); + + assert_contains!( + &result, + format!("Database \"{db_name}\" deleted successfully") + ); + + // Query system.databases to verify hard_deletion_time follows default behavior + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name, deleted, hard_deletion_time FROM system.databases WHERE database_name LIKE '{db_name}-%' AND deleted = true" + )) + .run() + .expect("query system.databases"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be set to a future time when no option specified + assert!(data[0]["hard_deletion_time"].is_string()); +} + +#[test_log::test(tokio::test)] +async fn test_update_hard_delete_time_on_deleted_database() { + let server = TestServer::spawn().await; + let db_name = "test_update_hard_delete"; + + // Create database first + server + .create_database(db_name) + .run() + .expect("create database"); + + // Delete with hard-delete never + let result = server + .delete_database(db_name) + .with_hard_delete("never") + .run() + .expect("delete database with hard-delete never"); + + assert_contains!( + &result, + format!("Database \"{db_name}\" deleted successfully") + ); + + // Query to find the renamed database + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name, deleted, hard_deletion_time FROM system.databases WHERE database_name LIKE '{db_name}-%' AND deleted = true" + )) + .run() + .expect("query system.databases"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert!(data[0]["hard_deletion_time"].is_null()); + + // Get the renamed database name + let renamed_db_name = data[0]["database_name"] + .as_str() + .expect("database_name should be a string"); + + // Delete again with hard-delete now to update the time + let result = server + .delete_database(renamed_db_name) + .with_hard_delete("now") + .run() + .expect("update hard-delete time"); + + assert_contains!( + &result, + format!("Database \"{renamed_db_name}\" deleted successfully") + ); + + // Verify hard_deletion_time is now set + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name, deleted, hard_deletion_time FROM system.databases WHERE database_name = '{renamed_db_name}'" + )) + .run() + .expect("query system.databases after update"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should now be set + assert!(data[0]["hard_deletion_time"].is_string()); +} + +#[test_log::test(tokio::test)] +async fn test_delete_already_deleted_database_same_hard_delete() { + let server = TestServer::spawn().await; + let db_name = "test_already_deleted"; + + // Create database first + server + .create_database(db_name) + .run() + .expect("create database"); + + // Delete with hard-delete never + let result = server + .delete_database(db_name) + .with_hard_delete("never") + .run() + .expect("delete database with hard-delete never"); + + assert_contains!( + &result, + format!("Database \"{db_name}\" deleted successfully") + ); + + // Query to find the renamed database + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT database_name FROM system.databases WHERE database_name LIKE '{db_name}-%' AND deleted = true" + )) + .run() + .expect("query system.databases"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + + // Get the renamed database name + let renamed_db_name = data[0]["database_name"] + .as_str() + .expect("database_name should be a string"); + + // Try to delete again with same hard-delete option + let result = server + .delete_database(renamed_db_name) + .with_hard_delete("never") + .run(); + + // Should get an error for already deleted + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert_contains!(&err, "409"); // Conflict - already deleted +} + #[test_log::test(tokio::test)] async fn test_create_table() { let server = TestServer::spawn().await; @@ -513,6 +832,251 @@ async fn test_delete_missing_table() { assert_contains!(err.to_string(), "404"); } +#[test_log::test(tokio::test)] +async fn test_delete_table_with_hard_delete_now() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_table_now"; + let table_name = "cpu"; + + // Create database and write data to create table + server + .create_database(db_name) + .run() + .expect("create database"); + + server + .write_lp_to_db( + db_name, + format!("{table_name},host=a temp=1.0 1000"), + influxdb3_client::Precision::Second, + ) + .await + .expect("write to db"); + + // Delete with hard-delete now + let result = server + .delete_table(db_name, table_name) + .with_hard_delete("now") + .run() + .expect("delete table with hard-delete now"); + + assert_contains!( + &result, + format!("Table \"{db_name}\".\"{table_name}\" deleted successfully") + ); + + // Query system.tables to verify hard_deletion_time is set + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT table_name, deleted, hard_deletion_time FROM system.tables WHERE table_name LIKE '{table_name}-%' AND deleted = true AND database_name = '{db_name}'" + )) + .run() + .expect("query system.tables"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be set (not null) and close to current time + assert!(data[0]["hard_deletion_time"].is_string()); +} + +#[test_log::test(tokio::test)] +async fn test_delete_table_with_hard_delete_never() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_table_never"; + let table_name = "memory"; + + // Create database and write data to create table + server + .create_database(db_name) + .run() + .expect("create database"); + + server + .write_lp_to_db( + db_name, + format!("{table_name},host=b used=100 2000"), + influxdb3_client::Precision::Second, + ) + .await + .expect("write to db"); + + // Delete with hard-delete never + let result = server + .delete_table(db_name, table_name) + .with_hard_delete("never") + .run() + .expect("delete table with hard-delete never"); + + assert_contains!( + &result, + format!("Table \"{db_name}\".\"{table_name}\" deleted successfully") + ); + + // Query system.tables to verify hard_deletion_time is NULL + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT table_name, deleted, hard_deletion_time FROM system.tables WHERE table_name LIKE '{table_name}-%' AND deleted = true AND database_name = '{db_name}'" + )) + .run() + .expect("query system.tables"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be null + assert!(data[0]["hard_deletion_time"].is_null()); +} + +#[test_log::test(tokio::test)] +async fn test_delete_table_with_hard_delete_default() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_table_default"; + let table_name = "disk"; + + // Create database and write data to create table + server + .create_database(db_name) + .run() + .expect("create database"); + + server + .write_lp_to_db( + db_name, + format!("{table_name},device=sda used=500 3000"), + influxdb3_client::Precision::Second, + ) + .await + .expect("write to db"); + + // Delete with hard-delete default + let result = server + .delete_table(db_name, table_name) + .with_hard_delete("default") + .run() + .expect("delete table with hard-delete default"); + + assert_contains!( + &result, + format!("Table \"{db_name}\".\"{table_name}\" deleted successfully") + ); + + // Query system.tables to verify hard_deletion_time is set to future time + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT table_name, deleted, hard_deletion_time FROM system.tables WHERE table_name LIKE '{table_name}-%' AND deleted = true AND database_name = '{db_name}'" + )) + .run() + .expect("query system.tables"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be set to a future time + assert!(data[0]["hard_deletion_time"].is_string()); +} + +#[test_log::test(tokio::test)] +async fn test_delete_table_with_hard_delete_timestamp() { + let server = TestServer::spawn().await; + let db_name = "test_hard_delete_table_timestamp"; + let table_name = "network"; + let timestamp = "2025-12-31T23:59:59Z"; + + // Create database and write data to create table + server + .create_database(db_name) + .run() + .expect("create database"); + + server + .write_lp_to_db( + db_name, + format!("{table_name},interface=eth0 bytes=1000 4000"), + influxdb3_client::Precision::Second, + ) + .await + .expect("write to db"); + + // Delete with hard-delete timestamp + let result = server + .delete_table(db_name, table_name) + .with_hard_delete(timestamp) + .run() + .expect("delete table with hard-delete timestamp"); + + assert_contains!( + &result, + format!("Table \"{db_name}\".\"{table_name}\" deleted successfully") + ); + + // Query system.tables to verify hard_deletion_time matches timestamp + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT table_name, deleted, hard_deletion_time FROM system.tables WHERE table_name LIKE '{table_name}-%' AND deleted = true AND database_name = '{db_name}'" + )) + .run() + .expect("query system.tables"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should match the specified timestamp + assert_eq!(data[0]["hard_deletion_time"], timestamp); +} + +#[test_log::test(tokio::test)] +async fn test_delete_table_without_hard_delete_option() { + let server = TestServer::spawn().await; + let db_name = "test_delete_table_no_option"; + let table_name = "sensors"; + + // Create database and write data to create table + server + .create_database(db_name) + .run() + .expect("create database"); + + server + .write_lp_to_db( + db_name, + format!("{table_name},sensor=temp1 value=22.5 5000"), + influxdb3_client::Precision::Second, + ) + .await + .expect("write to db"); + + // Delete without hard-delete option + let result = server + .delete_table(db_name, table_name) + .run() + .expect("delete table without hard-delete option"); + + assert_contains!( + &result, + format!("Table \"{db_name}\".\"{table_name}\" deleted successfully") + ); + + // Query system.tables to verify default behavior + let result = server + .query_sql("_internal") + .with_sql(format!( + "SELECT table_name, deleted, hard_deletion_time FROM system.tables WHERE table_name LIKE '{table_name}-%' AND deleted = true AND database_name = '{db_name}'" + )) + .run() + .expect("query system.tables"); + + let data = result.as_array().expect("result should be an array"); + assert_eq!(data.len(), 1); + assert_eq!(data[0]["deleted"], true); + // hard_deletion_time should be set to a future time (default retention) when no option specified + assert!(data[0]["hard_deletion_time"].is_string()); +} + #[tokio::test] async fn test_create_delete_distinct_cache() { let server = TestServer::spawn().await; diff --git a/influxdb3_catalog/src/catalog.rs b/influxdb3_catalog/src/catalog.rs index 2ce56fdd2d8..f94abf16be1 100644 --- a/influxdb3_catalog/src/catalog.rs +++ b/influxdb3_catalog/src/catalog.rs @@ -1800,10 +1800,14 @@ impl UpdateDatabaseSchema for SoftDeleteDatabaseLog { &self, mut schema: Cow<'a, DatabaseSchema>, ) -> Result> { - let deletion_time = Time::from_timestamp_nanos(self.deletion_time); let owned = schema.to_mut(); - owned.name = make_new_name_using_deleted_time(&self.database_name, deletion_time); - owned.deleted = true; + // If it isn't already deleted, then we must generate a "deleted" name for the schema, + // based on the deletion_time + if !owned.deleted { + let deletion_time = Time::from_timestamp_nanos(self.deletion_time); + owned.name = make_new_name_using_deleted_time(&self.database_name, deletion_time); + owned.deleted = true; + } owned.hard_delete_time = self.hard_deletion_time.map(Time::from_timestamp_nanos); Ok(schema) } @@ -1820,13 +1824,17 @@ impl UpdateDatabaseSchema for SoftDeleteTableLog { } let mut_schema = schema.to_mut(); if let Some(mut deleted_table) = mut_schema.tables.get_by_id(&self.table_id) { - let deletion_time = Time::from_timestamp_nanos(self.deletion_time); - let table_name = make_new_name_using_deleted_time(&self.table_name, deletion_time); let new_table_def = Arc::make_mut(&mut deleted_table); - new_table_def.deleted = true; + // If it isn't already deleted, then we must generate a "deleted" name for the schema, + // based on the deletion_time + if !new_table_def.deleted { + let deletion_time = Time::from_timestamp_nanos(self.deletion_time); + let table_name = make_new_name_using_deleted_time(&self.table_name, deletion_time); + new_table_def.deleted = true; + new_table_def.table_name = table_name; + } new_table_def.hard_delete_time = self.hard_deletion_time.map(Time::from_timestamp_nanos); - new_table_def.table_name = table_name; mut_schema .tables .update(new_table_def.table_id, deleted_table) @@ -5266,4 +5274,461 @@ mod tests { other => panic!("Expected Hard deletion status for table3, got {other:?}"), } } + + // Tests for idempotent default hard deletion behavior + + #[test_log::test(tokio::test)] + async fn test_database_soft_delete_default_preserves_existing_hard_delete_time() { + // Test that soft deleting a database with Default preserves existing hard_delete_time + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + + // Get database ID before soft delete + let db_id = catalog.db_name_to_id("test_db").unwrap(); + + // First soft delete with a specific timestamp + let specific_time = Time::from_timestamp_nanos(5000000000); + catalog + .soft_delete_database("test_db", HardDeletionTime::Timestamp(specific_time)) + .await + .unwrap(); + + // Verify the database is soft deleted with the specific hard_delete_time + let db_schema = catalog.db_schema_by_id(&db_id).unwrap(); + assert!(db_schema.deleted); + assert_eq!(db_schema.hard_delete_time, Some(specific_time)); + + // Get the renamed database name using the ID + let renamed_db_name = catalog + .db_schema_by_id(&db_id) + .expect("soft-deleted database should exist") + .name(); + + // Now soft delete again with Default using the renamed name + // This should return AlreadyDeleted since nothing changes + let result = catalog + .soft_delete_database(&renamed_db_name, HardDeletionTime::Default) + .await; + + // Should get AlreadyDeleted error since hard_delete_time doesn't change + assert!( + matches!(result, Err(CatalogError::AlreadyDeleted)), + "Expected AlreadyDeleted error, got {result:?}" + ); + + // Verify hard_delete_time is unchanged + let db_schema = catalog.db_schema_by_id(&db_id).unwrap(); + assert!(db_schema.deleted); + assert_eq!(db_schema.hard_delete_time, Some(specific_time)); + } + + #[test_log::test(tokio::test)] + async fn test_database_soft_delete_default_sets_new_when_none_exists() { + // Test that soft deleting a database with Default sets new hard_delete_time when none exists + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + + // Get database ID before soft delete + let db_id = catalog.db_name_to_id("test_db").unwrap(); + + // Soft delete with Default - should set new hard_delete_time + catalog + .soft_delete_database("test_db", HardDeletionTime::Default) + .await + .unwrap(); + + // Verify hard_delete_time is set to now + default duration + let expected_time = now + Catalog::DEFAULT_HARD_DELETE_DURATION; + let db_schema = catalog.db_schema_by_id(&db_id).unwrap(); + assert!(db_schema.deleted); + assert_eq!(db_schema.hard_delete_time, Some(expected_time)); + } + + #[test_log::test(tokio::test)] + async fn test_database_soft_delete_default_multiple_calls_idempotent() { + // Test that multiple soft delete calls with Default are idempotent + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + + // Get database ID before soft delete + let db_id = catalog.db_name_to_id("test_db").unwrap(); + + // First soft delete with a specific timestamp + let specific_time = Time::from_timestamp_nanos(5000000000); + catalog + .soft_delete_database("test_db", HardDeletionTime::Timestamp(specific_time)) + .await + .unwrap(); + + // Verify initial state + let db_schema = catalog.db_schema_by_id(&db_id).unwrap(); + assert!(db_schema.deleted); + assert_eq!(db_schema.hard_delete_time, Some(specific_time)); + + // Get the renamed database name using the ID + let renamed_db_name = catalog + .db_schema_by_id(&db_id) + .expect("soft-deleted database should exist") + .name(); + + // Call soft delete with Default multiple times - all should be idempotent + for i in 1..=3 { + let result = catalog + .soft_delete_database(&renamed_db_name, HardDeletionTime::Default) + .await; + + // Should always get AlreadyDeleted since nothing changes + assert!( + matches!(result, Err(CatalogError::AlreadyDeleted)), + "Call {i} expected AlreadyDeleted error, got {result:?}" + ); + + // Verify hard_delete_time remains unchanged + let db_schema = catalog.db_schema_by_id(&db_id).unwrap(); + assert!(db_schema.deleted); + assert_eq!( + db_schema.hard_delete_time, + Some(specific_time), + "hard_delete_time should remain unchanged after call {}", + i + ); + } + } + + #[test_log::test(tokio::test)] + async fn test_database_soft_delete_override_existing_with_specific_time() { + // Test that soft deleting with specific time overrides existing hard_delete_time + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + + // Get database ID before soft delete + let db_id = catalog.db_name_to_id("test_db").unwrap(); + + // First soft delete with Default + catalog + .soft_delete_database("test_db", HardDeletionTime::Default) + .await + .unwrap(); + + // Verify initial state with default hard_delete_time + let expected_default_time = now + Catalog::DEFAULT_HARD_DELETE_DURATION; + let db_schema = catalog.db_schema_by_id(&db_id).unwrap(); + assert!(db_schema.deleted); + assert_eq!(db_schema.hard_delete_time, Some(expected_default_time)); + + // Get the renamed database name using the ID + let renamed_db_name = catalog + .db_schema_by_id(&db_id) + .expect("soft-deleted database should exist") + .name(); + + // Now soft delete again with a specific timestamp - should update the hard_delete_time + let new_specific_time = Time::from_timestamp_nanos(7000000000); + catalog + .soft_delete_database( + &renamed_db_name, + HardDeletionTime::Timestamp(new_specific_time), + ) + .await + .unwrap(); + + // Verify hard_delete_time was updated to the new specific time + let db_schema = catalog.db_schema_by_id(&db_id).unwrap(); + assert!(db_schema.deleted); + assert_eq!(db_schema.hard_delete_time, Some(new_specific_time)); + } + + #[test_log::test(tokio::test)] + async fn test_table_soft_delete_default_preserves_existing_hard_delete_time() { + // Test that soft deleting a table with Default preserves existing hard_delete_time + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + catalog + .create_table( + "test_db", + "test_table", + &["tag1", "tag2"], + &[("field1", FieldDataType::String)], + ) + .await + .unwrap(); + + // Get the table ID before soft delete + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_id = db_schema.table_name_to_id("test_table").unwrap(); + + // First soft delete with a specific timestamp + let specific_time = Time::from_timestamp_nanos(5000000000); + catalog + .soft_delete_table( + "test_db", + "test_table", + HardDeletionTime::Timestamp(specific_time), + ) + .await + .unwrap(); + + // Get the renamed table using the table ID + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_def = db_schema + .table_definition_by_id(&table_id) + .expect("soft-deleted table should exist"); + let renamed_table_name = Arc::::clone(&table_def.table_name); + + // Verify the table is soft deleted with the specific hard_delete_time + assert!(table_def.deleted); + assert_eq!(table_def.hard_delete_time, Some(specific_time)); + + // Now soft delete again with Default using the renamed name + // This should return AlreadyDeleted since nothing changes + let result = catalog + .soft_delete_table("test_db", &renamed_table_name, HardDeletionTime::Default) + .await; + + // Should get AlreadyDeleted error since hard_delete_time doesn't change + assert!( + matches!(result, Err(CatalogError::AlreadyDeleted)), + "Expected AlreadyDeleted error, got {result:?}" + ); + + // Verify hard_delete_time is unchanged + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_def = db_schema.table_definition(&renamed_table_name).unwrap(); + assert!(table_def.deleted); + assert_eq!(table_def.hard_delete_time, Some(specific_time)); + } + + #[test_log::test(tokio::test)] + async fn test_table_soft_delete_default_sets_new_when_none_exists() { + // Test that soft deleting a table with Default sets new hard_delete_time when none exists + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + catalog + .create_table( + "test_db", + "test_table", + &["tag1"], + &[("field1", FieldDataType::Float)], + ) + .await + .unwrap(); + + // Get the table ID before soft delete + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_id = db_schema.table_name_to_id("test_table").unwrap(); + + // Soft delete with Default - should set new hard_delete_time + catalog + .soft_delete_table("test_db", "test_table", HardDeletionTime::Default) + .await + .unwrap(); + + // Get the table using the table ID + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_def = db_schema + .table_definition_by_id(&table_id) + .expect("soft-deleted table should exist"); + + // Verify hard_delete_time is set to now + default duration + let expected_time = now + Catalog::DEFAULT_HARD_DELETE_DURATION; + assert!(table_def.deleted); + assert_eq!(table_def.hard_delete_time, Some(expected_time)); + } + + #[test_log::test(tokio::test)] + async fn test_table_soft_delete_default_multiple_calls_idempotent() { + // Test that multiple soft delete calls with Default are idempotent + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + catalog + .create_table( + "test_db", + "test_table", + &["tag1", "tag2"], + &[("field1", FieldDataType::Integer)], + ) + .await + .unwrap(); + + // Get the table ID before soft delete + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_id = db_schema.table_name_to_id("test_table").unwrap(); + + // First soft delete with a specific timestamp + let specific_time = Time::from_timestamp_nanos(5000000000); + catalog + .soft_delete_table( + "test_db", + "test_table", + HardDeletionTime::Timestamp(specific_time), + ) + .await + .unwrap(); + + // Get the renamed table name using the table ID + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_def = db_schema + .table_definition_by_id(&table_id) + .expect("soft-deleted table should exist"); + let renamed_table_name = Arc::::clone(&table_def.table_name); + + // Call soft delete with Default multiple times - all should be idempotent + for i in 1..=3 { + let result = catalog + .soft_delete_table("test_db", &renamed_table_name, HardDeletionTime::Default) + .await; + + // Should always get AlreadyDeleted since nothing changes + assert!( + matches!(result, Err(CatalogError::AlreadyDeleted)), + "Call {i} expected AlreadyDeleted error, got {result:?}" + ); + + // Verify hard_delete_time remains unchanged + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_def = db_schema.table_definition_by_id(&table_id).unwrap(); + assert!(table_def.deleted); + assert_eq!( + table_def.hard_delete_time, + Some(specific_time), + "hard_delete_time should remain unchanged after call {}", + i + ); + } + } + + #[test_log::test(tokio::test)] + async fn test_table_soft_delete_override_existing_with_specific_time() { + // Test that soft deleting with specific time overrides existing hard_delete_time + use iox_time::MockProvider; + let now = Time::from_timestamp_nanos(1000000000); + let time_provider = Arc::new(MockProvider::new(now)); + let catalog = Catalog::new_in_memory_with_args( + "test-catalog", + Arc::clone(&time_provider) as _, + CatalogArgs::default(), + ) + .await + .unwrap(); + + catalog.create_database("test_db").await.unwrap(); + catalog + .create_table( + "test_db", + "test_table", + &["tag1"], + &[("field1", FieldDataType::UInteger)], + ) + .await + .unwrap(); + + // Get the table ID before soft delete + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_id = db_schema.table_name_to_id("test_table").unwrap(); + + // First soft delete with Default + catalog + .soft_delete_table("test_db", "test_table", HardDeletionTime::Default) + .await + .unwrap(); + + // Get the renamed table and verify initial state + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_def = db_schema + .table_definition_by_id(&table_id) + .expect("soft-deleted table should exist"); + let renamed_table_name = Arc::::clone(&table_def.table_name); + + // Verify initial state with default hard_delete_time + let expected_default_time = now + Catalog::DEFAULT_HARD_DELETE_DURATION; + assert!(table_def.deleted); + assert_eq!(table_def.hard_delete_time, Some(expected_default_time)); + + // Now soft delete again with a specific timestamp - should update the hard_delete_time + let new_specific_time = Time::from_timestamp_nanos(7000000000); + catalog + .soft_delete_table( + "test_db", + &renamed_table_name, + HardDeletionTime::Timestamp(new_specific_time), + ) + .await + .unwrap(); + + // Verify hard_delete_time was updated to the new specific time + let db_schema = catalog.db_schema("test_db").unwrap(); + let table_def = db_schema.table_definition_by_id(&table_id).unwrap(); + assert!(table_def.deleted); + assert_eq!(table_def.hard_delete_time, Some(new_specific_time)); + } } diff --git a/influxdb3_catalog/src/catalog/update.rs b/influxdb3_catalog/src/catalog/update.rs index 9424cdbd2ea..03eeb7bcecd 100644 --- a/influxdb3_catalog/src/catalog/update.rs +++ b/influxdb3_catalog/src/catalog/update.rs @@ -51,12 +51,31 @@ pub enum HardDeletionTime { } impl HardDeletionTime { - fn value(self, time_provider: &dyn TimeProvider, default: Duration) -> Option { + fn as_time( + self, + time_provider: &dyn TimeProvider, + default: Duration, + ) -> Option { match self { HardDeletionTime::Never => None, - HardDeletionTime::Default => Some(time_provider.now().add(default).timestamp_nanos()), - HardDeletionTime::Timestamp(time) => Some(time.timestamp_nanos()), - HardDeletionTime::Now => Some(time_provider.now().timestamp_nanos()), + HardDeletionTime::Default => Some(time_provider.now().add(default)), + HardDeletionTime::Timestamp(time) => Some(time), + HardDeletionTime::Now => Some(time_provider.now()), + } + } + + fn is_default(self) -> bool { + matches!(self, HardDeletionTime::Default) + } +} + +impl std::fmt::Display for HardDeletionTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HardDeletionTime::Never => write!(f, "never"), + HardDeletionTime::Default => write!(f, "default"), + HardDeletionTime::Timestamp(time) => write!(f, "{time}"), + HardDeletionTime::Now => write!(f, "now"), } } } @@ -285,7 +304,6 @@ impl Catalog { name: &str, hard_delete_time: HardDeletionTime, ) -> Result { - info!(name, "soft delete database"); self.catalog_update_with_retry(|| { if name == INTERNAL_DB_NAME { return Err(CatalogError::CannotDeleteInternalDatabase); @@ -294,7 +312,17 @@ impl Catalog { let Some(db) = self.db_schema(name) else { return Err(CatalogError::NotFound); }; - if db.deleted { + + // If the request specifies the default hard-delete time, and the schema has an existing hard_delete_time, + // use that for the default, so the DELETE operation is idempotent. + let resolved_hard_delete_time = if hard_delete_time.is_default() && let Some(existing) = db.hard_delete_time { + Some(existing) + } else { + hard_delete_time.as_time(&self.time_provider, self.default_hard_delete_duration()) + }; + + let hard_delete_changed = db.hard_delete_time != resolved_hard_delete_time; + if db.deleted && !hard_delete_changed { return Err(CatalogError::AlreadyDeleted); } let deletion_time = self.time_provider.now().timestamp_nanos(); @@ -308,13 +336,24 @@ impl Catalog { database_id, database_name: db.name(), deletion_time, - hard_deletion_time: hard_delete_time - .value(&self.time_provider, self.default_hard_delete_duration()), + hard_deletion_time: resolved_hard_delete_time.map(|t|t.timestamp_nanos()), }, )], )) }) .await + .inspect(|batch| { + let Some(op) = batch + .catalog_batch + .as_database() + .and_then(|db| db.ops.first()) + .and_then(|op| op.as_soft_delete_database()) + else { + return; + }; + + info!(db_name = %op.database_name, db_id = %op.database_id, %hard_delete_time, "Delete database."); + }) } pub async fn create_table( @@ -339,7 +378,6 @@ impl Catalog { table_name: &str, hard_delete_time: HardDeletionTime, ) -> Result { - info!(db_name, table_name, "soft delete database"); self.catalog_update_with_retry(|| { let Some(db) = self.db_schema(db_name) else { return Err(CatalogError::NotFound); @@ -347,6 +385,19 @@ impl Catalog { let Some(tbl_def) = db.table_definition(table_name) else { return Err(CatalogError::NotFound); }; + + // If the request specifies the default hard-delete time, and the schema has an existing hard_delete_time, + // use that for the default, so the DELETE operation is idempotent. + let resolved_hard_delete_time = if hard_delete_time.is_default() && let Some(existing) = tbl_def.hard_delete_time { + Some(existing) + } else { + hard_delete_time.as_time(&self.time_provider, self.default_hard_delete_duration()) + }; + + let hard_delete_changed = tbl_def.hard_delete_time != resolved_hard_delete_time; + if tbl_def.deleted && !hard_delete_changed { + return Err(CatalogError::AlreadyDeleted); + } let deletion_time = self.time_provider.now().timestamp_nanos(); Ok(CatalogBatch::database( deletion_time, @@ -358,12 +409,23 @@ impl Catalog { table_id: tbl_def.table_id, table_name: Arc::clone(&tbl_def.table_name), deletion_time, - hard_deletion_time: hard_delete_time - .value(&self.time_provider, self.default_hard_delete_duration()), + hard_deletion_time: resolved_hard_delete_time.map(|t|t.timestamp_nanos()), })], )) }) .await + .inspect(|batch| { + let Some(op) = batch + .catalog_batch + .as_database() + .and_then(|db| db.ops.first()) + .and_then(|op| op.as_soft_delete_table()) + else { + return; + }; + + info!(db_name = %op.database_name, db_id = %op.database_id, table_name = %op.table_name, table_id = %op.table_id, %hard_delete_time, "Delete table.") + }) } /// Permanently delete a table from the catalog. diff --git a/influxdb3_catalog/src/log/versions/v3.rs b/influxdb3_catalog/src/log/versions/v3.rs index 8c3869b79c8..4a45bcd9838 100644 --- a/influxdb3_catalog/src/log/versions/v3.rs +++ b/influxdb3_catalog/src/log/versions/v3.rs @@ -258,9 +258,21 @@ pub enum DatabaseCatalogOp { impl DatabaseCatalogOp { pub fn to_create_last_cache(self) -> Option { match self { - DatabaseCatalogOp::CreateLastCache(create_last_cache_log) => { - Some(create_last_cache_log) - } + Self::CreateLastCache(create_last_cache_log) => Some(create_last_cache_log), + _ => None, + } + } + + pub fn as_soft_delete_database(&self) -> Option<&SoftDeleteDatabaseLog> { + match self { + Self::SoftDeleteDatabase(log) => Some(log), + _ => None, + } + } + + pub fn as_soft_delete_table(&self) -> Option<&SoftDeleteTableLog> { + match self { + Self::SoftDeleteTable(log) => Some(log), _ => None, } } diff --git a/influxdb3_server/src/lib.rs b/influxdb3_server/src/lib.rs index 8f6c794feec..a1424fc1780 100644 --- a/influxdb3_server/src/lib.rs +++ b/influxdb3_server/src/lib.rs @@ -696,7 +696,7 @@ mod tests { } #[tokio::test] - async fn delete_table_defaults_to_hard_delete_never() { + async fn delete_table_defaults_to_hard_delete_default() { let start_time = 0; let (server, shutdown, write_buffer) = setup_server(start_time).await; @@ -737,11 +737,12 @@ mod tests { .find(|table| table.deleted && table.table_name.starts_with(table_name)) .expect("deleted table should exist"); - // Verify the table is marked as deleted and hard_delete_time is None (Never) + // Verify the table is marked as deleted and hard_delete_time is set to default duration assert!(deleted_table.deleted, "table should be marked as deleted"); - assert!( - deleted_table.hard_delete_time.is_none(), - "hard_delete_time should be None (Never) when hard_delete_at is omitted" + assert_eq!( + deleted_table.hard_delete_time.unwrap().timestamp_nanos(), + start_time + Catalog::DEFAULT_HARD_DELETE_DURATION.as_nanos() as i64, + "hard_delete_time should be set to default duration when hard_delete_at is omitted" ); shutdown.cancel(); @@ -1021,7 +1022,7 @@ mod tests { } #[tokio::test] - async fn delete_database_defaults_to_hard_delete_never() { + async fn delete_database_defaults_to_hard_delete_default() { let start_time = 0; let (server, shutdown, write_buffer) = setup_server(start_time).await; @@ -1058,11 +1059,12 @@ mod tests { .find(|db| db.deleted && db.name.starts_with(db_name)) .expect("deleted database should exist"); - // Verify the database is marked as deleted and hard_delete_time is None (Never) + // Verify the database is marked as deleted and hard_delete_time is set to default duration assert!(deleted_db.deleted, "database should be marked as deleted"); - assert!( - deleted_db.hard_delete_time.is_none(), - "hard_delete_time should be None (Never) when hard_delete_at is omitted" + assert_eq!( + deleted_db.hard_delete_time.unwrap().timestamp_nanos(), + start_time + Catalog::DEFAULT_HARD_DELETE_DURATION.as_nanos() as i64, + "hard_delete_time should be set to default duration when hard_delete_at is omitted" ); shutdown.cancel(); diff --git a/influxdb3_server/src/system_tables/databases.rs b/influxdb3_server/src/system_tables/databases.rs index 78a325899e1..e9fd1eb5391 100644 --- a/influxdb3_server/src/system_tables/databases.rs +++ b/influxdb3_server/src/system_tables/databases.rs @@ -1,8 +1,9 @@ use std::sync::Arc; +use crate::system_tables::DEFAULT_TIMEZONE; use arrow::array::{StringViewBuilder, UInt64Builder}; use arrow_array::{ArrayRef, RecordBatch}; -use arrow_schema::{DataType, Field, Schema, SchemaRef}; +use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit}; use datafusion::{error::DataFusionError, logical_expr::Expr}; use influxdb3_catalog::catalog::{Catalog, RetentionPeriod}; use iox_system_tables::IoxSystemTable; @@ -27,6 +28,11 @@ fn databases_schema() -> SchemaRef { Field::new("database_name", DataType::Utf8View, false), Field::new("retention_period_ns", DataType::UInt64, true), Field::new("deleted", DataType::Boolean, false), + Field::new( + "hard_deletion_time", + DataType::Timestamp(TimeUnit::Second, Some(DEFAULT_TIMEZONE.into())), + true, + ), ]; Arc::new(Schema::new(columns)) } @@ -47,6 +53,10 @@ impl IoxSystemTable for DatabasesTable { let mut database_name_arr = StringViewBuilder::with_capacity(databases.len()); let mut retention_period_arr = UInt64Builder::with_capacity(databases.len()); let mut deleted_arr = arrow::array::BooleanBuilder::with_capacity(databases.len()); + let mut hard_deletion_time_arr = + arrow::array::TimestampSecondBuilder::with_capacity(databases.len()).with_data_type( + DataType::Timestamp(TimeUnit::Second, Some(DEFAULT_TIMEZONE.into())), + ); for db in databases { database_name_arr.append_value(&db.name); @@ -59,12 +69,19 @@ impl IoxSystemTable for DatabasesTable { } deleted_arr.append_value(db.deleted); + + if let Some(hard_delete_time) = &db.hard_delete_time { + hard_deletion_time_arr.append_value(hard_delete_time.timestamp()) + } else { + hard_deletion_time_arr.append_null() + } } let columns: Vec = vec![ Arc::new(database_name_arr.finish()), Arc::new(retention_period_arr.finish()), Arc::new(deleted_arr.finish()), + Arc::new(hard_deletion_time_arr.finish()), ]; RecordBatch::try_new(self.schema(), columns).map_err(DataFusionError::from) diff --git a/influxdb3_server/src/system_tables/mod.rs b/influxdb3_server/src/system_tables/mod.rs index 1c2f8dbf622..77aa79442dc 100644 --- a/influxdb3_server/src/system_tables/mod.rs +++ b/influxdb3_server/src/system_tables/mod.rs @@ -45,6 +45,8 @@ pub(crate) const TOKENS_TABLE_NAME: &str = "tokens"; pub(crate) const DATABASES_TABLE_NAME: &str = "databases"; pub(crate) const TABLES_TABLE_NAME: &str = "tables"; pub(crate) const GENERATION_DURATIONS_TABLE_NAME: &str = "generation_durations"; +/// The default timezone used in the system schema. +pub(crate) const DEFAULT_TIMEZONE: &str = "UTC"; const PROCESSING_ENGINE_TRIGGERS_TABLE_NAME: &str = "processing_engine_triggers"; diff --git a/influxdb3_server/src/system_tables/tables.rs b/influxdb3_server/src/system_tables/tables.rs index a7dcea1ddca..006cb6eda13 100644 --- a/influxdb3_server/src/system_tables/tables.rs +++ b/influxdb3_server/src/system_tables/tables.rs @@ -1,8 +1,9 @@ use std::sync::Arc; +use crate::system_tables::DEFAULT_TIMEZONE; use arrow::array::{StringViewBuilder, UInt64Builder}; use arrow_array::{ArrayRef, RecordBatch}; -use arrow_schema::{DataType, Field, Schema, SchemaRef}; +use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit}; use datafusion::{error::DataFusionError, logical_expr::Expr}; use influxdb3_catalog::catalog::Catalog; use iox_system_tables::IoxSystemTable; @@ -31,6 +32,11 @@ fn tables_schema() -> SchemaRef { Field::new("last_cache_count", DataType::UInt64, false), Field::new("distinct_cache_count", DataType::UInt64, false), Field::new("deleted", DataType::Boolean, false), + Field::new( + "hard_deletion_time", + DataType::Timestamp(TimeUnit::Second, Some(DEFAULT_TIMEZONE.into())), + true, + ), ]; Arc::new(Schema::new(columns)) } @@ -61,6 +67,10 @@ impl IoxSystemTable for TablesTable { let mut last_cache_count_arr = UInt64Builder::with_capacity(total_tables); let mut distinct_cache_count_arr = UInt64Builder::with_capacity(total_tables); let mut deleted_arr = arrow::array::BooleanBuilder::with_capacity(total_tables); + let mut hard_deletion_time_arr = + arrow::array::TimestampSecondBuilder::with_capacity(total_tables).with_data_type( + DataType::Timestamp(TimeUnit::Second, Some(DEFAULT_TIMEZONE.into())), + ); for db in databases { for table in db.tables.resource_iter() { @@ -76,6 +86,12 @@ impl IoxSystemTable for TablesTable { distinct_cache_count_arr .append_value(table.distinct_caches.resource_iter().count() as u64); deleted_arr.append_value(table.deleted); + + if let Some(hard_delete_time) = &table.hard_delete_time { + hard_deletion_time_arr.append_value(hard_delete_time.timestamp()) + } else { + hard_deletion_time_arr.append_null() + } } } @@ -87,6 +103,7 @@ impl IoxSystemTable for TablesTable { Arc::new(last_cache_count_arr.finish()), Arc::new(distinct_cache_count_arr.finish()), Arc::new(deleted_arr.finish()), + Arc::new(hard_deletion_time_arr.finish()), ]; RecordBatch::try_new(self.schema(), columns).map_err(DataFusionError::from) diff --git a/influxdb3_types/src/http.rs b/influxdb3_types/src/http.rs index b7b1b48076d..fc0d15cfe3e 100644 --- a/influxdb3_types/src/http.rs +++ b/influxdb3_types/src/http.rs @@ -18,10 +18,10 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Default)] pub enum HardDeletionTime { - #[default] Never, Timestamp(String), Now, + #[default] Default, }