Skip to content

change type none to type missing and improve type safety - #7491

Merged
mccanne merged 3 commits into
mainfrom
missing
Oct 19, 2026
Merged

mccanne merged 3 commits into
mainfrom
missing

Conversation

@mccanne

@mccanne mccanne commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

This commit addresses some confusion that has been arising regarding the difference between option types and a union (none|T). To do so, we renamed the none type as missing and introduce the concept that there are two types of nones: a missing none (i.e., none::missing), and an option none (i.e., none::option(T) aka none::T).

none::missing values can be cast to option(T) by casting the none to type T. While none::missing exists in the runtime as a transient value, they are not expected to be serialized anywhere (except for inside fusion values). That said, we allow their serialization for test and debug with the goal of making sure the runtime has sufficient type safety so that they can never be materialized, e.g., "values none" (which implies none::missing) should produce an error though we haven't implemented this yet. Option nones are always serializable.

Fusion now creates fused records with missing fields with the missing type. This seems to be less confusing.

The option type now includes vector.Missing to represent none case but we change the conventions so that the these none::missing values are never expressed out of an option value. Instead, option.Coalesce is called with default values or errors are expressed as nones.

We changed Apply so RipFusions does not rip options and RipOption continues to mean to separate the nones (as missings) and the somes. It's up to the caller of Apply to handle the option values appropriately. This moves that logic to the call sites where type-safe semantics can be implemented requiring users to specify queries that handle the none cases when they arise. (There are more cases to cover in the runtime, and the next step with the type checker is to catch at compile time as many as the option-handling type errors runtime as possible)

Fixes #7431

Comment thread runtime/expr/agg/union.go Outdated
func (u *union) Consume(vec vector.Any) {
if k := vec.Kind(); k == vector.KindNone {
// XXX TBD - missing should be error
if k := vec.Kind(); k == vector.KindMissing {

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.

Nit:

Suggested change
if k := vec.Kind(); k == vector.KindMissing {
if vec.Kind() == vector.KindMissing {

Comment thread runtime/expr/eval.go
Comment thread value.go Outdated
return v
}

// to its underlying value. It does not Deunion union values that are named types.

@nwt nwt Oct 19, 2026 •

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.

Nit: The sentence fragment "to its underlying value." does not make sense.

Comment thread context.go
Comment thread complex.go
tag := DecodeUint(it.Next())
if tag == OptionNoneTag {
return TypeNone, nil
return nil, nil

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.

Nit: Adding a Boolean "ok" return paramter would make for a nicer interface.

Comment thread vector/view.go
Comment thread vector/pick.go Outdated
return resultTags, results
}

func optionTags(index []uint32, d *Dynamic) ([]uint32, []uint32, uint32) {

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.

Nits: We usually put a vector parameter before an index parameter. And simplify both this interface and the implementation by letting the caller compute noneLen since it's just len(newTags)-len(someIndex).

Comment thread vector/option.go Outdated
// Dynamic with the first value a some and the second value a none.
// This design relies upon super.OptionSomeTag=0 and super.OptionNoneTag=1.
// Note that the Missing vector use here is arbitrary: we just need a singleton
// type that is cannot otherwise be an option type because we need to

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.

Suggested change
// type that is cannot otherwise be an option type because we need to
// type that cannot otherwise be an option type because we need to

Comment thread vector/option.go Outdated
Comment on lines +27 to +31
func NewSome(typ *super.TypeOption, vec Any) *Option {
return &Option{Typ: typ, Any: vec}
}

func NewNone(typ *super.TypeOption, noneLen uint32) *Option {

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.

Nit: I think we should call these NewOptionSome and NewOptionNone.

Comment on lines +72 to +73
atyp := sctx.LookupTypeArray(super.TypeMissing)
return vector.NewArray(atyp, []uint32{0, 0}, vector.NewMissing(0))

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.

Not trying to hold up the review but I'm not sure about giving an empty array a type of missing- feels like its in an error state when there is nothing wrong the array.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's not an error. typeof([]) has to have a type for the inner type. As we discussed, I'm going to put back TypeNone in a subsequent PR and this will change back to [none] then.

@nwt nwt 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.

All my unresolved comments are just nits.

This commit addresses some confusion that has been arising regarding
the difference between option types and a union (none|T).
To do so, we renamed the none type as missing and introduce
the concept that there are two types of nones: a missing none
(i.e., none::missing), and an option none (i.e., none::option(T)
aka none::T).

none::missing values can be cast to option(T) by casting
the none to type T.  While none::missing exists in the runtime
as a transient value, they are not expected to be serialized
anywhere (except for inside fusion values).  That said, we allow
their serialization for test and debug with the goal of making
sure the runtime has sufficient type safety so that they
can never be materialized, e.g., "values none" (which implies
none::missing) should produce an error though we haven't
implemented this yet. Option nones are always serializable.

Fusion now creates fused records with missing fields with the
missing type.  This seems to be less confusing.

The option type now includes vector.Missing to represent none case
but we change the conventions so that the these none::missing
values are never expressed out of an option value.  Instead,
option.Coalesce is called with default values or errors are
expressed as nones.

We changed Apply so RipFusions does not rip options and RipOption
continues to mean to separate the nones (as missings) and the somes.
It's up to the caller of Apply to handle the option values
appropriately.  This moves that logic to the call sites where
type-safe semantics can be implemented requiring users to specify
queries that handle the none cases when they arise. (There are
more cases to cover in the runtime, and the next step with the
type checker is to catch at compile time as many as the option-handling
type errors runtime as possiblea)
@mccanne
mccanne merged commit e85d306 into main Oct 19, 2026
4 checks passed
@mccanne
mccanne deleted the missing branch October 19, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested none values being printed in JSON as quoted string "null"

3 participants