Skip to content

Commit 2073c9b

Browse files
sanujbasuSanuj Basu
authored andcommitted
feat: Add with_table_properties method to CreateTableTransactionBuilder
Add the ability to set table properties when creating a Delta table. Changes: - Add with_table_properties() method that accepts any iterable of (K, V) pairs - Supports only custom properties. Delta properties and signal flags to support features are deny listed. They will be selectively enabled for supported functionality. - Method can be called multiple times (properties merge, duplicates overwrite) Example: create_table(path, schema, engine_info) .with_table_properties([ ("myapp.version", "1.0"), ]) .build(engine, committer)? .commit(engine)?; Tests Unit tests added to validate create table building with supported and unsupported properties.
1 parent 48038fc commit 2073c9b

File tree

3 files changed

+202
-9
lines changed

3 files changed

+202
-9
lines changed

kernel/src/table_features/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ pub const TABLE_FEATURES_MIN_READER_VERSION: i32 = 3;
2424
/// When set to 7, the protocol requires an explicit `writerFeatures` array.
2525
pub const TABLE_FEATURES_MIN_WRITER_VERSION: i32 = 7;
2626

27+
/// Prefix for table feature override properties.
28+
/// Properties with this prefix (e.g., `delta.feature.deletionVectors`) are used to
29+
/// explicitly turn on support for the feature in the protocol.
30+
pub const SET_TABLE_FEATURE_SUPPORTED_PREFIX: &str = "delta.feature.";
31+
32+
/// Value to add support for a table feature when used with [`SET_TABLE_FEATURE_SUPPORTED_PREFIX`].
33+
/// Example: `"delta.feature.deletionVectors" -> "supported"`
34+
pub const SET_TABLE_FEATURE_SUPPORTED_VALUE: &str = "supported";
35+
2736
/// Table features represent protocol capabilities required to correctly read or write a given table.
2837
/// - Readers must implement all features required for correct table reads.
2938
/// - Writers must implement all features required for correct table writes.

kernel/src/table_properties.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ use strum::EnumString;
2323
mod deserialize;
2424
pub use deserialize::ParseIntervalError;
2525

26+
/// Prefix for delta table properties (e.g., `delta.enableChangeDataFeed`, `delta.appendOnly`).
27+
pub const DELTA_PROPERTY_PREFIX: &str = "delta.";
28+
2629
/// Delta table properties. These are parsed from the 'configuration' map in the most recent
2730
/// 'Metadata' action of a table.
2831
///

kernel/src/transaction/create_table.rs

Lines changed: 190 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,29 @@
22
//!
33
//! This module provides a type-safe API for creating Delta tables.
44
//! Use the [`create_table`] function to get a [`CreateTableTransactionBuilder`] that can be
5-
//! configured before building the [`Transaction`].
5+
//! configured with table properties and other options before building the [`Transaction`].
6+
//!
7+
//! # Example
8+
//!
9+
//! ```rust,no_run
10+
//! use delta_kernel::transaction::create_table::create_table;
11+
//! use delta_kernel::schema::{StructType, StructField, DataType};
12+
//! use delta_kernel::committer::FileSystemCommitter;
13+
//! use std::sync::Arc;
14+
//! # use delta_kernel::Engine;
15+
//! # fn example(engine: &dyn Engine) -> delta_kernel::DeltaResult<()> {
16+
//!
17+
//! let schema = Arc::new(StructType::try_new(vec![
18+
//! StructField::new("id", DataType::INTEGER, false),
19+
//! ])?);
20+
//!
21+
//! let result = create_table("/path/to/table", schema, "MyApp/1.0")
22+
//! .with_table_properties([("myapp.version", "1.0")])
23+
//! .build(engine, Box::new(FileSystemCommitter::new()))?
24+
//! .commit(engine)?;
25+
//! # Ok(())
26+
//! # }
27+
//! ```
628
729
// Allow `pub` items in this module even though the module itself may be `pub(crate)`.
830
// The module visibility controls external access; items are `pub` for use within the crate
@@ -20,11 +42,25 @@ use crate::log_segment::LogSegment;
2042
use crate::schema::SchemaRef;
2143
use crate::snapshot::Snapshot;
2244
use crate::table_configuration::TableConfiguration;
23-
use crate::table_features::{TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION};
45+
use crate::table_features::{
46+
SET_TABLE_FEATURE_SUPPORTED_PREFIX, TABLE_FEATURES_MIN_READER_VERSION,
47+
TABLE_FEATURES_MIN_WRITER_VERSION,
48+
};
49+
use crate::table_properties::DELTA_PROPERTY_PREFIX;
2450
use crate::transaction::Transaction;
2551
use crate::utils::{current_time_ms, try_parse_uri};
2652
use crate::{DeltaResult, Engine, Error, StorageHandler, PRE_COMMIT_VERSION};
2753

54+
/// Properties that are allowed to be set during create table.
55+
/// This list will expand as more features are supported (e.g., column mapping, clustering).
56+
/// The allow list will be deprecated once auto feature enablement is implemented
57+
/// like the Java Kernel.
58+
const ALLOWED_DELTA_PROPERTIES: &[&str] = &[
59+
// Empty for now - will add properties as features are implemented:
60+
// - "delta.columnMapping.mode" (for column mapping)
61+
// - etc.
62+
];
63+
2864
/// Ensures that no Delta table exists at the given path.
2965
///
3066
/// This function checks the `_delta_log` directory to determine if a table already exists.
@@ -78,6 +114,35 @@ fn ensure_table_does_not_exist(
78114
}
79115
}
80116

117+
/// Validates that table properties are allowed during CREATE TABLE.
118+
///
119+
/// This function enforces an allow list for delta properties:
120+
/// - Feature override properties (`delta.feature.*`) are never allowed
121+
/// - Delta properties (`delta.*`) must be on the allow list
122+
/// - Non-delta properties (user/application properties) are always allowed
123+
fn validate_table_properties(properties: &HashMap<String, String>) -> DeltaResult<()> {
124+
for key in properties.keys() {
125+
// Block all delta.feature.* properties (feature override properties)
126+
if key.starts_with(SET_TABLE_FEATURE_SUPPORTED_PREFIX) {
127+
return Err(Error::generic(format!(
128+
"Setting feature override property '{}' is not supported during CREATE TABLE",
129+
key
130+
)));
131+
}
132+
// For delta.* properties, check against allow list
133+
if key.starts_with(DELTA_PROPERTY_PREFIX)
134+
&& !ALLOWED_DELTA_PROPERTIES.contains(&key.as_str())
135+
{
136+
return Err(Error::generic(format!(
137+
"Setting delta property '{}' is not supported during CREATE TABLE",
138+
key
139+
)));
140+
}
141+
// Non-delta properties (user/application properties) are always allowed
142+
}
143+
Ok(())
144+
}
145+
81146
/// Creates a builder for creating a new Delta table.
82147
///
83148
/// This function returns a [`CreateTableTransactionBuilder`] that can be configured with table
@@ -150,12 +215,55 @@ impl CreateTableTransactionBuilder {
150215
}
151216
}
152217

218+
/// Sets table properties for the new Delta table.
219+
///
220+
/// Custom application properties (those not starting with `delta.`) are always allowed.
221+
/// Delta properties (`delta.*`) are validated against an allow list during [`build()`].
222+
/// Feature flags (`delta.feature.*`) are not supported during CREATE TABLE.
223+
///
224+
/// This method can be called multiple times. If a property key already exists from a
225+
/// previous call, the new value will overwrite the old one.
226+
///
227+
/// # Arguments
228+
///
229+
/// * `properties` - A map of table property names to their values
230+
///
231+
/// # Example
232+
///
233+
/// ```rust,no_run
234+
/// # use delta_kernel::transaction::create_table::create_table;
235+
/// # use delta_kernel::schema::{StructType, DataType, StructField};
236+
/// # use std::sync::Arc;
237+
/// # fn example() -> delta_kernel::DeltaResult<()> {
238+
/// # let schema = Arc::new(StructType::try_new(vec![StructField::new("id", DataType::INTEGER, false)])?);
239+
/// let builder = create_table("/path/to/table", schema, "MyApp/1.0")
240+
/// .with_table_properties([
241+
/// ("myapp.version", "1.0"),
242+
/// ("myapp.author", "test"),
243+
/// ]);
244+
/// # Ok(())
245+
/// # }
246+
/// ```
247+
///
248+
/// [`build()`]: CreateTableTransactionBuilder::build
249+
pub fn with_table_properties<I, K, V>(mut self, properties: I) -> Self
250+
where
251+
I: IntoIterator<Item = (K, V)>,
252+
K: Into<String>,
253+
V: Into<String>,
254+
{
255+
self.table_properties
256+
.extend(properties.into_iter().map(|(k, v)| (k.into(), v.into())));
257+
self
258+
}
259+
153260
/// Builds a [`Transaction`] that can be committed to create the table.
154261
///
155262
/// This method performs validation:
156263
/// - Checks that the table path is valid
157264
/// - Verifies the table doesn't already exist
158265
/// - Validates the schema is non-empty
266+
/// - Validates table properties against the allow list
159267
///
160268
/// # Arguments
161269
///
@@ -168,6 +276,7 @@ impl CreateTableTransactionBuilder {
168276
/// - The table path is invalid
169277
/// - A table already exists at the given path
170278
/// - The schema is empty
279+
/// - Unsupported delta properties or feature flags are specified
171280
pub fn build(
172281
self,
173282
engine: &dyn Engine,
@@ -181,6 +290,9 @@ impl CreateTableTransactionBuilder {
181290
return Err(Error::generic("Schema cannot be empty"));
182291
}
183292

293+
// Validate table properties against allow list
294+
validate_table_properties(&self.table_properties)?;
295+
184296
// Check if table already exists by looking for _delta_log directory
185297
let delta_log_url = table_url.join("_delta_log/")?;
186298
let storage = engine.storage_handler();
@@ -207,13 +319,6 @@ impl CreateTableTransactionBuilder {
207319
// Create pre-commit snapshot from protocol/metadata
208320
let log_root = table_url.join("_delta_log/")?;
209321
let log_segment = LogSegment::for_pre_commit(log_root);
210-
211-
// We validate that the table properties are empty. Supported
212-
// table properties for create table will be allowlisted in the future.
213-
assert!(
214-
metadata.configuration().is_empty(),
215-
"Base create table API does not support table properties"
216-
);
217322
let table_configuration =
218323
TableConfiguration::try_new(metadata, protocol, table_url, PRE_COMMIT_VERSION)?;
219324

@@ -231,6 +336,7 @@ impl CreateTableTransactionBuilder {
231336
mod tests {
232337
use super::*;
233338
use crate::schema::{DataType, StructField, StructType};
339+
use crate::utils::test_utils::assert_result_error_with_message;
234340
use std::sync::Arc;
235341

236342
fn test_schema() -> SchemaRef {
@@ -263,4 +369,79 @@ mod tests {
263369

264370
assert_eq!(builder.path, "/path/to/table/nested");
265371
}
372+
373+
#[test]
374+
fn test_with_table_properties() {
375+
let schema = test_schema();
376+
377+
let builder = CreateTableTransactionBuilder::new("/path/to/table", schema, "TestApp/1.0")
378+
.with_table_properties([("key1", "value1")]);
379+
380+
assert_eq!(
381+
builder.table_properties.get("key1"),
382+
Some(&"value1".to_string())
383+
);
384+
}
385+
386+
#[test]
387+
fn test_with_multiple_table_properties() {
388+
let schema = test_schema();
389+
390+
let builder = CreateTableTransactionBuilder::new("/path/to/table", schema, "TestApp/1.0")
391+
.with_table_properties([("key1", "value1")])
392+
.with_table_properties([("key2", "value2")]);
393+
394+
assert_eq!(
395+
builder.table_properties.get("key1"),
396+
Some(&"value1".to_string())
397+
);
398+
assert_eq!(
399+
builder.table_properties.get("key2"),
400+
Some(&"value2".to_string())
401+
);
402+
}
403+
404+
#[test]
405+
fn test_validate_supported_properties() {
406+
// Empty properties are allowed
407+
let properties = HashMap::new();
408+
assert!(validate_table_properties(&properties).is_ok());
409+
410+
// User/application properties are allowed
411+
let mut properties = HashMap::new();
412+
properties.insert("myapp.version".to_string(), "1.0".to_string());
413+
properties.insert("custom.setting".to_string(), "value".to_string());
414+
assert!(validate_table_properties(&properties).is_ok());
415+
}
416+
417+
#[test]
418+
fn test_validate_unsupported_properties() {
419+
// Delta properties not on allow list are rejected
420+
let mut properties = HashMap::new();
421+
properties.insert("delta.enableChangeDataFeed".to_string(), "true".to_string());
422+
assert_result_error_with_message(
423+
validate_table_properties(&properties),
424+
"Setting delta property 'delta.enableChangeDataFeed' is not supported",
425+
);
426+
427+
// Feature override properties are rejected
428+
let mut properties = HashMap::new();
429+
properties.insert(
430+
"delta.feature.domainMetadata".to_string(),
431+
"supported".to_string(),
432+
);
433+
assert_result_error_with_message(
434+
validate_table_properties(&properties),
435+
"Setting feature override property 'delta.feature.domainMetadata' is not supported",
436+
);
437+
438+
// Mixed properties with unsupported delta property are rejected
439+
let mut properties = HashMap::new();
440+
properties.insert("myapp.version".to_string(), "1.0".to_string());
441+
properties.insert("delta.appendOnly".to_string(), "true".to_string());
442+
assert_result_error_with_message(
443+
validate_table_properties(&properties),
444+
"Setting delta property 'delta.appendOnly' is not supported",
445+
);
446+
}
266447
}

0 commit comments

Comments
 (0)