Bug: DEFINE INDEX on a sub-field of a nullable object (`null | { ... }`) fails on SCHEMAFULL tables
Describe the bug
On a SCHEMAFULL table, DEFINE INDEX rejects a sub-field path when the parent field's type is a union that includes null, such as null | { city: string }. The same path is accepted when the union includes none instead (option<{ city: string }>).
There is no workaround that keeps null: defining the sub-field explicitly is rejected as a type mismatch with the parent.
This affects every index kind (standard, UNIQUE, FULLTEXT, HNSW, DISKANN). Nullable objects are common when records are written from JSON clients, where a missing object is serialized as null, and such a field can't be switched to option<...> because option<...> rejects NULL.
Steps to reproduce
DEFINE TABLE person SCHEMAFULL;
DEFINE FIELD address ON person TYPE null | { city: string } DEFAULT NULL;
DEFINE INDEX person_city ON person FIELDS address.city;
-- Error: The field 'address.city' does not exist
DEFINE FIELD address.city ON person TYPE string;
-- Error: Cannot set field `address.city` with type `string` as it mismatched
-- with field `address` with type `null | { city: string }`
-- With `none` instead of `null` the index is accepted:
DEFINE FIELD OVERWRITE address ON person TYPE option<{ city: string }>;
DEFINE INDEX person_city ON person FIELDS address.city;
-- OK
-- but `option<...>` doesn't accept NULL values:
CREATE person:2 SET address = NULL;
-- Error: Couldn't coerce value for field `address` of `person:2`:
-- Expected `none | { city: string }` but found `NULL`{ city: string } | null and option<null | { city: string }> fail the same way. SCHEMALESS tables are not affected.
Expected behaviour
DEFINE INDEX ... FIELDS address.city succeeds when address is null | { city: string }, the same as for none | { city: string }. Reading address.city when address is NULL yields NONE, exactly as when address is NONE, so a null variant doesn't make the sub-field path any less valid to index.
Cause
The schemafull check added in #6885 falls back to Kind::allows_sub_fields() for sub-field paths (core/src/expr/statements/define/index.rs). For unions it only exempts none:
Kind::Either(kinds) => {
kinds.iter().all(|k| matches!(k, Kind::None) || k.allows_sub_fields())
}Kind::Null falls through to false, so any union containing null is rejected. #6885 describes the intent as "union types where every non-none variant itself permits sub-fields", and its test table has no null case, so this looks like an oversight rather than a deliberate restriction. Treating null like none here would fix it:
kinds.iter().all(|k| matches!(k, Kind::None | Kind::Null) || k.allows_sub_fields())The error message could also be clearer: "The field 'address.city' does not exist" suggests a missing definition rather than a type that doesn't allow sub-fields.
SurrealDB version
3.2.4+20260803.93ab219 for linux on x86_64 (surreal start memory). The same code is on main.
Source: surrealdb/surrealdb