Skip to content

fix: preserve case-sensitive option keys in BaseLanceNamespaceSparkCatalog - #720

Open
Yohahaha wants to merge 2 commits into
lance-format:mainfrom
Yohahaha:worktree-fix-717
Open

fix: preserve case-sensitive option keys in BaseLanceNamespaceSparkCatalog#720
Yohahaha wants to merge 2 commits into
lance-format:mainfrom
Yohahaha:worktree-fix-717

Conversation

@Yohahaha

Copy link
Copy Markdown

CaseInsensitiveStringMap lowercases all keys when used directly, causing case-sensitive storage backend configs to fail. Use asCaseSensitiveMap() to preserve original key casing, consistent with other code paths.

Closes #717

….initialize()

CaseInsensitiveStringMap lowercases all keys when used directly, causing
case-sensitive storage backend configs to fail. Use asCaseSensitiveMap()
to preserve original key casing, consistent with other code paths.

Closes lance-format#717
@github-actions

Copy link
Copy Markdown
Contributor

ACTION NEEDED
Lance follows the Conventional Commits specification for release automation.

The PR title and description are used as the merge commit message. Please update your PR title and description to match the specification.

For details on the error please inspect the "PR Title Check" action.

@Yohahaha Yohahaha changed the title Fix case sensitivity in BaseLanceNamespaceSparkCatalog initialization fix: preserve case-sensitive option keys in BaseLanceNamespaceSparkCatalog Jul 24, 2026
@github-actions github-actions Bot added the bug Something isn't working label Jul 24, 2026
@Yohahaha

Copy link
Copy Markdown
Author

@LuciferYang @ivscheianu could you help review this PR? thank you!

@ivscheianu

Copy link
Copy Markdown
Contributor

@LuciferYang @ivscheianu could you help review this PR? thank you!

hey @Yohahaha, thanks for the fix! Looks good to me, opened a PR targeting this branch to also add a unit test, just to cover this case: Yohahaha#1

Feel free to merge it if you think it's fine.

cc: @hamersaw, probably good to have a look on this

@LuciferYang LuciferYang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one more copy of this pattern left at LanceBlobSourceContextRule.scala:58: new java.util.HashMap[String, String](relation.options) normalizes the keys the same way, then line 60 wraps them back into a CaseInsensitiveStringMap and writes them onto the relation. AbstractLanceFtsPredicateRule.scala:96 in the same package already uses rel.options.asCaseSensitiveMap(), so the two differ by that one call. The blast radius is smaller than the catalog case: it needs a blob target, a relation carrying options, and a user key with uppercase in it, and the V2Writes.mergeOptions assert only exists on Spark 4.x. Given one-PR-one-concern, a separate issue seems cleaner. Want me to file one?


// Initialize the namespace with proper configuration
Map<String, String> namespaceOptions = new HashMap<>(options);
Map<String, String> namespaceOptions = new HashMap<>(options.asCaseSensitiveMap());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on board with the direction: namespaceOptions and storageOptions at line 208 should have been the same key shape all along. What I want to check is that this drops the normalization wholesale instead of keeping it for the keys that actually need case preserved. Two things. First, the backend matches keys exactly (properties.get("root") and k.strip_prefix("storage.") in dir.rs, strip_prefix("headers.") in rest.rs). So ROOT=/tmp now fails with Missing required property 'root', wrapped in Failed to construct namespace impl …, while Headers.x-api-key, credential_vendor.Enabled and Storage.region are dropped with no error at all. Case in the suffix alone is still fine: storage.Region works because both as_s3_options and opendal lowercase the key again. Second, this method still reads impl, parent and single_level_ns through options.get, which is case-insensitive, so you can end up with the catalog accepting a config that the backend then reports as missing. My preference is to keep the behavior this PR has and add two small things: a warn when a key differs from its canonical form only by case, scoped to the keys the connector owns (impl, parent, parent_delimiter, single_level_ns) plus the first-party prefixes, and a note in docs/src/config.md that keys are case-sensitive. I was going to suggest keeping the original keys and adding a lowercase copy, but I tested it and it does not work. opendal's Configurator::from_iter lowercases keys before handing them to the serde-derived config, and a duplicate field comes back as ConfigInvalid. On 0.57.0, the version this PR pins, passing both Region and region gives duplicate field `region` . OSS and TOS pass the user's keys straight into from_iter (options.clone() at oss.rs:68 and tos.rs:74), and S3/GCS/Azure do the same under use_opendal=true (aws.rs:122), so a doubled map turns keys that work today into hard failures, for example storage.Endpoint on S3 with use_opendal=true. COS, GooseFS and HuggingFace always go through opendal but build their own canonical config map and read exact keys, so they are unaffected. That route needs the duplicate-field problem solved first, or narrowing down to normalizing only the suffixes under the first-party prefixes (storage., credential_vendor., headers., header., tls.).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, keys in docs/src/config.md now have to be copied with their exact case, but the docs don't say so. storage.* at :70, root at :126 and headers.* at :241 each show one spelling only. When it goes wrong the error is Missing required property 'root', and someone looking at their own ROOT won't connect that to casing. A sentence in the docs plus the warn scoped to connector-owned keys should cover it. Worth a look at pushDownFilters and topN_push_down in select.md:245-246 while you're there: they're the two documented keys whose canonical form isn't all lowercase, they go through exact containsKey, and getting the case wrong just silently disables them. Both were already case-sensitive before this change, so that part is a pre-existing trap rather than something this PR introduces.

@Yohahaha Yohahaha left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @LuciferYang! Created #737 to track the same bug in LanceBlobSourceContextRule separately.

@hamersaw

hamersaw commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

@Yohahaha is there any specific use-case that is failing right now? It looks like lance core is inconsistent on applying case sensitivity to options. Without a compelling reason this feels like something we should just leave to lance core to handle IMO.

@Yohahaha

Yohahaha commented Aug 6, 2026

Copy link
Copy Markdown
Author

@Yohahaha is there any specific use-case that is failing right now? It looks like lance core is inconsistent on applying case sensitivity to options. Without a compelling reason this feels like something we should just leave to lance core to handle IMO.

our internal storage backend needs all the storage options must be case sensitive, but lance spark pass case in-sensitive options when call namespace#connect.
I have described the case in issue #717

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BaseLanceNamespaceSparkCatalog passes lowercased option keys to LanceNamespace.connect()

4 participants