#7524·surrealdb

Bug: DEFINE INDEX on a sub-field of a nullable object (`null | { ... }`) fails on SCHEMAFULL tables

Author: rymskipCreated Sep 17, 2026Updated Sep 17, 2026

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

surql
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:

rust
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:

rust
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.