A covering index scan decodes an index entry into a partial record: the columns the index carries are set, and every other field of the record type is left absent. That only works while absence is representable for every field. A not-nullable field makes it unrepresentable; the decoder has no value to supply, and the type does not admit that the field may be missing, therefore supporting not-nullable columns is blocked until a covering scan can express what it actually returns.
Example
Setup:
CREATE TABLE T(id BIGINT, a BIGINT NOT NULL, c BIGINT, d BIGINT, PRIMARY KEY(id));
CREATE INDEX mv AS SELECT c, d FROM T ORDER BY c, d;
The query:
SELECT c, d FROM T WHERE c = 20; -- planned as a covering scan over mv
The plan is a covering scan whose copiers fill three fields:
COVERING(MV [EQUALS promote(@c9 AS LONG)] -> [C: KEY:[0], D: KEY:[1], ID: KEY:[3]]) | MAP (_.C AS C, _.D AS D)
and the entry against the record type looks like this:
index entry : KEY( c, d, <record type>, id ) VALUE( )
record type : id, a, c, d
filled : c, d, id
absent : a <-- NOT NULL, and nothing can fill it
a is not carried by the index, so no copier fills it, yet the operator still declares its result type to be the full record type, in which a cannot be absent. IndexKeyValueToPartialRecord.toRecordInternal already anticipates the failure:
if (isRequired) {
// If any of the copiers refused since they were asked to copy a null into a required field, this following
// build call will fail with an exception.
return recordBuilder.build();
}
A copier refuses when asked to put a null into a required field (FieldCopier.copy returns !fieldDescriptor.isRequired()), and a field nobody fills is simply never set, so build() fails on an uninitialized message. Nullability in the type system follows the same fact:
Type.Record.Field.fromDescriptor derives it as !fieldDescriptor.isRequired().
This effectively means that we currently can not have a covering index scan when one of the uncovered fields is constrained to be not-null, or, in protobuf parlance, required.
A covering index scan decodes an index entry into a partial record: the columns the index carries are set, and every other field of the record type is left absent. That only works while absence is representable for every field. A not-nullable field makes it unrepresentable; the decoder has no value to supply, and the type does not admit that the field may be missing, therefore supporting not-nullable columns is blocked until a covering scan can express what it actually returns.
Example
Setup:
The query:
The plan is a covering scan whose copiers fill three fields:
and the entry against the record type looks like this:
ais not carried by the index, so no copier fills it, yet the operator still declares its result type to be the full record type, in whichacannot be absent.IndexKeyValueToPartialRecord.toRecordInternalalready anticipates the failure:A copier refuses when asked to put a null into a required field (
FieldCopier.copyreturns!fieldDescriptor.isRequired()), and a field nobody fills is simply never set, sobuild()fails on an uninitialized message. Nullability in the type system follows the same fact:Type.Record.Field.fromDescriptorderives it as!fieldDescriptor.isRequired().This effectively means that we currently can not have a covering index scan when one of the uncovered fields is constrained to be not-null, or, in protobuf parlance, required.