Skip to content

CASSANDRA-20667: Accord: BEGIN TRANSACTIONs IF condition logic does not properly support meaningless emptiness and null values #4175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: trunk
Choose a base branch
from

Conversation

dcapwell
Copy link
Contributor

No description provided.

…ot properly support meaningless emptiness and null values
@Override
public boolean isNull(Row row)
{
return column.type.isNull(rowValue(row));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

isNull has the following semantics

  1. for non-meangingless emptyness null and empty are both null
  2. for meangingless emptyness null and empty are both null
  3. for types that support empty, only null is null

The main difference between 1/2 is that in 1 empty is rejected during validation, so normally means you passed a tombstone here by mistake (historic bug on the read path in RowFilter). 2 allows empty during validation

{
case MAP:
{
exists = !type.nameComparator().isNull(cell.path().get(0));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

im still not 100% sold on this, here is the test that hits this

map<int, int>

IF row.map[<empty>] IS NULL -- returns true as the key is "null".  The data has map[<empty>] -> 42
IF row.map[42] IS NULL -- returns true if the value is <empty>

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we have a particularly good principled behaviour we can implement here, but I think treating empty as NULL wherever it is safe to do so, and unsafe to treat it as a raw value, is perhaps as good as we can get.

@@ -532,7 +589,13 @@ else if (type.isUDT())
@Override
public boolean applies(TxnData data)
{
return getBounds(data).appliesTo(reference.getRow(data));
Bound bounds = getBounds(data);
if (reference.column().type.unwrap().isNull(bounds.value))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

semantic difference with CAS IF. if input or value are null, applies is

reference.column().type.unwrap().isNull(bounds.value) == bounds.isNull(row)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is OK, because bound cannot be an IS NULL or IS NOT NULL check.

@@ -351,4 +407,100 @@ public long serializedSize(TxnReference reference, TableMetadatas tables, Versio
return size;
}
};

private static class ClusteringColumnData extends AbstractCell<ByteBuffer>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is kinda a hack... it was this or refactor the code more. I could also special case clustering, but felt that this was best as it kept the logic simpler

private final ByteBuffer value;

public Constant(ByteBuffer value)
public Constant(@Nullable ByteBuffer value)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

should add a test to show this, but the value can be null which leads to NPE later on, so marked this nullable and made it handled

@@ -115,19 +118,24 @@ void collect(TableMetadatas.Collector collector)
@Override
public void serialize(Constant constant, TableMetadatas tables, DataOutputPlus out, Version version) throws IOException
{
ByteBufferUtil.writeWithVIntLength(constant.value, out);
out.writeBoolean(constant.value != null);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not backwards or mixed compatible

import org.apache.cassandra.utils.memory.Cloner;
import org.apache.cassandra.utils.memory.HeapCloner;

public class SimplePartition extends AbstractBTreePartition
Copy link
Contributor Author

Choose a reason for hiding this comment

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

creating the RowIterator i need for the test was a lot of boiler plate, so figured could make this class to make writing tests less verbose

}
}

private class Updater implements UpdateFunction<Row, Row>, ColumnData.PostReconciliationFunction
Copy link
Contributor Author

Choose a reason for hiding this comment

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

honestly copied a ton from atomic btree partition, the issue i faced with reusing is having an allocator... I could try to make that logic reusable but didn't want to touch a critical section of code so I forked

@Override
public boolean isNull(Row row)
{
return row == null || row.getComplexColumnData(column) == null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is all filtered data, so can this data be non-live?

@@ -300,7 +300,7 @@ public Setter(ColumnMetadata column, Term t)
@Override
public boolean requiresTimestamp()
{
return true;
return column.type.isMultiCell();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

bug found in fuzz testing, frozen lists were requiring timestamp which made it hit this block

public void add(Operation operation, TableMetadata tableMetadata)
    {
        if (isForTxn && (operation.requiresRead() || operation.requiresTimestamp()))
        {
            add(operation.column, ReferenceOperation.create(operation, tableMetadata));
            return;
        }

this is a immutable bb so can avoid converting to a reference (i think it fails as a reference but forgot)

@@ -142,6 +143,8 @@ else if (isTxnRequest && coordinationFailed instanceof TopologyMismatch)
}
else
{
logger.error("Unexpected exception", fail);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was seeing that logs were clean but exceptions were happening, and found that this code block was hiding all these exceptions. The error isn't a CoordinationFailed so its not expected, so we should be loud! I found several unexpected exceptions by adding this in

Comment on lines +359 to +370
else if (reference.isElementSelection())
{
// This is frozen, so check if the Cell is a tombstone and that the element is present.
Cell<?> cell = (Cell<?>) columnData;
exists = exists(cell, reference.column().type);
if (exists)
{
ByteBuffer element = reference.getFrozenCollectionElement(cell);
exists = !reference.getFrozenCollectionElementType().isNull(element);
}
}
else if (reference.isFieldSelection())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

im not sure if this is allowed in CQL, but it was already here in the code so i made sure it worked

@dcapwell dcapwell requested a review from belliottsmith May 21, 2025 17:28
@maedhroz maedhroz self-requested a review June 6, 2025 16:59
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.

2 participants