use lance::dataset::{ColumnAlteration, NewColumnTransform};
use serde::{Deserialize, Serialize};
use super::NativeTable;
use crate::Result;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AddColumnsResult {
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AlterColumnsResult {
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DropColumnsResult {
#[serde(default)]
pub version: u64,
}
pub(crate) async fn execute_add_columns(
table: &NativeTable,
transforms: NewColumnTransform,
read_columns: Option<Vec<String>>,
) -> Result<AddColumnsResult> {
table.dataset.ensure_mutable()?;
let mut dataset = (*table.dataset.get().await?).clone();
dataset.add_columns(transforms, read_columns, None).await?;
let version = dataset.version().version;
table.dataset.update(dataset);
Ok(AddColumnsResult { version })
}
pub(crate) async fn execute_alter_columns(
table: &NativeTable,
alterations: &[ColumnAlteration],
) -> Result<AlterColumnsResult> {
table.dataset.ensure_mutable()?;
let mut dataset = (*table.dataset.get().await?).clone();
dataset.alter_columns(alterations).await?;
let version = dataset.version().version;
table.dataset.update(dataset);
Ok(AlterColumnsResult { version })
}
pub(crate) async fn execute_drop_columns(
table: &NativeTable,
columns: &[&str],
) -> Result<DropColumnsResult> {
table.dataset.ensure_mutable()?;
let mut dataset = (*table.dataset.get().await?).clone();
dataset.drop_columns(columns).await?;
let version = dataset.version().version;
table.dataset.update(dataset);
Ok(DropColumnsResult { version })
}
#[cfg(test)]
mod tests {
use arrow_array::{Int32Array, StringArray, record_batch};
use arrow_schema::DataType;
use futures::TryStreamExt;
use lance::dataset::ColumnAlteration;
use crate::connect;
use crate::query::{ExecutableQuery, QueryBase, Select};
use crate::table::NewColumnTransform;
#[tokio::test]
async fn test_add_columns_with_sql_expression() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("id", Int32, [1, 2, 3, 4, 5])).unwrap();
let table = conn
.create_table("test_add_columns", batch)
.execute()
.await
.unwrap();
let initial_version = table.version().await.unwrap();
let result = table
.add_columns(
NewColumnTransform::SqlExpressions(vec![("doubled".into(), "id * 2".into())]),
None,
)
.await
.unwrap();
assert!(result.version > initial_version);
let batches = table
.query()
.select(Select::columns(&["id", "doubled"]))
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
let batch = &batches[0];
let ids: Vec<i32> = batch
.column(0)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.iter()
.map(|v| v.unwrap())
.collect();
let doubled: Vec<i32> = batch
.column(1)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.iter()
.map(|v| v.unwrap())
.collect();
for (id, d) in ids.iter().zip(doubled.iter()) {
assert_eq!(*d, id * 2);
}
}
#[tokio::test]
async fn test_add_multiple_columns() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("x", Int32, [10, 20, 30])).unwrap();
let table = conn
.create_table("test_add_multi_columns", batch)
.execute()
.await
.unwrap();
table
.add_columns(
NewColumnTransform::SqlExpressions(vec![
("y".into(), "x + 1".into()),
("z".into(), "x * x".into()),
]),
None,
)
.await
.unwrap();
let schema = table.schema().await.unwrap();
assert_eq!(schema.fields().len(), 3);
assert!(schema.field_with_name("x").is_ok());
assert!(schema.field_with_name("y").is_ok());
assert!(schema.field_with_name("z").is_ok());
}
#[tokio::test]
async fn test_add_column_with_constant_expression() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("id", Int32, [1, 2, 3])).unwrap();
let table = conn
.create_table("test_add_const_column", batch)
.execute()
.await
.unwrap();
table
.add_columns(
NewColumnTransform::SqlExpressions(vec![("constant".into(), "42".into())]),
None,
)
.await
.unwrap();
let schema = table.schema().await.unwrap();
assert!(schema.field_with_name("constant").is_ok());
let batches = table
.query()
.select(Select::columns(&["constant"]))
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
let batch = &batches[0];
let values = batch["constant"]
.as_any()
.downcast_ref::<arrow_array::Int64Array>()
.unwrap()
.values();
assert!(values.iter().all(|&v| v == 42));
}
#[tokio::test]
async fn test_alter_column_rename() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("old_name", Int32, [1, 2, 3])).unwrap();
let table = conn
.create_table("test_alter_rename", batch)
.execute()
.await
.unwrap();
let initial_version = table.version().await.unwrap();
let result = table
.alter_columns(&[ColumnAlteration::new("old_name".into()).rename("new_name".into())])
.await
.unwrap();
assert!(result.version > initial_version);
let schema = table.schema().await.unwrap();
assert!(schema.field_with_name("old_name").is_err());
assert!(schema.field_with_name("new_name").is_ok());
}
#[tokio::test]
async fn test_alter_column_set_nullable() {
use arrow_array::RecordBatch;
use arrow_schema::{Field, Schema};
use std::sync::Arc;
let conn = connect("memory://").execute().await.unwrap();
let schema = Arc::new(Schema::new(vec![Field::new(
"value",
DataType::Int32,
false,
)]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
)
.unwrap();
let table = conn
.create_table("test_alter_nullable", batch)
.execute()
.await
.unwrap();
let schema = table.schema().await.unwrap();
assert!(!schema.field_with_name("value").unwrap().is_nullable());
table
.alter_columns(&[ColumnAlteration::new("value".into()).set_nullable(true)])
.await
.unwrap();
let schema = table.schema().await.unwrap();
assert!(schema.field_with_name("value").unwrap().is_nullable());
}
#[tokio::test]
async fn test_alter_column_cast_type() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("num", Int32, [1, 2, 3])).unwrap();
let table = conn
.create_table("test_cast_type", batch)
.execute()
.await
.unwrap();
table
.alter_columns(&[ColumnAlteration::new("num".into()).cast_to(DataType::Int64)])
.await
.unwrap();
let schema = table.schema().await.unwrap();
assert_eq!(
schema.field_with_name("num").unwrap().data_type(),
&DataType::Int64
);
let batches = table
.query()
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
let batch = &batches[0];
let values = batch["num"]
.as_any()
.downcast_ref::<arrow_array::Int64Array>()
.unwrap()
.values();
assert_eq!(values.as_ref(), &[1i64, 2, 3]);
}
#[tokio::test]
async fn test_alter_column_invalid_cast_fails() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("num", Int32, [1, 2, 3])).unwrap();
let table = conn
.create_table("test_invalid_cast", batch)
.execute()
.await
.unwrap();
let result = table
.alter_columns(&[ColumnAlteration::new("num".into()).cast_to(DataType::Float64)])
.await;
let err = result.unwrap_err();
assert!(
err.to_string().contains("cast"),
"Expected error message to contain 'cast', got: {}",
err
);
}
#[tokio::test]
async fn test_alter_multiple_columns() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("a", Int32, [1, 2, 3]), ("b", Int32, [4, 5, 6])).unwrap();
let table = conn
.create_table("test_alter_multi", batch)
.execute()
.await
.unwrap();
table
.alter_columns(&[
ColumnAlteration::new("a".into()).rename("alpha".into()),
ColumnAlteration::new("b".into()).set_nullable(true),
])
.await
.unwrap();
let schema = table.schema().await.unwrap();
assert!(schema.field_with_name("alpha").is_ok());
assert!(schema.field_with_name("a").is_err());
assert!(schema.field_with_name("b").unwrap().is_nullable());
}
#[tokio::test]
async fn test_drop_single_column() {
let conn = connect("memory://").execute().await.unwrap();
let batch =
record_batch!(("keep", Int32, [1, 2, 3]), ("remove", Int32, [4, 5, 6])).unwrap();
let table = conn
.create_table("test_drop_single", batch)
.execute()
.await
.unwrap();
let initial_version = table.version().await.unwrap();
let result = table.drop_columns(&["remove"]).await.unwrap();
assert!(result.version > initial_version);
let schema = table.schema().await.unwrap();
assert_eq!(schema.fields().len(), 1);
assert!(schema.field_with_name("keep").is_ok());
assert!(schema.field_with_name("remove").is_err());
}
#[tokio::test]
async fn test_drop_multiple_columns() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(
("a", Int32, [1, 2]),
("b", Int32, [3, 4]),
("c", Int32, [5, 6]),
("d", Int32, [7, 8])
)
.unwrap();
let table = conn
.create_table("test_drop_multi", batch)
.execute()
.await
.unwrap();
table.drop_columns(&["b", "d"]).await.unwrap();
let schema = table.schema().await.unwrap();
assert_eq!(schema.fields().len(), 2);
assert!(schema.field_with_name("a").is_ok());
assert!(schema.field_with_name("c").is_ok());
assert!(schema.field_with_name("b").is_err());
assert!(schema.field_with_name("d").is_err());
}
#[tokio::test]
async fn test_drop_column_preserves_data() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(
("id", Int32, [1, 2, 3]),
("name", Utf8, ["a", "b", "c"]),
("extra", Int32, [10, 20, 30])
)
.unwrap();
let table = conn
.create_table("test_drop_preserves", batch)
.execute()
.await
.unwrap();
table.drop_columns(&["extra"]).await.unwrap();
let batches = table
.query()
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
let batch = &batches[0];
assert_eq!(batch.num_columns(), 2);
assert_eq!(batch.num_rows(), 3);
let ids: Vec<i32> = batch
.column(0)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.iter()
.map(|v| v.unwrap())
.collect();
assert_eq!(ids, vec![1, 2, 3]);
let names: Vec<&str> = batch
.column(1)
.as_any()
.downcast_ref::<StringArray>()
.unwrap()
.iter()
.map(|v| v.unwrap())
.collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[tokio::test]
async fn test_drop_nonexistent_column_fails() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("existing", Int32, [1, 2, 3])).unwrap();
let table = conn
.create_table("test_drop_nonexistent", batch)
.execute()
.await
.unwrap();
let result = table.drop_columns(&["nonexistent"]).await;
let err = result.unwrap_err();
assert!(
err.to_string().contains("nonexistent"),
"Expected error message to contain column name 'nonexistent', got: {}",
err
);
}
#[tokio::test]
async fn test_alter_nonexistent_column_fails() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("existing", Int32, [1, 2, 3])).unwrap();
let table = conn
.create_table("test_alter_nonexistent", batch)
.execute()
.await
.unwrap();
let result = table
.alter_columns(&[ColumnAlteration::new("nonexistent".into()).rename("new".into())])
.await;
let err = result.unwrap_err();
assert!(
err.to_string().contains("nonexistent"),
"Expected error message to contain column name 'nonexistent', got: {}",
err
);
}
#[tokio::test]
async fn test_schema_operations_increment_version() {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(("a", Int32, [1, 2, 3]), ("b", Int32, [4, 5, 6])).unwrap();
let table = conn
.create_table("test_version_increment", batch)
.execute()
.await
.unwrap();
let v1 = table.version().await.unwrap();
let add_result = table
.add_columns(
NewColumnTransform::SqlExpressions(vec![("c".into(), "a + b".into())]),
None,
)
.await
.unwrap();
assert!(add_result.version > v1);
let v2 = table.version().await.unwrap();
assert_eq!(add_result.version, v2);
let alter_result = table
.alter_columns(&[ColumnAlteration::new("c".into()).rename("sum".into())])
.await
.unwrap();
assert!(alter_result.version > v2);
let v3 = table.version().await.unwrap();
assert_eq!(alter_result.version, v3);
let drop_result = table.drop_columns(&["b"]).await.unwrap();
assert!(drop_result.version > v3);
let v4 = table.version().await.unwrap();
assert_eq!(drop_result.version, v4);
}
}