feat: guard generated metadata on overwrite

This commit is contained in:
Xuanwo
2026-08-12 19:08:13 +08:00
parent 7a46da2e67
commit 9b46e7a448
5 changed files with 1098 additions and 7 deletions
@@ -35,3 +35,19 @@ pub fn reject_caller_authored_generated_column_schema(schema: &Schema) -> Result
}
Ok(())
}
/// Conditionally admit an input schema for append vs overwrite.
///
/// Overwrite is schema replacement and must reject reserved top-level field
/// metadata. Append is not schema replacement: caller field metadata is
/// discarded by cast-to-table-schema, so reserved input keys remain accepted.
pub fn reject_caller_authored_generated_column_schema_on_overwrite(
schema: &Schema,
is_overwrite: bool,
) -> Result<()> {
if is_overwrite {
reject_caller_authored_generated_column_schema(schema)
} else {
Ok(())
}
}
+6
View File
@@ -12,6 +12,7 @@ use super::{ARROW_FILE_CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE};
use crate::blob::BlobFile;
use crate::data::scannable::{PeekedScannable, Scannable, estimate_write_partitions};
use crate::expr::expr_to_sql_string;
use crate::function::schema_admission::reject_caller_authored_generated_column_schema_on_overwrite;
use crate::function::{CreateGeneratedColumnJobSpec, GeneratedColumnBindingSnapshot};
use crate::index::Index;
use crate::index::IndexStatistics;
@@ -2320,6 +2321,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
})
}
async fn add(&self, mut add: AddDataBuilder) -> Result<AddResult> {
add.admit_input_schema()?;
self.check_mutable().await?;
let table_schema = self.schema().await?;
@@ -3188,6 +3190,10 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
write_params: lance::dataset::WriteParams,
) -> Result<Arc<dyn ExecutionPlan>> {
let overwrite = matches!(write_params.mode, lance::dataset::WriteMode::Overwrite);
reject_caller_authored_generated_column_schema_on_overwrite(
input.schema().as_ref(),
overwrite,
)?;
Ok(Arc::new(insert::RemoteWriteExec::new(
self.name.clone(),
self.identifier.clone(),
+9 -1
View File
@@ -54,6 +54,7 @@ use crate::database::listing::LANCE_FILE_EXTENSION;
use crate::database::read_freshness::TableFreshness;
use crate::embeddings::{EmbeddingDefinition, EmbeddingRegistry, MemoryRegistry};
use crate::error::{Error, Result};
use crate::function::schema_admission::reject_caller_authored_generated_column_schema_on_overwrite;
use crate::function::{
CreateGeneratedColumnJobSpec, GeneratedColumnBindingSnapshot, GeneratedColumnStatus,
};
@@ -95,6 +96,8 @@ mod delete_generated_column_invalidation_contract;
#[cfg(test)]
mod merge_insert_generated_column_reject_contract;
#[cfg(test)]
mod overwrite_generated_column_schema_admission_contract;
#[cfg(test)]
mod schema_metadata_updates_dependency_contract;
#[cfg(test)]
mod update_field_metadata_generated_column_guard_contract;
@@ -3234,6 +3237,7 @@ impl BaseTable for NativeTable {
}
async fn add(&self, mut add: AddDataBuilder) -> Result<AddResult> {
add.admit_input_schema()?;
self.dataset.ensure_mutable()?;
let ds_wrapper = self.dataset.clone();
// One exact dataset supplies table definition, schema, binding snapshot,
@@ -3796,9 +3800,13 @@ impl BaseTable for NativeTable {
input: Arc<dyn datafusion_physical_plan::ExecutionPlan>,
write_params: WriteParams,
) -> Result<Arc<dyn datafusion_physical_plan::ExecutionPlan>> {
let is_overwrite = matches!(write_params.mode, WriteMode::Overwrite);
reject_caller_authored_generated_column_schema_on_overwrite(
input.schema().as_ref(),
is_overwrite,
)?;
// One exact dataset supplies planning basis and the InsertExec basis.
let dataset = self.dataset.get().await?;
let is_overwrite = matches!(write_params.mode, WriteMode::Overwrite);
// Reject generated-table overwrite before returning an execution plan.
let schema_metadata_updates =
generated_column_invalidation::plan_native_append_generated_column_invalidation(
+23 -6
View File
@@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
use crate::data::scannable::Scannable;
use crate::data::scannable::scannable_with_embeddings;
use crate::embeddings::EmbeddingRegistry;
use crate::function::schema_admission::reject_caller_authored_generated_column_schema_on_overwrite;
use crate::table::datafusion::cast::cast_to_table_schema;
use crate::table::datafusion::reject_nan::reject_nan_vectors;
use crate::table::datafusion::scannable_exec::ScannableExec;
@@ -151,6 +152,27 @@ impl AddDataBuilder {
self.parent.clone().add(self).await
}
/// Effective overwrite for schema-replacement admission and planning.
///
/// True when either `WriteOptions.lance_write_params.mode` is
/// [`WriteMode::Overwrite`] or [`AddDataMode::Overwrite`] is set.
pub(crate) fn is_effective_overwrite(&self) -> bool {
self.write_options
.lance_write_params
.as_ref()
.is_some_and(|p| matches!(p.mode, WriteMode::Overwrite))
|| matches!(self.mode, AddDataMode::Overwrite)
}
/// Borrowed preflight: reject reserved generated-column metadata on
/// effective overwrite before source scan or table/network work.
pub(crate) fn admit_input_schema(&self) -> Result<()> {
reject_caller_authored_generated_column_schema_on_overwrite(
self.data.schema().as_ref(),
self.is_effective_overwrite(),
)
}
/// Build a DataFusion execution plan that applies embeddings, casts data to
/// the table schema, and optionally rejects NaN vectors.
///
@@ -161,12 +183,7 @@ impl AddDataBuilder {
table_schema: &Schema,
table_def: &TableDefinition,
) -> Result<PreprocessingOutput> {
let overwrite = self
.write_options
.lance_write_params
.as_ref()
.is_some_and(|p| matches!(p.mode, WriteMode::Overwrite))
|| matches!(self.mode, AddDataMode::Overwrite);
let overwrite = self.is_effective_overwrite();
if !overwrite {
validate_schema(&self.data.schema(), table_schema)?;
File diff suppressed because it is too large Load Diff