Repository navigation
[Bug report] Lance REST rejects the spec's PascalCase ExistOk mode on CreateNamespace and CreateTable #13680
Description
Version
1.3.0, and main — CommonUtil.normalizeToken is unchanged on origin/main.
Describe what's wrong
The Lance REST server rejects the ExistOk mode value that the lance-namespace spec documents, on both CreateNamespace and CreateTable.
The spec declares these mode fields as "Case insensitive, supports both PascalCase and snake_case", with valid values Create / ExistOk / Overwrite. CommonUtil.normalizeToken only trims and upper-cases:
public static String normalizeToken(String value) {
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
}so ExistOk becomes EXISTOK, which does not match the EXIST_OK enum constant that parseEnumToken resolves against:
static <E extends Enum<E>> E parseEnumToken(
Class<E> enumClass, String value, String errorMessagePrefix, String instance) {
try {
return Enum.valueOf(enumClass, normalizeToken(value));
} catch (IllegalArgumentException e) {
throw new InvalidInputException(
errorMessagePrefix + value, formatCurrentStackTrace(), instance);
}
}snake_case works (exist_ok → EXIST_OK), so the "supports both" promise holds for exactly one of the two spellings.
ExistOk is the only multi-word constant in the spec's mode/behavior vocabulary, which is why nothing else is affected:
| Request field | Spec values | Status |
|---|---|---|
CreateNamespaceRequest.mode |
Create / ExistOk / Overwrite | ExistOk rejected |
CreateTableRequest.mode |
Create / ExistOk / Overwrite | ExistOk rejected |
RegisterTableRequest.mode |
Create / Overwrite | ok |
DropNamespaceRequest.mode |
Fail / Skip | ok |
DropNamespaceRequest.behavior |
Restrict / Cascade | ok |
The five parseEnumToken call sites are GravitinoLanceNameSpaceOperations.java:449,457,465 and GravitinoLanceTableOperations.java:444,452.
For reference, the dir namespace implementation in lancedb/lance parses the same field with eq_ignore_ascii_case("existok") || eq_ignore_ascii_case("exist_ok") (rust/lance-namespace-impls/src/dir/manifest.rs), i.e. it accepts both spellings.
Error message and/or stacktrace
lance_namespace.errors.InvalidInputError: Invalid input: Unknown create namespace mode: ExistOk
Server side (includeErrorStackTrace = true):
java.lang.RuntimeException: Captured stacktrace
at org.apache.gravitino.lance.common.ops.gravitino.CommonUtil.formatCurrentStackTrace(CommonUtil.java:31)
at org.apache.gravitino.lance.common.ops.gravitino.CommonUtil.parseEnumToken(CommonUtil.java:54)
at org.apache.gravitino.lance.common.ops.gravitino.GravitinoLanceNameSpaceOperations.parseCreateMode(GravitinoLanceNameSpaceOperations.java:449)
at org.apache.gravitino.lance.service.rest.LanceNamespaceOperations.createNamespace(LanceNamespaceOperations.java:130)
The table path produces the same thing with Unknown create table mode: ExistOk.
How to reproduce
Gravitino 1.3.0 with the lance-rest auxiliary service on port 9101 and a lakehouse-generic catalog named lance_catalog:
gravitino.auxService.names = iceberg-rest,lance-rest
gravitino.lance-rest.classpath = lance-rest-server/libs
gravitino.lance-rest.httpPort = 9101
gravitino.lance-rest.namespace-backend = gravitino
gravitino.lance-rest.gravitino-uri = http://127.0.0.1:8090
gravitino.lance-rest.gravitino-metalake = test# pip install lance-namespace==0.8.6 pyarrow
import io, uuid, pyarrow as pa, lance_namespace as ln
ns = ln.connect("rest", {"uri": "http://127.0.0.1:9101/lance"})
nsid = ["lance_catalog", f"modeprobe_{uuid.uuid4().hex[:6]}"]
def probe(label, fn):
try:
fn()
print(f" {label:36} -> OK")
except Exception as e:
print(f" {label:36} -> {type(e).__name__}: {str(e).splitlines()[0]}")
ns.create_namespace(ln.CreateNamespaceRequest(id=nsid, mode="EXIST_OK"))
print("CreateNamespace, namespace already exists:")
for mode in ["ExistOk", "exist_ok", "EXIST_OK", "Overwrite"]:
probe(f"mode={mode!r}", lambda m=mode: ns.create_namespace(ln.CreateNamespaceRequest(id=nsid, mode=m)))
sink = io.BytesIO()
with pa.ipc.new_stream(sink, pa.schema([pa.field("id", pa.int64())])):
pass
schema_ipc = sink.getvalue()
tid = nsid + ["t1"]
ns.create_table(ln.CreateTableRequest(id=tid), schema_ipc)
print("CreateTable, table already exists:")
for mode in ["ExistOk", "exist_ok", "EXIST_OK"]:
probe(f"mode={mode!r}", lambda m=mode: ns.create_table(ln.CreateTableRequest(id=tid, mode=m), schema_ipc))
ns.drop_namespace(ln.DropNamespaceRequest(id=nsid, mode="Skip", behavior="Cascade"))Observed:
CreateNamespace, namespace already exists:
mode='ExistOk' -> InvalidInputError: Invalid input: Unknown create namespace mode: ExistOk
mode='exist_ok' -> OK
mode='EXIST_OK' -> OK
mode='Overwrite' -> OK
CreateTable, table already exists:
mode='ExistOk' -> InvalidInputError: Invalid input: Unknown create table mode: ExistOk
mode='exist_ok' -> OK
mode='EXIST_OK' -> OK
Expected: ExistOk is accepted wherever exist_ok is.
Additional context
Found while building a Daft × Gravitino Lance REST × MinIO end-to-end suite. A client that follows the spec's own example spelling cannot create a namespace or a table idempotently and has to discover the snake_case spelling by trial.
TestGravitinoLanceModeParsing already covers the snake_case half:
Assertions.assertEquals("EXIST_OK", CommonUtil.normalizeToken("exist_ok"));
Assertions.assertEquals(
TestMode.EXIST_OK,
CommonUtil.parseEnumToken(TestMode.class, "exist_ok", "Unknown mode: ", "table"));There is no corresponding PascalCase assertion, which is presumably why this went unnoticed.
normalizeToken has one other caller, LanceMetadataAuthorizationMethodInterceptor.java:223, which compares against OVERWRITE. That token is single-word, so it is unaffected.
Activity
[apache#13680] mention PascalCase ExistOk and fold the duplicate crea…
FANNG1 commentedon Oct 9, 2026
@kalayciburak could you comment on this issue, so I could assign to you, thanks!
[apache#13680] fix: accept PascalCase ExistOk as a Lance create mode (a…