A dimension table with a BYTES primary key column can never be looked up. Every probe misses, whatever the caller
passes. This affects the single-stage lookup transform function and the multi-stage lookup join, in both dimension
table storage modes.
The failure is silent. containsKey returns false, lookupRow / lookupValue / lookupValues return null, and the
query returns no rows with no error.
Reproduction
I built a real single-column BYTES primary key dimension table from a real segment, loaded it through
DimensionTableDataManager, and probed it four ways. A STRING primary key is included as a control.
########## PK TYPE = BYTES disablePreload=false ##########
stored key class = [B
probe with ByteArray(copy) -> containsKey=false lookupValues=null
probe with ByteArray(same instance) -> containsKey=false lookupValues=null
probe with byte[] copy -> containsKey=false lookupValues=null
probe with byte[] same instance -> containsKey=false lookupValues=null
########## PK TYPE = STRING disablePreload=false ##########
stored key class = java.lang.String
probe with String -> containsKey=true lookupValues=[abc, alpha]
########## PK TYPE = BYTES disablePreload=true ##########
stored key class = [B
probe with ByteArray(copy) -> containsKey=false lookupValues=null
probe with ByteArray(same instance) -> containsKey=false lookupValues=null
probe with byte[] copy -> containsKey=false lookupValues=null
probe with byte[] same instance -> containsKey=false lookupValues=null
########## PK TYPE = STRING disablePreload=true ##########
stored key class = java.lang.String
probe with String -> containsKey=true lookupValues=[abc, alpha]
Note that even the identical byte[] instance that was ingested fails to match, because the value in the map is a new
array produced when the segment was read back.
Root cause
The lookup map stores the primary key as an Object[] whose BYTES element is a raw byte[]:
DimensionTableDataManager builds each key with recordReader.getRecordValues(i, pkIndexes)
(line 242
for the preloaded table, line 320
for the memory-optimized one).
- That reaches
PinotSegmentColumnReader.getValue, which returns a raw byte[] for a BYTES column on both the
dictionary-encoded path (BytesDictionary.get returns byte[], while the ByteArray variant is getInternal,
which this path does not call) and the raw path (ForwardIndexReader.getBytes).
- The map compares keys with
DimensionTableDataManager.HASH_STRATEGY
(line 64),
which is Arrays.hashCode(Object[]) and Arrays.equals(Object[]). Both delegate to the hashCode and equals of
each element.
byte[] inherits both from Object, so they are identity based.
A key that compares by identity cannot be reproduced by a caller. A ByteArray fails because it is a different class
with a content based hash, and a fresh byte[] fails because it is a different object.
Callers
LookupTransformFunction wraps the probe value in a ByteArray
(line 218),
which is the representation the rest of Pinot uses for BYTES. That is the reasonable choice, and it still misses,
because the stored side is the part that is wrong.
The multi-stage lookup join reaches the same map through lookupValues and misses the same way.
Why no test caught it
LookupTransformFunctionTest.primaryKeyTypeTest
(line 346)
covers a BYTES primary key, but it mocks DimensionTableDataManager and stubs lookupValue to return
"lookup_value_for_[" + pk.hashCode() + "]". It asserts the hash of the key that the transform function builds and
never loads a dimension table. It therefore verifies that the probe is well formed, and cannot observe whether the
probe matches anything.
Suggested fix
Store ByteArray rather than byte[] in the key array, in both createFastLookupDimensionTable and the
memory-optimized path. LookupTransformFunction already sends a ByteArray, so it starts working with no change.
The multi-stage lookup join then works as well.
Worth adding at the same time:
- A test that loads a real dimension table with a
BYTES primary key and asserts a successful lookup. The current test
cannot fail, whatever the storage side does.
- The same check for
UUID, which shares the BYTES stored type.
Until this is fixed, PR #19210 rejects a BYTES primary key constant in the multi-stage lookup join with a clear
error, rather than returning an empty result.
Related
BIG_DECIMAL primary keys have a milder version of the same class of problem. BigDecimal.equals compares the scale,
so a stored 1.50 does not match a probe of 1.5. That one is not specific to dimension tables, because a hash join
compares BIG_DECIMAL keys the same way through ObjectLookupTable, so it is left out of this issue.
A dimension table with a
BYTESprimary key column can never be looked up. Every probe misses, whatever the callerpasses. This affects the single-stage
lookuptransform function and the multi-stage lookup join, in both dimensiontable storage modes.
The failure is silent.
containsKeyreturns false,lookupRow/lookupValue/lookupValuesreturn null, and thequery returns no rows with no error.
Reproduction
I built a real single-column
BYTESprimary key dimension table from a real segment, loaded it throughDimensionTableDataManager, and probed it four ways. ASTRINGprimary key is included as a control.Note that even the identical
byte[]instance that was ingested fails to match, because the value in the map is a newarray produced when the segment was read back.
Root cause
The lookup map stores the primary key as an
Object[]whoseBYTESelement is a rawbyte[]:DimensionTableDataManagerbuilds each key withrecordReader.getRecordValues(i, pkIndexes)(line 242
for the preloaded table, line 320
for the memory-optimized one).
PinotSegmentColumnReader.getValue, which returns a rawbyte[]for aBYTEScolumn on both thedictionary-encoded path (
BytesDictionary.getreturnsbyte[], while theByteArrayvariant isgetInternal,which this path does not call) and the raw path (
ForwardIndexReader.getBytes).DimensionTableDataManager.HASH_STRATEGY(line 64),
which is
Arrays.hashCode(Object[])andArrays.equals(Object[]). Both delegate to thehashCodeandequalsofeach element.
byte[]inherits both fromObject, so they are identity based.A key that compares by identity cannot be reproduced by a caller. A
ByteArrayfails because it is a different classwith a content based hash, and a fresh
byte[]fails because it is a different object.Callers
LookupTransformFunctionwraps the probe value in aByteArray(line 218),
which is the representation the rest of Pinot uses for
BYTES. That is the reasonable choice, and it still misses,because the stored side is the part that is wrong.
The multi-stage lookup join reaches the same map through
lookupValuesand misses the same way.Why no test caught it
LookupTransformFunctionTest.primaryKeyTypeTest(line 346)
covers a
BYTESprimary key, but it mocksDimensionTableDataManagerand stubslookupValueto return"lookup_value_for_[" + pk.hashCode() + "]". It asserts the hash of the key that the transform function builds andnever loads a dimension table. It therefore verifies that the probe is well formed, and cannot observe whether the
probe matches anything.
Suggested fix
Store
ByteArrayrather thanbyte[]in the key array, in bothcreateFastLookupDimensionTableand thememory-optimized path.
LookupTransformFunctionalready sends aByteArray, so it starts working with no change.The multi-stage lookup join then works as well.
Worth adding at the same time:
BYTESprimary key and asserts a successful lookup. The current testcannot fail, whatever the storage side does.
UUID, which shares theBYTESstored type.Until this is fixed, PR #19210 rejects a
BYTESprimary key constant in the multi-stage lookup join with a clearerror, rather than returning an empty result.
Related
BIG_DECIMALprimary keys have a milder version of the same class of problem.BigDecimal.equalscompares the scale,so a stored
1.50does not match a probe of1.5. That one is not specific to dimension tables, because a hash joincompares
BIG_DECIMALkeys the same way throughObjectLookupTable, so it is left out of this issue.