> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lancedb.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schema and Data Evolution

> Learn how to manage table schemas in LanceDB, including adding, altering, and dropping columns.

Schema evolution enables non-breaking modifications to a database table's structure — such as adding columns, altering data types, or dropping fields — to adapt to evolving data requirements without service interruptions.
LanceDB supports ACID-compliant schema evolution through granular operations (add/alter/drop columns), allowing you to:

* Iterate Safely: Modify schemas in production with versioned datasets and backward compatibility
* Scale Seamlessly: Handle ML model iterations, regulatory changes, or feature additions
* Optimize Continuously: Remove unused fields or enforce new constraints without downtime

## Schema evolution operations

LanceDB supports four primary schema evolution operations:

1. **Adding new columns**: Extend your table with additional attributes
2. **Altering existing columns**: Change column names, data types, or nullability
3. **Updating field metadata**: Attach or change per-column Arrow metadata
4. **Dropping columns**: Remove unnecessary columns from your schema

> [!TIP]
>
> Schema evolution operations are applied immediately but do not typically require rewriting all data. However, data type changes may involve more substantial operations.

Each schema evolution operation commits a new table version and returns status metadata such as
the committed `version`. Run these operations from a mutable table handle; if you checked out an
older version for reads, call `checkout_latest` / `checkoutLatest` before modifying the schema.

## Add new columns

You can add new columns to a table with the [`add_columns`](https://lancedb.github.io/lancedb/python/python/#lancedb.table.Table.add_columns)
method in Python, [`addColumns`](https://lancedb.github.io/lancedb/js/classes/Table/#addcolumns) in TypeScript/JavaScript, or `add_columns` in Rust.
New columns are populated based on SQL expressions you provide.

### Set up the example table

First, let's create a sample table with product data to demonstrate schema evolution:

table\_name = "schema\_evolution\_add\_example"
if data is None:
&#x20;   data = \[
&#x20;       \{
&#x20;           "id": 1,
&#x20;           "name": "Laptop",
&#x20;           "price": 1200.00,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;       \{
&#x20;           "id": 2,
&#x20;           "name": "Smartphone",
&#x20;           "price": 800.00,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;       \{
&#x20;           "id": 3,
&#x20;           "name": "Headphones",
&#x20;           "price": 150.00,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;   ]
table = tmp\_db.create\_table(table\_name, data, mode="overwrite")


const schemaAddData = \[
&#x20; \{
&#x20;   id: 1,
&#x20;   name: "Laptop",
&#x20;   price: 1200.0,
&#x20;   vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20; },
&#x20; \{
&#x20;   id: 2,
&#x20;   name: "Smartphone",
&#x20;   price: 800.0,
&#x20;   vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20; },
&#x20; \{
&#x20;   id: 3,
&#x20;   name: "Headphones",
&#x20;   price: 150.0,
&#x20;   vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20; },
];
const schemaAddTable = await db.createTable(
&#x20; "schema\_evolution\_add\_example",
&#x20; schemaAddData,
&#x20; \{ mode: "overwrite" },
);


let schema\_add\_schema = Arc::new(Schema::new(vec!\[
&#x20;   Field::new("id", DataType::Int64, false),
&#x20;   Field::new("name", DataType::Utf8, false),
&#x20;   Field::new("price", DataType::Float64, false),
&#x20;   Field::new(
&#x20;       "vector",
&#x20;       DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 128),
&#x20;       false,
&#x20;   ),
]));
let schema\_add\_batch = RecordBatch::try\_new(
&#x20;   schema\_add\_schema.clone(),
&#x20;   vec!\[
&#x20;       Arc::new(Int64Array::from(vec!\[1, 2, 3])),
&#x20;       Arc::new(StringArray::from(vec!\["Laptop", "Smartphone", "Headphones"])),
&#x20;       Arc::new(Float64Array::from(vec!\[1200.0, 800.0, 150.0])),
&#x20;       Arc::new(
&#x20;           FixedSizeListArray::from\_iter\_primitive::\<Float32Type, \_, \_>(
&#x20;               vec!\[
&#x20;                   Some(vec!\[Some(0.1\_f32); 128]),
&#x20;                   Some(vec!\[Some(0.2\_f32); 128]),
&#x20;                   Some(vec!\[Some(0.3\_f32); 128]),
&#x20;               ],
&#x20;               128,
&#x20;           ),
&#x20;       ),
&#x20;   ],
)
.unwrap();
let schema\_add\_reader: Box\<dyn RecordBatchReader + Send> = Box::new(RecordBatchIterator::new(
&#x20;   vec!\[Ok(schema\_add\_batch)].into\_iter(),
&#x20;   schema\_add\_schema.clone(),
));
let schema\_add\_table = db
&#x20;   .create\_table("schema\_evolution\_add\_example", schema\_add\_reader)
&#x20;   .mode(CreateTableMode::Overwrite)
&#x20;   .execute()
&#x20;   .await
&#x20;   .unwrap();


### Add derived columns

You can add new columns that are derived from existing data using SQL expressions.
For feature engineering on large existing tables, group related derived features into
one `add_columns` operation instead of running many separate writes. This creates one
new table version for the schema change and computes the new columns from the existing
rows, which avoids growing the table's version history with many small updates.

\# Add a discounted price column (10% discount)
table.add\_columns(\{"discounted\_price": "cast((price \* 0.9) as float)"})


// Add a discounted price column (10% discount)
await schemaAddTable.addColumns(\[
&#x20; \{
&#x20;   name: "discounted\_price",
&#x20;   valueSql: "cast((price \* 0.9) as float)",
&#x20; },
]);


// Add a discounted price column (10% discount)
schema\_add\_table
&#x20;   .add\_columns(
&#x20;       NewColumnTransform::SqlExpressions(vec!\[(
&#x20;           "discounted\_price".to\_string(),
&#x20;           "cast((price \* 0.9) as float)".to\_string(),
&#x20;       )]),
&#x20;       None,
&#x20;   )
&#x20;   .await
&#x20;   .unwrap();


The same call can add multiple derived columns at once. For example, if you are
building several lightweight features from existing product fields, pass all of the
new column expressions together:

table.add\_columns(
&#x20;   \{
&#x20;       "price\_per\_id": "cast(price / id as float)",
&#x20;       "price\_log": "ln(price)",
&#x20;       "price\_score": "cast(price / (price + 100.0) as float)",
&#x20;   }
)


await schemaAddTable.addColumns(\[
&#x20; \{
&#x20;   name: "price\_per\_id",
&#x20;   valueSql: "cast(price / id as float)",
&#x20; },
&#x20; \{
&#x20;   name: "price\_log",
&#x20;   valueSql: "ln(price)",
&#x20; },
&#x20; \{
&#x20;   name: "price\_score",
&#x20;   valueSql: "cast(price / (price + 100.0) as float)",
&#x20; },
]);


schema\_add\_table
&#x20;   .add\_columns(
&#x20;       NewColumnTransform::SqlExpressions(vec!\[
&#x20;           (
&#x20;               "price\_per\_id".to\_string(),
&#x20;               "cast(price / id as float)".to\_string(),
&#x20;           ),
&#x20;           ("price\_log".to\_string(), "ln(price)".to\_string()),
&#x20;           (
&#x20;               "price\_score".to\_string(),
&#x20;               "cast(price / (price + 100.0) as float)".to\_string(),
&#x20;           ),
&#x20;       ]),
&#x20;       None,
&#x20;   )
&#x20;   .await
&#x20;   .unwrap();


> [!NOTE]
>
> LanceDB `add_columns` does not currently accept Python callables, batch UDFs, or
> PyArrow `RecordBatch` iterators for populating new columns. New column values must be
> defined with SQL expressions, or added as NULL columns from an Arrow field or schema.
> If your transformation cannot be expressed in SQL, compute the values outside
> `add_columns` before writing them back through another workflow.

### Add columns with default values

Add boolean columns with default values for status tracking:

\# Add a stock status column with default value
table.add\_columns(\{"in\_stock": "cast(true as boolean)"})


// Add a stock status column with default value
await schemaAddTable.addColumns(\[
&#x20; \{
&#x20;   name: "in\_stock",
&#x20;   valueSql: "cast(true as boolean)",
&#x20; },
]);


// Add a stock status column with default value
schema\_add\_table
&#x20;   .add\_columns(
&#x20;       NewColumnTransform::SqlExpressions(vec!\[(
&#x20;           "in\_stock".to\_string(),
&#x20;           "cast(true as boolean)".to\_string(),
&#x20;       )]),
&#x20;       None,
&#x20;   )
&#x20;   .await
&#x20;   .unwrap();


### Add nullable columns

Add timestamp columns that can contain NULL values:

\# Add a nullable timestamp column
table.add\_columns(\{"last\_ordered": "cast(NULL as timestamp)"})


// Add a nullable timestamp column
await schemaAddTable.addColumns(\[
&#x20; \{
&#x20;   name: "last\_ordered",
&#x20;   valueSql: "cast(NULL as timestamp)",
&#x20; },
]);


// Add a nullable timestamp column
schema\_add\_table
&#x20;   .add\_columns(
&#x20;       NewColumnTransform::SqlExpressions(vec!\[(
&#x20;           "last\_ordered".to\_string(),
&#x20;           "cast(NULL as timestamp)".to\_string(),
&#x20;       )]),
&#x20;       None,
&#x20;   )
&#x20;   .await
&#x20;   .unwrap();


> [!WARNING]
>
> When adding columns that should contain NULL values, be sure to cast the NULL to the appropriate type, e.g., `cast(NULL as timestamp)`.

## Alter existing columns

You can alter columns using the [`alter_columns`](https://lancedb.github.io/lancedb/python/python/#lancedb.table.Table.alter_columns)
method in Python, [`alterColumns`](https://lancedb.github.io/lancedb/js/classes/Table/#altercolumns) in TypeScript/JavaScript, or `alter_columns` in Rust. This allows you to:

* Rename a column
* Change a column's data type
* Modify nullability (whether a column can contain NULL values)

### Set up the example table

Create a table with a custom schema to demonstrate column alterations:

table\_name = "schema\_evolution\_alter\_example"
if data is None:
&#x20;   data = \[
&#x20;       \{
&#x20;           "id": 1,
&#x20;           "name": "Laptop",
&#x20;           "price": 1200,
&#x20;           "discount\_price": 1080.0,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;       \{
&#x20;           "id": 2,
&#x20;           "name": "Smartphone",
&#x20;           "price": 800,
&#x20;           "discount\_price": 720.0,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;   ]
schema = pa.schema(
&#x20;   \{
&#x20;       "id": pa.int64(),
&#x20;       "name": pa.string(),
&#x20;       "price": pa.int32(),
&#x20;       "discount\_price": pa.float64(),
&#x20;       "vector": pa.list\_(pa.float32(), 128),
&#x20;   }
)
table = tmp\_db.create\_table(table\_name, data, schema=schema, mode="overwrite")


const schemaAlter = new arrow\.Schema(\[
&#x20; new arrow\.Field("id", new arrow\.Int64()),
&#x20; new arrow\.Field("name", new arrow\.Utf8()),
&#x20; new arrow\.Field("price", new arrow\.Int32()),
&#x20; new arrow\.Field("discount\_price", new arrow\.Float64()),
&#x20; new arrow\.Field(
&#x20;   "vector",
&#x20;   new arrow\.FixedSizeList(
&#x20;     128,
&#x20;     new arrow\.Field("item", new arrow\.Float32(), true),
&#x20;   ),
&#x20; ),
]);
const schemaAlterData = lancedb.makeArrowTable(
&#x20; \[
&#x20;   \{
&#x20;     id: 1,
&#x20;     name: "Laptop",
&#x20;     price: 1200,
&#x20;     discount\_price: 1080.0,
&#x20;     vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20;   },
&#x20;   \{
&#x20;     id: 2,
&#x20;     name: "Smartphone",
&#x20;     price: 800,
&#x20;     discount\_price: 720.0,
&#x20;     vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20;   },
&#x20; ],
&#x20; \{ schema: schemaAlter },
);
const schemaAlterTable = await db.createTable(
&#x20; "schema\_evolution\_alter\_example",
&#x20; schemaAlterData,
&#x20; \{ mode: "overwrite" },
);


let schema\_alter\_schema = Arc::new(Schema::new(vec!\[
&#x20;   Field::new("id", DataType::Int64, false),
&#x20;   Field::new("name", DataType::Utf8, false),
&#x20;   Field::new("price", DataType::Int32, false),
&#x20;   Field::new("discount\_price", DataType::Float64, false),
&#x20;   Field::new(
&#x20;       "vector",
&#x20;       DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 128),
&#x20;       false,
&#x20;   ),
]));
let schema\_alter\_batch = RecordBatch::try\_new(
&#x20;   schema\_alter\_schema.clone(),
&#x20;   vec!\[
&#x20;       Arc::new(Int64Array::from(vec!\[1, 2])),
&#x20;       Arc::new(StringArray::from(vec!\["Laptop", "Smartphone"])),
&#x20;       Arc::new(Int32Array::from(vec!\[1200, 800])),
&#x20;       Arc::new(Float64Array::from(vec!\[1080.0, 720.0])),
&#x20;       Arc::new(
&#x20;           FixedSizeListArray::from\_iter\_primitive::\<Float32Type, \_, \_>(
&#x20;               vec!\[Some(vec!\[Some(0.1\_f32); 128]), Some(vec!\[Some(0.2\_f32); 128])],
&#x20;               128,
&#x20;           ),
&#x20;       ),
&#x20;   ],
)
.unwrap();
let schema\_alter\_reader: Box\<dyn RecordBatchReader + Send> = Box::new(RecordBatchIterator::new(
&#x20;   vec!\[Ok(schema\_alter\_batch)].into\_iter(),
&#x20;   schema\_alter\_schema.clone(),
));
let schema\_alter\_table = db
&#x20;   .create\_table("schema\_evolution\_alter\_example", schema\_alter\_reader)
&#x20;   .mode(CreateTableMode::Overwrite)
&#x20;   .execute()
&#x20;   .await
&#x20;   .unwrap();


### Rename columns

Change column names to better reflect their purpose:

\# Rename discount\_price to sale\_price
table.alter\_columns(\{"path": "discount\_price", "rename": "sale\_price"})


// Rename discount\_price to sale\_price
await schemaAlterTable.alterColumns(\[
&#x20; \{ path: "discount\_price", rename: "sale\_price" },
]);


// Rename discount\_price to sale\_price
schema\_alter\_table
&#x20;   .alter\_columns(&\[ColumnAlteration::new("discount\_price".to\_string())
&#x20;       .rename("sale\_price".to\_string())])
&#x20;   .await
&#x20;   .unwrap();


### Change data types

Convert column data types for better performance or compatibility:

\# Change price from int32 to int64 for larger numbers
table.alter\_columns(\{"path": "price", "data\_type": pa.int64()})


// Change price from int32 to int64 for larger numbers
await schemaAlterTable.alterColumns(\[
&#x20; \{ path: "price", dataType: new arrow\.Int64() },
]);


// Change price from int32 to int64 for larger numbers
schema\_alter\_table
&#x20;   .alter\_columns(&\[ColumnAlteration::new("price".to\_string()).cast\_to(DataType::Int64)])
&#x20;   .await
&#x20;   .unwrap();


### Make columns nullable

You can alter columns to contain NULL values:

\# Make the name column nullable
table.alter\_columns(\{"path": "name", "nullable": True})


// Make the name column nullable
await schemaAlterTable.alterColumns(\[\{ path: "name", nullable: true }]);


// Make the name column nullable
schema\_alter\_table
&#x20;   .alter\_columns(&\[ColumnAlteration::new("name".to\_string()).set\_nullable(true)])
&#x20;   .await
&#x20;   .unwrap();


Changing a column to nullable affects future writes and merges too: missing values are accepted
only when the target column is nullable.

### Multiple changes at once

Apply several alterations in a single operation:

\# Rename, change type, and make nullable in one operation
table.alter\_columns(
&#x20;   \{
&#x20;       "path": "sale\_price",
&#x20;       "rename": "final\_price",
&#x20;       "data\_type": pa.float64(),
&#x20;       "nullable": True,
&#x20;   }
)


// Rename, change type, and make nullable in one operation
await schemaAlterTable.alterColumns(\[
&#x20; \{
&#x20;   path: "sale\_price",
&#x20;   rename: "final\_price",
&#x20;   dataType: new arrow\.Float64(),
&#x20;   nullable: true,
&#x20; },
]);


// Rename, change type, and make nullable in one operation
schema\_alter\_table
&#x20;   .alter\_columns(&\[ColumnAlteration::new("sale\_price".to\_string())
&#x20;       .rename("final\_price".to\_string())
&#x20;       .cast\_to(DataType::Float64)
&#x20;       .set\_nullable(true)])
&#x20;   .await
&#x20;   .unwrap();


### Expression-based type changes

For transformations that are not simple casts (for example, converting `"$100"` to an integer), use a SQL-expression column add, then drop and rename:

\# For custom transforms, create a new column from a SQL expression.
expression\_table = tmp\_db.create\_table(
&#x20;   "schema\_evolution\_expression\_example",
&#x20;   \[\{"id": 1, "price\_text": "\$100"}],
&#x20;   mode="overwrite",
)

expression\_table.add\_columns(
&#x20;   \{"price\_numeric": "cast(replace(price\_text, '\$', '') as int)"}
)
expression\_table.drop\_columns(\["price\_text"])
expression\_table.alter\_columns(\{"path": "price\_numeric", "rename": "price"})


// For custom transforms, create a new column from a SQL expression.
const expressionTable = await db.createTable(
&#x20; "schema\_evolution\_expression\_example",
&#x20; \[\{ id: 1, price\_text: "\$100" }],
&#x20; \{ mode: "overwrite" },
);

await expressionTable.addColumns(\[
&#x20; \{
&#x20;   name: "price\_numeric",
&#x20;   valueSql: "cast(replace(price\_text, '\$', '') as int)",
&#x20; },
]);
await expressionTable.dropColumns(\["price\_text"]);
await expressionTable.alterColumns(\[
&#x20; \{ path: "price\_numeric", rename: "price" },
]);


// For custom transforms, create a new column from a SQL expression.
let expression\_schema = Arc::new(Schema::new(vec!\[
&#x20;   Field::new("id", DataType::Int64, false),
&#x20;   Field::new("price\_text", DataType::Utf8, false),
]));
let expression\_batch = RecordBatch::try\_new(
&#x20;   expression\_schema.clone(),
&#x20;   vec!\[
&#x20;       Arc::new(Int64Array::from(vec!\[1])),
&#x20;       Arc::new(StringArray::from(vec!\["\$100"])),
&#x20;   ],
)
.unwrap();
let expression\_reader: Box\<dyn RecordBatchReader + Send> = Box::new(RecordBatchIterator::new(
&#x20;   vec!\[Ok(expression\_batch)].into\_iter(),
&#x20;   expression\_schema.clone(),
));
let expression\_table = db
&#x20;   .create\_table("schema\_evolution\_expression\_example", expression\_reader)
&#x20;   .mode(CreateTableMode::Overwrite)
&#x20;   .execute()
&#x20;   .await
&#x20;   .unwrap();

expression\_table
&#x20;   .add\_columns(
&#x20;       NewColumnTransform::SqlExpressions(vec!\[(
&#x20;           "price\_numeric".to\_string(),
&#x20;           "cast(replace(price\_text, '\$', '') as int)".to\_string(),
&#x20;       )]),
&#x20;       None,
&#x20;   )
&#x20;   .await
&#x20;   .unwrap();
expression\_table.drop\_columns(&\["price\_text"]).await.unwrap();
expression\_table
&#x20;   .alter\_columns(&\[ColumnAlteration::new("price\_numeric".to\_string())
&#x20;       .rename("price".to\_string())])
&#x20;   .await
&#x20;   .unwrap();


### Alter embedding types and dimensions

It's quite common to need to change an embedding column's schema, in case a new model becomes available with a different embedding dimension.

* In Python, the example shows an in-place type update when the cast is compatible.
* In TypeScript and Rust, the example shows a dimension change (`384 -> 1024`), which cannot be cast in-place.

For dimension changes, use this 3-step pattern: add a new column with the target type, drop the old column, then rename the new column to the original name.

vector\_dim = 768  # Your embedding dimension
table\_name = "vector\_alter\_example"
db = tmp\_db
data = \[
&#x20;   \{
&#x20;       "id": 1,
&#x20;       "embedding": np.random.random(vector\_dim).tolist(),
&#x20;   },
]
table = db.create\_table(table\_name, data, mode="overwrite")

table.alter\_columns(
&#x20;   dict(path="embedding", data\_type=pa.list\_(pa.float32(), vector\_dim))
)


const oldDim = 384;
const newDim = 1024;
const vectorSchema = new arrow\.Schema(\[
&#x20; new arrow\.Field("id", new arrow\.Int64()),
&#x20; new arrow\.Field(
&#x20;   "embedding",
&#x20;   new arrow\.FixedSizeList(
&#x20;     oldDim,
&#x20;     new arrow\.Field("item", new arrow\.Float16(), true),
&#x20;   ),
&#x20;   true,
&#x20; ),
]);
const vectorData = lancedb.makeArrowTable(
&#x20; \[\{ id: 1, embedding: Array.from(\{ length: oldDim }, () => Math.random()) }],
&#x20; \{ schema: vectorSchema },
);
const vectorTable = await db.createTable("vector\_alter\_example", vectorData, \{
&#x20; mode: "overwrite",
});

// Changing FixedSizeList dimensions (384 -> 1024) is not supported via alterColumns.
// Use addColumns + dropColumns + alterColumns(rename) to replace the column.
await vectorTable.addColumns(\[
&#x20; \{
&#x20;   name: "embedding\_v2",
&#x20;   valueSql: \`arrow\_cast(NULL, 'FixedSizeList(\$\{newDim}, Float16)')\`,
&#x20; },
]);
await vectorTable.dropColumns(\["embedding"]);
await vectorTable.alterColumns(\[\{ path: "embedding\_v2", rename: "embedding" }]);


let old\_dim = 384;
let new\_dim = 1024;
let vector\_schema = Arc::new(Schema::new(vec!\[
&#x20;   Field::new("id", DataType::Int64, false),
&#x20;   Field::new(
&#x20;       "embedding",
&#x20;       DataType::FixedSizeList(
&#x20;           Arc::new(Field::new("item", DataType::Float32, true)),
&#x20;           old\_dim,
&#x20;       ),
&#x20;       true,
&#x20;   ),
]));
let vector\_batch = RecordBatch::try\_new(
&#x20;   vector\_schema.clone(),
&#x20;   vec!\[
&#x20;       Arc::new(Int64Array::from(vec!\[1])),
&#x20;       Arc::new(
&#x20;           FixedSizeListArray::from\_iter\_primitive::\<Float32Type, \_, \_>(
&#x20;               vec!\[Some(vec!\[Some(0.1\_f32); old\_dim as usize])],
&#x20;               old\_dim,
&#x20;           ),
&#x20;       ),
&#x20;   ],
)
.unwrap();
let vector\_reader: Box\<dyn RecordBatchReader + Send> =
&#x20;   Box::new(RecordBatchIterator::new(vec!\[Ok(vector\_batch)].into\_iter(), vector\_schema.clone()));
let vector\_table = db
&#x20;   .create\_table("vector\_alter\_example", vector\_reader)
&#x20;   .mode(CreateTableMode::Overwrite)
&#x20;   .execute()
&#x20;   .await
&#x20;   .unwrap();

// Changing FixedSizeList dimensions (384 -> 1024) is not supported via alter\_columns.
// Use add\_columns + drop\_columns + alter\_columns(rename) to replace the column.
vector\_table
&#x20;   .add\_columns(
&#x20;       NewColumnTransform::SqlExpressions(vec!\[(
&#x20;           "embedding\_v2".to\_string(),
&#x20;           format!("arrow\_cast(NULL, 'FixedSizeList(\{}, Float32)')", new\_dim),
&#x20;       )]),
&#x20;       None,
&#x20;   )
&#x20;   .await
&#x20;   .unwrap();
vector\_table.drop\_columns(&\["embedding"]).await.unwrap();
vector\_table
&#x20;   .alter\_columns(&\[ColumnAlteration::new("embedding\_v2".to\_string())
&#x20;       .rename("embedding".to\_string())])
&#x20;   .await
&#x20;   .unwrap();


> [!WARNING]
>
> **`FixedSizeList` Dimension Changes in TypeScript and Rust**
>
> `alterColumns` / `alter_columns` can cast between compatible types, but changing `FixedSizeList` dimensions (for example `384 -> 1024`) is not a compatible cast.
> For such cases, use `addColumns` / `add_columns` (with `arrow_cast`), then `dropColumns` / `drop_columns`, then rename the replacement column.

> [!WARNING]
>
> Changing data types requires rewriting the column data and may be resource-intensive for large tables. Renaming columns or changing nullability is more efficient as it only updates metadata.

## Update field metadata

Each column in a LanceDB table can carry a small key/value map of Arrow field metadata — useful
for annotating columns with units, provenance, PII flags, embedding model versions, or any other
schema-level context your application needs.

Use [`update_field_metadata`](https://lancedb.github.io/lancedb/python/python/#lancedb.table.Table.update_field_metadata)
in Python, [`updateFieldMetadata`](https://lancedb.github.io/lancedb/js/classes/Table/#updatefieldmetadata)
in TypeScript/JavaScript, or `update_field_metadata` in Rust to add, change, or remove these
key/value pairs without rewriting the column data. Each call commits a new table version and returns
the new `version`.

Each update targets one field by **dot-path**: top-level columns are addressed by name (for
example `"embedding"`), and nested fields by their full path (for example `"address.zip"`). By
default, the keys you pass are **merged** into the field's existing metadata — keys you do not
mention are preserved, and passing `None` (Python) or `null` (TypeScript) deletes a key. Set
`replace: true` to swap the field's entire metadata map instead of merging.

\# Set two metadata keys on the \`category\` field.
res = table.update\_field\_metadata(
&#x20;   \{"path": "category", "metadata": \{"unit": "label", "pii": "false"}}
)
print(res.version)

\# Merge: add a new key, delete one with None, keep the rest.
table.update\_field\_metadata(
&#x20;   \{"path": "category", "metadata": \{"source": "import", "pii": None}}
)

\# Arrow stores field metadata as bytes.
assert table.schema.field("category").metadata == \{
&#x20;   b"unit": b"label",
&#x20;   b"source": b"import",
}


// Set two metadata keys on the \`category\` field.
const res = await fieldMetadataTable.updateFieldMetadata(\[
&#x20; \{ path: "category", metadata: \{ unit: "label", pii: "false" } },
]);
console.log(res.version);

// Merge: add a new key, delete one via null, keep the rest.
await fieldMetadataTable.updateFieldMetadata(\[
&#x20; \{ path: "category", metadata: \{ source: "import", pii: null } },
]);


// Set two metadata keys on the \`category\` field.
let res = field\_metadata\_table
&#x20;   .update\_field\_metadata(&\[FieldMetadataUpdate::new("category")
&#x20;       .set("unit", "label")
&#x20;       .set("pii", "false")])
&#x20;   .await
&#x20;   .unwrap();
println!("version: \{}", res.version);

// Merge: add a new key, delete one with \`.remove\`, keep the rest.
field\_metadata\_table
&#x20;   .update\_field\_metadata(&\[FieldMetadataUpdate::new("category")
&#x20;       .set("source", "import")
&#x20;       .remove("pii")])
&#x20;   .await
&#x20;   .unwrap();


To overwrite a field's metadata entirely instead of merging, set `replace` to `true`:

table.update\_field\_metadata(
&#x20;   \{
&#x20;       "path": "category",
&#x20;       "metadata": \{"owner": "search-team"},
&#x20;       "replace": True,
&#x20;   }
)


await fieldMetadataTable.updateFieldMetadata(\[
&#x20; \{
&#x20;   path: "category",
&#x20;   metadata: \{ owner: "search-team" },
&#x20;   replace: true,
&#x20; },
]);


field\_metadata\_table
&#x20;   .update\_field\_metadata(&\[FieldMetadataUpdate::new("category")
&#x20;       .set("owner", "search-team")
&#x20;       .replace()])
&#x20;   .await
&#x20;   .unwrap();


> [!TIP]
>
> You can pass multiple updates in a single call to change metadata on several fields at once —
> each call commits a single new table version.

### Canonical metadata keys

You can use any string as a metadata key. By convention, LanceDB treats keys prefixed with
`lancedb:` as canonical. LanceDB Enterprise displays these keys in the UI, and agents can rely
on finding this information in a consistent place. Use them when they fit your use case:

| Key | Purpose | Example |
| - | - | - |
| `lancedb:description` | Human-readable description of the field. | `"CLIP embedding of the product image"` |
| `lancedb:tag:<name>` | User-defined key/value tag, where the suffix names the tag category. | `"lancedb:tag:model": "clip"` |
| `lancedb:logical-column` | Groups related columns into one logical column. For example, `feature_v1` and `feature_v2` might belong to the same logical column. | `"lancedb:logical-column": "feature"` |
| `lancedb:status` | Life cycle state of the column: `production`, `candidate`, `deprecated`, or `archived`. | `"lancedb:status": "production"` |

> [!NOTE]
>
> These keys are conventions, not enforced constraints. LanceDB does not validate their values,
> but following the conventions keeps your tables consistent with Enterprise tooling.

## Drop columns

You can remove columns using the [`drop_columns`](https://lancedb.github.io/lancedb/python/python/#lancedb.table.Table.drop_columns)
method in Python, [`dropColumns`](https://lancedb.github.io/lancedb/js/classes/Table/#dropcolumns) in TypeScript/JavaScript, or `drop_columns` in Rust.

### Set Up the example table

Create a table with temporary columns that we'll remove:

if data is None:
&#x20;   data = \[
&#x20;       \{
&#x20;           "id": 1,
&#x20;           "name": "Laptop",
&#x20;           "price": 1200.00,
&#x20;           "temp\_col1": "X",
&#x20;           "temp\_col2": 100,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;       \{
&#x20;           "id": 2,
&#x20;           "name": "Smartphone",
&#x20;           "price": 800.00,
&#x20;           "temp\_col1": "Y",
&#x20;           "temp\_col2": 200,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;       \{
&#x20;           "id": 3,
&#x20;           "name": "Headphones",
&#x20;           "price": 150.00,
&#x20;           "temp\_col1": "Z",
&#x20;           "temp\_col2": 300,
&#x20;           "vector": np.random.random(128).tolist(),
&#x20;       },
&#x20;   ]
table = tmp\_db.create\_table("schema\_evolution\_drop\_example", data, mode="overwrite")


const schemaDropData = \[
&#x20; \{
&#x20;   id: 1,
&#x20;   name: "Laptop",
&#x20;   price: 1200.0,
&#x20;   temp\_col1: "X",
&#x20;   temp\_col2: 100,
&#x20;   vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20; },
&#x20; \{
&#x20;   id: 2,
&#x20;   name: "Smartphone",
&#x20;   price: 800.0,
&#x20;   temp\_col1: "Y",
&#x20;   temp\_col2: 200,
&#x20;   vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20; },
&#x20; \{
&#x20;   id: 3,
&#x20;   name: "Headphones",
&#x20;   price: 150.0,
&#x20;   temp\_col1: "Z",
&#x20;   temp\_col2: 300,
&#x20;   vector: Array.from(\{ length: 128 }, () => Math.random()),
&#x20; },
];
const schemaDropTable = await db.createTable(
&#x20; "schema\_evolution\_drop\_example",
&#x20; schemaDropData,
&#x20; \{ mode: "overwrite" },
);


let schema\_drop\_schema = Arc::new(Schema::new(vec!\[
&#x20;   Field::new("id", DataType::Int64, false),
&#x20;   Field::new("name", DataType::Utf8, false),
&#x20;   Field::new("price", DataType::Float64, false),
&#x20;   Field::new("temp\_col1", DataType::Utf8, false),
&#x20;   Field::new("temp\_col2", DataType::Int32, false),
&#x20;   Field::new(
&#x20;       "vector",
&#x20;       DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 128),
&#x20;       false,
&#x20;   ),
]));
let schema\_drop\_batch = RecordBatch::try\_new(
&#x20;   schema\_drop\_schema.clone(),
&#x20;   vec!\[
&#x20;       Arc::new(Int64Array::from(vec!\[1, 2, 3])),
&#x20;       Arc::new(StringArray::from(vec!\["Laptop", "Smartphone", "Headphones"])),
&#x20;       Arc::new(Float64Array::from(vec!\[1200.0, 800.0, 150.0])),
&#x20;       Arc::new(StringArray::from(vec!\["X", "Y", "Z"])),
&#x20;       Arc::new(Int32Array::from(vec!\[100, 200, 300])),
&#x20;       Arc::new(
&#x20;           FixedSizeListArray::from\_iter\_primitive::\<Float32Type, \_, \_>(
&#x20;               vec!\[
&#x20;                   Some(vec!\[Some(0.1\_f32); 128]),
&#x20;                   Some(vec!\[Some(0.2\_f32); 128]),
&#x20;                   Some(vec!\[Some(0.3\_f32); 128]),
&#x20;               ],
&#x20;               128,
&#x20;           ),
&#x20;       ),
&#x20;   ],
)
.unwrap();
let schema\_drop\_reader: Box\<dyn RecordBatchReader + Send> = Box::new(RecordBatchIterator::new(
&#x20;   vec!\[Ok(schema\_drop\_batch)].into\_iter(),
&#x20;   schema\_drop\_schema.clone(),
));
let schema\_drop\_table = db
&#x20;   .create\_table("schema\_evolution\_drop\_example", schema\_drop\_reader)
&#x20;   .mode(CreateTableMode::Overwrite)
&#x20;   .execute()
&#x20;   .await
&#x20;   .unwrap();


### Drop single columns

Remove individual columns that are no longer needed:

\# Remove the first temporary column
table.drop\_columns(\["temp\_col1"])


// Remove the first temporary column
await schemaDropTable.dropColumns(\["temp\_col1"]);


// Remove the first temporary column
schema\_drop\_table.drop\_columns(&\["temp\_col1"]).await.unwrap();


### Drop multiple columns

Remove several columns at once for efficiency:

\# Remove the second temporary column
table.drop\_columns(\["temp\_col2"])


// Remove the second temporary column
await schemaDropTable.dropColumns(\["temp\_col2"]);


// Remove the second temporary column
schema\_drop\_table.drop\_columns(&\["temp\_col2"]).await.unwrap();


> [!WARNING]
>
> Dropping columns cannot be undone. Make sure you have backups or are certain before removing columns.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.