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 9672107d fix(datafusion): normalize index_type in global index 
procedures (#715)
9672107d is described below

commit 9672107d58b7ffc2c1a057cd87b936279bd1a884
Author: jackylee <[email protected]>
AuthorDate: Sat Aug 15 16:14:41 2026 +0800

    fix(datafusion): normalize index_type in global index procedures (#715)
---
 crates/integrations/datafusion/src/procedures.rs   |  77 ++++++++++++--
 crates/integrations/datafusion/tests/procedures.rs | 115 +++++++++++++++++++++
 docs/src/sql.md                                    |  16 +--
 3 files changed, 191 insertions(+), 17 deletions(-)

diff --git a/crates/integrations/datafusion/src/procedures.rs 
b/crates/integrations/datafusion/src/procedures.rs
index 9d2d9196..672300ae 100644
--- a/crates/integrations/datafusion/src/procedures.rs
+++ b/crates/integrations/datafusion/src/procedures.rs
@@ -28,6 +28,9 @@
 //! - `CALL sys.create_global_index(table => '...', index_column => '...', 
index_type => 'ivf-pq')`
 //! - `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 => '...')`
+//!
+//! The `index_type` argument of the three global index procedures is
+//! case-insensitive and surrounding whitespace is ignored.
 
 use std::collections::HashMap;
 use std::sync::Arc;
@@ -42,6 +45,7 @@ use datafusion::sql::sqlparser::ast::{
     FunctionArguments, ObjectName, Value as SqlValue,
 };
 use paimon::catalog::{Catalog, Identifier};
+use paimon::lumina::LUMINA_IDENTIFIER;
 use paimon::spec::Snapshot;
 use paimon::table::{
     normalize_global_index_type_for_drop, SnapshotManager, Table, TagManager,
@@ -51,6 +55,10 @@ use paimon::vindex::is_vindex_index_type;
 
 use crate::error::to_datafusion_error;
 
+/// Default `index_type` for the global index procedures when the argument is
+/// omitted, matching Java's `CreateGlobalIndexProcedure`.
+const DEFAULT_GLOBAL_INDEX_TYPE: &str = "btree";
+
 /// Resolve a snapshot by id: try live snapshot file first, then fall back to 
tag metadata.
 async fn resolve_snapshot_by_id(
     sm: &SnapshotManager,
@@ -527,9 +535,12 @@ async fn proc_create_lumina_index(
     let index_column = require_arg(args, "index_column")?;
     let mut builder = table.new_lumina_index_build_builder();
     builder.with_index_column(index_column);
-    if let Some(index_type) = args.get("index_type") {
-        builder.with_index_type(index_type);
-    }
+    let index_type = normalize_index_type(
+        args.get("index_type")
+            .map(String::as_str)
+            .unwrap_or(LUMINA_IDENTIFIER),
+    );
+    builder.with_index_type(&index_type);
     if let Some(options) = args.get("options") {
         builder.with_options(parse_key_value_options(options)?);
     }
@@ -545,10 +556,12 @@ async fn proc_create_global_index(
 ) -> DFResult<DataFrame> {
     let table = get_table(catalog, catalog_name, args).await?;
     let index_column = require_arg(args, "index_column")?;
-    let index_type = args
+    let index_type_arg = args
         .get("index_type")
         .map(String::as_str)
-        .unwrap_or("btree");
+        .unwrap_or(DEFAULT_GLOBAL_INDEX_TYPE);
+    let index_type = normalize_index_type(index_type_arg);
+    let index_type = index_type.as_str();
     if is_sorted_global_index_type(index_type) {
         if args.contains_key("options") {
             return Err(DataFusionError::NotImplemented(
@@ -569,9 +582,10 @@ async fn proc_create_global_index(
         }
         builder.execute().await.map_err(to_datafusion_error)?;
     } else {
+        // Echo the raw argument, not the normalized one, so a typo stays 
visible.
         return Err(DataFusionError::NotImplemented(format!(
             "create_global_index only supports index_type => 'btree', 
'bitmap', or vindex types \
-             ('ivf-flat', 'ivf-pq'), got '{index_type}'"
+             ('ivf-flat', 'ivf-pq'), got '{index_type_arg}'"
         )));
     }
     ok_result(ctx)
@@ -585,13 +599,16 @@ async fn proc_drop_global_index(
 ) -> DFResult<DataFrame> {
     let table = get_table(catalog, catalog_name, args).await?;
     let index_column = require_arg(args, "index_column")?;
-    let index_type = args
+    let index_type_arg = args
         .get("index_type")
         .map(String::as_str)
-        .unwrap_or("btree");
+        .unwrap_or(DEFAULT_GLOBAL_INDEX_TYPE);
+    let index_type = normalize_index_type(index_type_arg);
+    let index_type = index_type.as_str();
     if normalize_global_index_type_for_drop(index_type).is_none() {
+        // Echo the raw argument, not the normalized one, so a typo stays 
visible.
         return Err(DataFusionError::NotImplemented(format!(
-            "unsupported global index type '{index_type}'; supported: 
{SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP}"
+            "unsupported global index type '{index_type_arg}'; supported: 
{SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP}"
         )));
     }
     if args.contains_key("partitions") {
@@ -612,8 +629,19 @@ async fn proc_drop_global_index(
     ok_result(ctx)
 }
 
+/// Precondition: `index_type` is already canonical (see 
`normalize_index_type`).
 fn is_sorted_global_index_type(index_type: &str) -> bool {
-    index_type.eq_ignore_ascii_case("btree") || 
index_type.eq_ignore_ascii_case("bitmap")
+    index_type == "btree" || index_type == "bitmap"
+}
+
+/// Canonicalize a procedure's `index_type` argument: trim, then lowercase.
+/// Mirrors `indexType.toLowerCase(Locale.ROOT).trim()` in Java's Flink and 
Spark
+/// `CreateGlobalIndexProcedure` / `DropGlobalIndexProcedure`. Normalizing at 
this
+/// boundary keeps the core builders' exact matching intact -- they are the 
analog
+/// of Java's `GlobalIndexer`, which likewise receives an already-canonical 
value
+/// and persists it into index metadata.
+fn normalize_index_type(index_type: &str) -> String {
+    index_type.trim().to_ascii_lowercase()
 }
 
 fn parse_key_value_options(options: &str) -> DFResult<HashMap<String, String>> 
{
@@ -768,4 +796,33 @@ mod tests {
         let result = earlier_or_equal_from_all(&sm, &tm, 1500).await.unwrap();
         assert_eq!(result.unwrap().id(), 1);
     }
+
+    #[test]
+    fn test_normalize_index_type() {
+        // Casing and surrounding whitespace are both absorbed, matching Java's
+        // `indexType.toLowerCase(Locale.ROOT).trim()`.
+        assert_eq!(normalize_index_type("BTREE"), "btree");
+        assert_eq!(normalize_index_type(" btree "), "btree");
+        assert_eq!(normalize_index_type(" BitMap\t"), "bitmap");
+        assert_eq!(normalize_index_type("IVF-FLAT"), "ivf-flat");
+        assert_eq!(normalize_index_type("Ivf-Pq"), "ivf-pq");
+        assert_eq!(
+            normalize_index_type("Lumina-Vector-Ann"),
+            "lumina-vector-ann"
+        );
+        // Already canonical values are returned unchanged, and an unknown type
+        // is normalized but not rewritten -- the caller still rejects it.
+        assert_eq!(normalize_index_type("ivf-flat"), "ivf-flat");
+        assert_eq!(normalize_index_type(" Full-Text "), "full-text");
+    }
+
+    #[test]
+    fn test_sorted_global_index_type_predicate() {
+        assert!(is_sorted_global_index_type("btree"));
+        assert!(is_sorted_global_index_type("bitmap"));
+        assert!(!is_sorted_global_index_type("ivf-flat"));
+        assert!(!is_sorted_global_index_type("lumina"));
+        // The predicate requires a canonical input; callers normalize first.
+        assert!(!is_sorted_global_index_type("BTREE"));
+    }
 }
diff --git a/crates/integrations/datafusion/tests/procedures.rs 
b/crates/integrations/datafusion/tests/procedures.rs
index be2ddcbe..2f0c2d1f 100644
--- a/crates/integrations/datafusion/tests/procedures.rs
+++ b/crates/integrations/datafusion/tests/procedures.rs
@@ -140,6 +140,38 @@ async fn setup_btree_global_index_table(
     (tmp, sql_context)
 }
 
+/// An `ARRAY<FLOAT>` table configured so the pure-Rust `ivf-flat` builder can
+/// train and commit an index without a native library.
+async fn setup_vindex_global_index_table(
+    table_name: &str,
+) -> (tempfile::TempDir, paimon_datafusion::SQLContext) {
+    let (tmp, sql_context) = setup_sql_context().await;
+    exec(
+        &sql_context,
+        &format!(
+            "CREATE TABLE paimon.test_db.{table_name} (id INT, embedding 
ARRAY<FLOAT>) WITH (\
+                'row-tracking.enabled' = 'true',\
+                'data-evolution.enabled' = 'true',\
+                'global-index.enabled' = 'true',\
+                'global-index.row-count-per-shard' = '100',\
+                'ivf-flat.dimension' = '2',\
+                'ivf-flat.nlist' = '1',\
+                'ivf-flat.distance.metric' = 'l2'\
+            )"
+        ),
+    )
+    .await;
+    exec(
+        &sql_context,
+        &format!(
+            "INSERT INTO paimon.test_db.{table_name} (id, embedding) VALUES \
+             (1, [1.0, 0.0]), (2, [0.0, 1.0]), (3, [1.0, 1.0])"
+        ),
+    )
+    .await;
+    (tmp, sql_context)
+}
+
 #[tokio::test]
 async fn test_create_global_index_requires_index_column() {
     let (_tmp, sql_context) = 
setup_btree_global_index_table("btree_missing_col").await;
@@ -184,6 +216,89 @@ async fn test_create_global_index_rejects_options() {
     .await;
 }
 
+/// `index_type` was matched case-insensitively for btree/bitmap but exactly 
for
+/// the vindex types, so `'IVF-FLAT'` was rejected as unsupported while 
`'BTREE'`
+/// worked. Build with an uppercase vindex type and assert the index lands with
+/// the canonical lowercase `index_type` in the manifest, which is what the 
read
+/// path matches on.
+#[tokio::test]
+async fn test_create_global_index_accepts_uppercase_vindex_type() {
+    let (_tmp, sql_context) = 
setup_vindex_global_index_table("vindex_upper").await;
+
+    exec(
+        &sql_context,
+        "CALL sys.create_global_index(table => 'test_db.vindex_upper', 
index_column => 'embedding', index_type => 'IVF-FLAT')",
+    )
+    .await;
+
+    let index_count = row_count(
+        &sql_context,
+        "SELECT * FROM paimon.test_db.`vindex_upper$table_indexes` \
+         WHERE index_type = 'ivf-flat' AND index_field_name = 'embedding'",
+    )
+    .await;
+    assert_eq!(index_count, 1);
+}
+
+/// Neither procedure trimmed the argument, so `' btree '` was reported as an
+/// unsupported type. Both sides now absorb surrounding whitespace, matching
+/// Java's `toLowerCase().trim()`.
+#[tokio::test]
+async fn test_global_index_procedures_ignore_surrounding_whitespace() {
+    let (_tmp, sql_context) = 
setup_btree_global_index_table("btree_trim").await;
+    exec(
+        &sql_context,
+        "INSERT INTO paimon.test_db.btree_trim (id, name) VALUES (1, 'alice'), 
(2, 'bob')",
+    )
+    .await;
+
+    exec(
+        &sql_context,
+        "CALL sys.create_global_index(table => 'test_db.btree_trim', 
index_column => 'id', index_type => ' btree ')",
+    )
+    .await;
+    let after_create = row_count(
+        &sql_context,
+        "SELECT * FROM paimon.test_db.`btree_trim$table_indexes` \
+         WHERE index_type = 'btree' AND index_field_name = 'id'",
+    )
+    .await;
+    assert_eq!(after_create, 1);
+
+    exec(
+        &sql_context,
+        "CALL sys.drop_global_index(table => 'test_db.btree_trim', 
index_column => 'id', index_type => ' BTREE ')",
+    )
+    .await;
+    let after_drop = row_count(
+        &sql_context,
+        "SELECT * FROM paimon.test_db.`btree_trim$table_indexes` \
+         WHERE index_type = 'btree' AND index_field_name = 'id'",
+    )
+    .await;
+    assert_eq!(after_drop, 0);
+}
+
+/// An unsupported type must be echoed back exactly as written, not lowercased,
+/// so a typo stays recognizable in the error message.
+#[tokio::test]
+async fn test_global_index_procedures_echo_raw_unsupported_type() {
+    let (_tmp, sql_context) = 
setup_btree_global_index_table("btree_echo").await;
+
+    assert_sql_error(
+        &sql_context,
+        "CALL sys.create_global_index(table => 'test_db.btree_echo', 
index_column => 'id', index_type => 'Full-Text')",
+        "got 'Full-Text'",
+    )
+    .await;
+    assert_sql_error(
+        &sql_context,
+        "CALL sys.drop_global_index(table => 'test_db.btree_echo', 
index_column => 'id', index_type => 'Full-Text')",
+        "unsupported global index type 'Full-Text'",
+    )
+    .await;
+}
+
 #[tokio::test]
 async fn test_create_global_index_builds_btree_and_filter_reads() {
     let (_tmp, sql_context) = 
setup_btree_global_index_table("btree_build").await;
diff --git a/docs/src/sql.md b/docs/src/sql.md
index f69635c0..eaf9758a 100644
--- a/docs/src/sql.md
+++ b/docs/src/sql.md
@@ -964,10 +964,10 @@ CALL sys.create_global_index(
 );
 ```
 
-`index_type` defaults to `btree`. BTree and bitmap global indexes support
-scalar columns and do not accept the `options` argument yet. Bitmap global
-indexes use the same on-disk file format as Java Paimon's
-`BitmapGlobalIndexFormat`.
+`index_type` defaults to `btree`. It is case-insensitive and surrounding
+whitespace is ignored. BTree and bitmap global indexes support scalar columns
+and do not accept the `options` argument yet. Bitmap global indexes use the 
same
+on-disk file format as Java Paimon's `BitmapGlobalIndexFormat`.
 
 The current global-index builders require a row-tracking data-evolution table
 with global indexes enabled. They do not support primary-key tables or tables
@@ -1052,7 +1052,8 @@ CALL sys.drop_global_index(
 
 `index_type` accepts every type the create procedures build: `btree`, `bitmap`,
 `lumina` (or `lumina-vector-ann`), and the vindex types `ivf-flat` and 
`ivf-pq`.
-It defaults to `btree`.
+It defaults to `btree`, is case-insensitive and surrounding whitespace is
+ignored.
 
 ### create_lumina_index
 
@@ -1062,8 +1063,9 @@ Build and commit a Lumina global vector index for a table 
column:
 CALL sys.create_lumina_index(table => 'paimon.my_db.my_table', index_column => 
'embedding');
 ```
 
-The optional `index_type` argument selects the Lumina index identifier. It 
defaults to
-`lumina`. Valid values are `lumina` and the legacy-compatible 
`lumina-vector-ann`.
+The optional `index_type` argument selects the Lumina index identifier. It
+defaults to `lumina`, is case-insensitive and surrounding whitespace is 
ignored.
+Valid values are `lumina` and the legacy-compatible `lumina-vector-ann`.
 
 ```sql
 CALL sys.create_lumina_index(

Reply via email to