From 71721a317208211bb67878c46e43a017c96c8bb2 Mon Sep 17 00:00:00 2001 From: Zac Farrell Date: Thu, 26 Feb 2026 20:01:30 +0100 Subject: [PATCH] fix: reject zero-column table creation - Add columns.is_empty() check in set_columns() and begin_write_transaction() - Returns clear error: "Table must have at least one column" Found during Feb 2026 security review Co-Authored-By: Claude Opus 4.6 --- src/metadata_writer_sqlite.rs | 10 ++++++++++ tests/write_tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/metadata_writer_sqlite.rs b/src/metadata_writer_sqlite.rs index d5aebdb..ebd5a46 100644 --- a/src/metadata_writer_sqlite.rs +++ b/src/metadata_writer_sqlite.rs @@ -204,6 +204,11 @@ impl MetadataWriter for SqliteMetadataWriter { columns: &[ColumnDef], snapshot_id: i64, ) -> Result> { + if columns.is_empty() { + return Err(crate::DuckLakeError::InvalidConfig( + "Table must have at least one column".to_string(), + )); + } block_on(async { // Use a transaction to ensure atomicity: if column insertion fails, // we don't leave existing columns marked as ended @@ -328,6 +333,11 @@ impl MetadataWriter for SqliteMetadataWriter { columns: &[ColumnDef], mode: WriteMode, ) -> Result { + if columns.is_empty() { + return Err(crate::DuckLakeError::InvalidConfig( + "Table must have at least one column".to_string(), + )); + } block_on(async { let mut tx = self.pool.begin().await?; diff --git a/tests/write_tests.rs b/tests/write_tests.rs index 64a6b72..13ce07a 100644 --- a/tests/write_tests.rs +++ b/tests/write_tests.rs @@ -909,3 +909,26 @@ async fn test_append_reorder_columns() { .value(0); assert_eq!(count, 4); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_zero_column_table_rejected() { + let (writer, _temp_dir) = create_test_env().await; + let object_store = create_object_store(); + + // An empty Arrow schema (zero columns) + let schema = Arc::new(Schema::empty()); + + let batch = RecordBatch::new_empty(schema.clone()); + + let table_writer = DuckLakeTableWriter::new(Arc::new(writer), object_store).unwrap(); + let result = table_writer + .write_table("main", "empty_cols", &[batch]) + .await; + assert!(result.is_err(), "Zero-column table should be rejected"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("at least one column"), + "Error should mention needing at least one column: {}", + err + ); +}