This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 4394dfe feat(index): support lumina and vindex global index drop
(#487)
4394dfe is described below
commit 4394dfe79618146a892afed945367a80b2032344
Author: Junrui Lee <[email protected]>
AuthorDate: Thu Jul 9 10:31:09 2026 +0800
feat(index): support lumina and vindex global index drop (#487)
---
crates/integrations/datafusion/src/procedures.rs | 18 +--
crates/integrations/datafusion/tests/procedures.rs | 36 +++++-
...rop_builder.rs => global_index_drop_builder.rs} | 143 ++++++++++++++++++---
crates/paimon/src/table/global_index_types.rs | 104 +++++++++++++++
crates/paimon/src/table/mod.rs | 11 +-
5 files changed, 284 insertions(+), 28 deletions(-)
diff --git a/crates/integrations/datafusion/src/procedures.rs
b/crates/integrations/datafusion/src/procedures.rs
index 76f3660..32b7989 100644
--- a/crates/integrations/datafusion/src/procedures.rs
+++ b/crates/integrations/datafusion/src/procedures.rs
@@ -26,8 +26,7 @@
//! - `CALL sys.create_global_index(table => '...', index_column => '...',
index_type => 'btree')`
//! - `CALL sys.create_global_index(table => '...', index_column => '...',
index_type => 'bitmap')`
//! - `CALL sys.create_global_index(table => '...', index_column => '...',
index_type => 'ivf-pq')`
-//! - `CALL sys.drop_global_index(table => '...', index_column => '...',
index_type => 'btree')`
-//! - `CALL sys.drop_global_index(table => '...', index_column => '...',
index_type => 'bitmap')`
+//! - `CALL sys.drop_global_index(table => '...', index_column => '...',
index_type => 'btree')` (also 'bitmap', 'lumina', or a vindex type such as
'ivf-pq')
//! - `CALL sys.create_lumina_index(table => '...', index_column => '...')`
use std::collections::HashMap;
@@ -44,7 +43,10 @@ use datafusion::sql::sqlparser::ast::{
};
use paimon::catalog::{Catalog, Identifier};
use paimon::spec::Snapshot;
-use paimon::table::{SnapshotManager, Table, TagManager};
+use paimon::table::{
+ normalize_global_index_type_for_drop, SnapshotManager, Table, TagManager,
+ SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP,
+};
use paimon::vindex::is_vindex_index_type;
use crate::error::to_datafusion_error;
@@ -587,23 +589,23 @@ async fn proc_drop_global_index(
.get("index_type")
.map(String::as_str)
.unwrap_or("btree");
- if !is_sorted_global_index_type(index_type) {
+ if normalize_global_index_type_for_drop(index_type).is_none() {
return Err(DataFusionError::NotImplemented(format!(
- "drop_global_index only supports index_type => 'btree' or
'bitmap', got '{index_type}'"
+ "unsupported global index type '{index_type}'; supported:
{SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP}"
)));
}
if args.contains_key("partitions") {
return Err(DataFusionError::NotImplemented(
- "drop_global_index partitions are not supported for btree
yet".to_string(),
+ "drop_global_index partitions are not supported yet".to_string(),
));
}
if args.contains_key("dry_run") {
return Err(DataFusionError::NotImplemented(
- "drop_global_index dry_run is not supported for btree
yet".to_string(),
+ "drop_global_index dry_run is not supported yet".to_string(),
));
}
- let mut builder = table.new_btree_global_index_drop_builder();
+ let mut builder = table.new_global_index_drop_builder();
builder.with_index_column(index_column);
builder.with_index_type(index_type);
builder.execute().await.map_err(to_datafusion_error)?;
diff --git a/crates/integrations/datafusion/tests/procedures.rs
b/crates/integrations/datafusion/tests/procedures.rs
index 15ac8d1..90d4d44 100644
--- a/crates/integrations/datafusion/tests/procedures.rs
+++ b/crates/integrations/datafusion/tests/procedures.rs
@@ -373,7 +373,41 @@ async fn
test_drop_global_index_rejects_unsupported_index_type() {
assert_sql_error(
&sql_context,
"CALL sys.drop_global_index(table =>
'test_db.global_index_drop_bad_type', index_column => 'id', index_type =>
'full-text')",
- "only supports index_type => 'btree' or 'bitmap'",
+ "unsupported global index type",
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn test_drop_global_index_accepts_lumina_type() {
+ let (_tmp, sql_context) =
setup_btree_global_index_table("lumina_drop_accepted").await;
+ exec(
+ &sql_context,
+ "INSERT INTO paimon.test_db.lumina_drop_accepted (id, name) VALUES (1,
'alice')",
+ )
+ .await;
+
+ // No lumina index exists; the call must be accepted and succeed (no-match
Ok),
+ // NOT rejected as an unsupported type.
+ exec(
+ &sql_context,
+ "CALL sys.drop_global_index(table => 'test_db.lumina_drop_accepted',
index_column => 'id', index_type => 'lumina')",
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn test_drop_global_index_accepts_vindex_type() {
+ let (_tmp, sql_context) =
setup_btree_global_index_table("vindex_drop_accepted").await;
+ exec(
+ &sql_context,
+ "INSERT INTO paimon.test_db.vindex_drop_accepted (id, name) VALUES (1,
'alice')",
+ )
+ .await;
+
+ exec(
+ &sql_context,
+ "CALL sys.drop_global_index(table => 'test_db.vindex_drop_accepted',
index_column => 'id', index_type => 'ivf-pq')",
)
.await;
}
diff --git a/crates/paimon/src/table/btree_global_index_drop_builder.rs
b/crates/paimon/src/table/global_index_drop_builder.rs
similarity index 71%
rename from crates/paimon/src/table/btree_global_index_drop_builder.rs
rename to crates/paimon/src/table/global_index_drop_builder.rs
index a1c497e..9d6f557 100644
--- a/crates/paimon/src/table/btree_global_index_drop_builder.rs
+++ b/crates/paimon/src/table/global_index_drop_builder.rs
@@ -15,19 +15,22 @@
// specific language governing permissions and limitations
// under the License.
-use super::global_index_types::{normalize_sorted_global_index_type,
BTREE_GLOBAL_INDEX_TYPE};
+use super::global_index_types::{
+ normalize_global_index_type_for_drop, BTREE_GLOBAL_INDEX_TYPE,
+ SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP,
+};
use crate::spec::{DataField, FileKind, IndexFileMeta, IndexManifest};
use crate::table::{CommitMessage, SnapshotManager, Table, TableCommit};
use crate::{Error, Result};
use std::collections::HashMap;
-pub struct BTreeGlobalIndexDropBuilder<'a> {
+pub struct GlobalIndexDropBuilder<'a> {
table: &'a Table,
index_column: Option<String>,
index_type: String,
}
-impl<'a> BTreeGlobalIndexDropBuilder<'a> {
+impl<'a> GlobalIndexDropBuilder<'a> {
pub(crate) fn new(table: &'a Table) -> Self {
Self {
table,
@@ -47,19 +50,20 @@ impl<'a> BTreeGlobalIndexDropBuilder<'a> {
}
pub async fn execute(&self) -> Result<usize> {
- let index_type =
normalize_sorted_global_index_type(&self.index_type).ok_or_else(|| {
- Error::Unsupported {
- message: format!(
- "Sorted global index drop only supports index_type =>
'btree' or 'bitmap', got '{}'",
- self.index_type
- ),
- }
- })?;
+ let index_type =
+
normalize_global_index_type_for_drop(&self.index_type).ok_or_else(|| {
+ Error::Unsupported {
+ message: format!(
+ "unsupported global index type '{}'; supported: {}",
+ self.index_type, SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP
+ ),
+ }
+ })?;
let index_column = self
.index_column
.as_deref()
.ok_or_else(|| Error::DataInvalid {
- message: "Sorted global index column is required".to_string(),
+ message: "Global index column is required".to_string(),
source: None,
})?;
let index_field = find_index_field(self.table, index_column)?;
@@ -84,7 +88,12 @@ impl<'a> BTreeGlobalIndexDropBuilder<'a> {
HashMap::new();
let mut dropped = 0;
for entry in index_entries {
- if entry.kind != FileKind::Add || entry.index_file.index_type !=
index_type {
+ if entry.kind != FileKind::Add {
+ continue;
+ }
+ if
normalize_global_index_type_for_drop(&entry.index_file.index_type)
+ != Some(index_type)
+ {
continue;
}
let Some(global_meta) =
entry.index_file.global_index_meta.as_ref() else {
@@ -310,7 +319,7 @@ mod tests {
.unwrap();
let dropped = table
- .new_btree_global_index_drop_builder()
+ .new_global_index_drop_builder()
.with_index_column("id")
.execute()
.await
@@ -353,7 +362,7 @@ mod tests {
.unwrap();
let dropped = table
- .new_btree_global_index_drop_builder()
+ .new_global_index_drop_builder()
.with_index_column("id")
.execute()
.await
@@ -364,4 +373,108 @@ mod tests {
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].index_file.file_name, "btree-name.index");
}
+
+ #[tokio::test]
+ async fn test_drop_lumina_matches_legacy_alias_entry() {
+ let table = test_table("memory:/test_drop_lumina_legacy");
+ setup_dirs(&table).await;
+
+ let mut message = CommitMessage::new(
+ BinaryRow::new(0).to_serialized_bytes(),
+ 0,
+ vec![data_file("data-0.parquet")],
+ );
+ // Stored under the LEGACY identifier; request uses the canonical name.
+ message.new_index_files = vec![
+ global_index_file("lumina-vector-ann", "lumina-id.index", 0, 0, 9),
+ global_index_file(BTREE_GLOBAL_INDEX_TYPE, "btree-id.index", 0, 0,
9),
+ ];
+ TableCommit::new(table.clone(), "test-user".to_string())
+ .commit(vec![message])
+ .await
+ .unwrap();
+
+ let dropped = table
+ .new_global_index_drop_builder()
+ .with_index_column("id")
+ .with_index_type("lumina")
+ .execute()
+ .await
+ .unwrap();
+ assert_eq!(dropped, 1);
+
+ let mut remaining = latest_index_entries(&table)
+ .await
+ .into_iter()
+ .map(|entry| entry.index_file.file_name)
+ .collect::<Vec<_>>();
+ remaining.sort();
+ assert_eq!(remaining, vec!["btree-id.index".to_string()]);
+ }
+
+ #[tokio::test]
+ async fn test_drop_vindex_preserves_other_types_case_insensitive() {
+ let table = test_table("memory:/test_drop_vindex_identity");
+ setup_dirs(&table).await;
+
+ let mut message = CommitMessage::new(
+ BinaryRow::new(0).to_serialized_bytes(),
+ 0,
+ vec![data_file("data-0.parquet")],
+ );
+ message.new_index_files = vec![
+ global_index_file("ivf-flat", "ivf-flat-id.index", 0, 0, 9),
+ global_index_file("ivf-pq", "ivf-pq-id.index", 0, 0, 9),
+ global_index_file("lumina", "lumina-id.index", 0, 0, 9),
+ global_index_file(BTREE_GLOBAL_INDEX_TYPE, "btree-id.index", 0, 0,
9),
+ ];
+ TableCommit::new(table.clone(), "test-user".to_string())
+ .commit(vec![message])
+ .await
+ .unwrap();
+
+ // Uppercase request must canonicalize and drop ONLY ivf-flat.
+ let dropped = table
+ .new_global_index_drop_builder()
+ .with_index_column("id")
+ .with_index_type("IVF-FLAT")
+ .execute()
+ .await
+ .unwrap();
+ assert_eq!(dropped, 1);
+
+ let mut remaining = latest_index_entries(&table)
+ .await
+ .into_iter()
+ .map(|entry| entry.index_file.file_name)
+ .collect::<Vec<_>>();
+ remaining.sort();
+ assert_eq!(
+ remaining,
+ vec![
+ "btree-id.index".to_string(),
+ "ivf-pq-id.index".to_string(),
+ "lumina-id.index".to_string(),
+ ]
+ );
+ }
+
+ #[tokio::test]
+ async fn test_drop_unsupported_type_errors() {
+ let table = test_table("memory:/test_drop_unsupported");
+ setup_dirs(&table).await;
+
+ let err = table
+ .new_global_index_drop_builder()
+ .with_index_column("id")
+ .with_index_type("full-text")
+ .execute()
+ .await
+ .expect_err("unsupported type must error");
+ assert!(matches!(
+ err,
+ Error::Unsupported { message }
+ if message.contains("unsupported global index type") &&
message.contains("ivf-pq")
+ ));
+ }
}
diff --git a/crates/paimon/src/table/global_index_types.rs
b/crates/paimon/src/table/global_index_types.rs
index bfa4430..ffd91a8 100644
--- a/crates/paimon/src/table/global_index_types.rs
+++ b/crates/paimon/src/table/global_index_types.rs
@@ -15,6 +15,12 @@
// specific language governing permissions and limitations
// under the License.
+use crate::lumina::{is_lumina_index_type, LUMINA_IDENTIFIER};
+use crate::vindex::{
+ is_vindex_index_type, IVF_FLAT_IDENTIFIER, IVF_HNSW_FLAT_IDENTIFIER,
IVF_HNSW_SQ_IDENTIFIER,
+ IVF_PQ_IDENTIFIER,
+};
+
pub(crate) const BTREE_GLOBAL_INDEX_TYPE: &str = "btree";
pub(crate) const BITMAP_GLOBAL_INDEX_TYPE: &str = "bitmap";
@@ -27,3 +33,101 @@ pub(crate) fn
normalize_sorted_global_index_type(index_type: &str) -> Option<&'s
None
}
}
+
+/// Human-readable list of the global index types the Rust drop path accepts.
+/// Used verbatim in the unsupported-type error of both the builder and the
+/// DataFusion procedure so the two messages stay in sync.
+pub const SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP: &str =
+ "btree, bitmap, lumina, lumina-vector-ann, ivf-flat, ivf-pq,
ivf-hnsw-flat, ivf-hnsw-sq";
+
+/// Canonicalize any supported global index type to a stable `&'static str`, or
+/// `None` if unsupported. Case-insensitive. Order: sorted -> lumina -> vindex.
+///
+/// Both lumina aliases (`lumina`, `lumina-vector-ann`) canonicalize to
+/// `"lumina"`; each vindex type keeps its own identity so dropping one vindex
+/// type never matches another on the same column. Callers should compare the
+/// canonical form of BOTH the request and each stored entry's `index_type`.
+pub fn normalize_global_index_type_for_drop(index_type: &str) ->
Option<&'static str> {
+ if let Some(sorted) = normalize_sorted_global_index_type(index_type) {
+ return Some(sorted);
+ }
+ // is_lumina_index_type / is_vindex_index_type match case-sensitively, so
+ // lowercase first (normalize_sorted_global_index_type is already
case-insensitive).
+ let lowered = index_type.to_ascii_lowercase();
+ if is_lumina_index_type(&lowered) {
+ return Some(LUMINA_IDENTIFIER);
+ }
+ if is_vindex_index_type(&lowered) {
+ return canonical_vindex_identifier(&lowered);
+ }
+ None
+}
+
+// Map a lowercased vindex identifier to its &'static constant (never return
the
+// borrowed `lowered` — wrong lifetime and not canonical).
+fn canonical_vindex_identifier(lowered: &str) -> Option<&'static str> {
+ match lowered {
+ IVF_FLAT_IDENTIFIER => Some(IVF_FLAT_IDENTIFIER),
+ IVF_PQ_IDENTIFIER => Some(IVF_PQ_IDENTIFIER),
+ IVF_HNSW_FLAT_IDENTIFIER => Some(IVF_HNSW_FLAT_IDENTIFIER),
+ IVF_HNSW_SQ_IDENTIFIER => Some(IVF_HNSW_SQ_IDENTIFIER),
+ _ => None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn sorted_types_canonicalize_case_insensitively() {
+ assert_eq!(normalize_global_index_type_for_drop("BTREE"),
Some("btree"));
+ assert_eq!(
+ normalize_global_index_type_for_drop("Bitmap"),
+ Some("bitmap")
+ );
+ }
+
+ #[test]
+ fn lumina_aliases_canonicalize_to_lumina() {
+ assert_eq!(
+ normalize_global_index_type_for_drop("lumina"),
+ Some("lumina")
+ );
+ assert_eq!(
+ normalize_global_index_type_for_drop("lumina-vector-ann"),
+ Some("lumina")
+ );
+ assert_eq!(
+ normalize_global_index_type_for_drop("LUMINA"),
+ Some("lumina")
+ );
+ }
+
+ #[test]
+ fn vindex_types_keep_distinct_identity() {
+ assert_eq!(
+ normalize_global_index_type_for_drop("ivf-flat"),
+ Some("ivf-flat")
+ );
+ assert_eq!(
+ normalize_global_index_type_for_drop("IVF-PQ"),
+ Some("ivf-pq")
+ );
+ assert_eq!(
+ normalize_global_index_type_for_drop("ivf-hnsw-flat"),
+ Some("ivf-hnsw-flat")
+ );
+ assert_eq!(
+ normalize_global_index_type_for_drop("ivf-hnsw-sq"),
+ Some("ivf-hnsw-sq")
+ );
+ }
+
+ #[test]
+ fn unsupported_types_return_none() {
+ assert_eq!(normalize_global_index_type_for_drop("full-text"), None);
+ assert_eq!(normalize_global_index_type_for_drop("hash"), None);
+ assert_eq!(normalize_global_index_type_for_drop(""), None);
+ }
+}
diff --git a/crates/paimon/src/table/mod.rs b/crates/paimon/src/table/mod.rs
index 1865593..e28e6f4 100644
--- a/crates/paimon/src/table/mod.rs
+++ b/crates/paimon/src/table/mod.rs
@@ -23,7 +23,6 @@ mod bitmap_global_index_reader;
mod blob_resolver;
mod branch_manager;
mod btree_global_index_build_builder;
-mod btree_global_index_drop_builder;
mod bucket_assigner;
mod bucket_assigner_constant;
mod bucket_assigner_cross;
@@ -41,6 +40,7 @@ mod dedicated_format_file_writer;
#[cfg(feature = "fulltext")]
mod full_text_search_builder;
pub(crate) mod global_index_build_common;
+mod global_index_drop_builder;
pub(crate) mod global_index_scanner;
mod global_index_types;
mod hybrid_search_builder;
@@ -77,13 +77,16 @@ use crate::Result;
use arrow_array::RecordBatch;
pub use branch_manager::BranchManager;
pub use btree_global_index_build_builder::BTreeGlobalIndexBuildBuilder;
-pub use btree_global_index_drop_builder::BTreeGlobalIndexDropBuilder;
pub use commit_message::CommitMessage;
pub use cow_writer::{CopyOnWriteMergeWriter, FileInfo};
pub use data_evolution_writer::{DataEvolutionDeleteWriter,
DataEvolutionWriter};
#[cfg(feature = "fulltext")]
pub use full_text_search_builder::FullTextSearchBuilder;
use futures::stream::BoxStream;
+pub use global_index_drop_builder::GlobalIndexDropBuilder;
+pub use global_index_types::{
+ normalize_global_index_type_for_drop,
SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP,
+};
pub use hybrid_search_builder::{
HybridSearchBuilder, HybridSearchRanker, HybridSearchRoute,
HybridSearchRouteKind,
};
@@ -220,8 +223,8 @@ impl Table {
BTreeGlobalIndexBuildBuilder::new(self)
}
- pub fn new_btree_global_index_drop_builder(&self) ->
BTreeGlobalIndexDropBuilder<'_> {
- BTreeGlobalIndexDropBuilder::new(self)
+ pub fn new_global_index_drop_builder(&self) -> GlobalIndexDropBuilder<'_> {
+ GlobalIndexDropBuilder::new(self)
}
pub fn new_vindex_index_build_builder(&self, index_type: &str) ->
VindexIndexBuildBuilder<'_> {