Repository: polarsignals/frostdb (go)

Issue:
Filtering for existing labels being `NULL`
With system-wide profiling in Parca and Parca Agent it might be interesting to once in a while filter for all series that don't have a specific label. Like, "show me all series that don't have a `namespace` label", basically excluding everything that's Kubernetes related. 

Here are some examples we should support in FrostDB and add to the [filter](https://github.com/polarsignals/frostdb/blob/main/logictest/testdata/exec/filter/filter) logic test.

```
exec
select labels, stacktrace, timestamp, value where labels.label5 = ''
----
value1  value2  null    null    stack1  1       1
value2  value2  value3  null    stack1  2       2
value3  value2  null    value4  stack1  3       3

exec
select labels, stacktrace, timestamp, value where labels.label4 = null
----
value1  value2  null    null    stack1  1       1
value2  value2  value3  null    stack1  2       2
```

We can debate whether the first one makes sense. However, the concept of `null` isn't really a concept in PromQL and most other languages afaik. 

Pull request description:
Add support for filtering for IS NULL or IS NOT NULL
Closes #350

Patch:
diff --git a/binaryscalarexpr.go b/binaryscalarexpr.go
index 779669c..5c8d210 100644
--- a/binaryscalarexpr.go
+++ b/binaryscalarexpr.go
@@ -69,6 +69,13 @@ var ErrUnsupportedBinaryOperation = errors.New("unsupported binary operation")
 func BinaryScalarOperation(left parquet.ColumnChunk, right parquet.Value, operator logicalplan.Op) (bool, error) {
 	switch operator {
 	case logicalplan.OpEq:
+		if right == parquet.NullValue() {
+			// Assume all ColumnChunk have NULLs for now.
+			// They will be read and added to a bitmap later on.
+			// TODO: Maybe there's a nice way of reading the NumNulls from the Pages, for me they always return 0
+			return true, nil
+		}
+
 		bloomFilter := left.BloomFilter()
 		if bloomFilter == nil {
 			// If there is no bloom filter then we cannot make a statement about true negative, instead check the min max values of the column chunk
diff --git a/pqarrow/parquet.go b/pqarrow/parquet.go
index 6532aa2..701f2c4 100644
--- a/pqarrow/parquet.go
+++ b/pqarrow/parquet.go
@@ -27,6 +27,8 @@ func ArrowScalarToParquetValue(sc scalar.Scalar) (parquet.Value, error) {
 		return parquet.ValueOf(v), nil
 	case *scalar.Boolean:
 		return parquet.ValueOf(s.Value), nil
+	case *scalar.Null:
+		return parquet.NullValue(), nil
 	case nil:
 		return parquet.Value{}, nil
 	default:
diff --git a/query/physicalplan/binaryscalarexpr.go b/query/physicalplan/binaryscalarexpr.go
index 81e5e0d..be3dad9 100644
--- a/query/physicalplan/binaryscalarexpr.go
+++ b/query/physicalplan/binaryscalarexpr.go
@@ -163,6 +163,16 @@ func DictionaryArrayScalarNotEqual(left *array.Dictionary, right scalar.Scalar)
 		data = r.Data()
 	}
 
+	// This is a special case for where the left side should not equal NULL
+	if right == scalar.ScalarNull {
+		for i := 0; i < left.Len(); i++ {
+			if !left.IsNull(i) {
+				res.Add(uint32(i))
+			}
+		}
+		return res, nil
+	}
+
 	for i := 0; i < left.Len(); i++ {
 		if left.IsNull(i) {
 			continue
@@ -193,6 +203,16 @@ func DictionaryArrayScalarEqual(left *array.Dictionary, right scalar.Scalar) (*B
 		data = r.Data()
 	}
 
+	// This is a special case for where the left side should equal NULL
+	if right == scalar.ScalarNull {
+		for i := 0; i < left.Len(); i++ {
+			if left.IsNull(i) {
+				res.Add(uint32(i))
+			}
+		}
+		return res, nil
+	}
+
 	for i := 0; i < left.Len(); i++ {
 		if left.IsNull(i) {
 			continue


Test patch:
diff --git a/logictest/testdata/exec/filter/filter b/logictest/testdata/exec/filter/filter
index 03bd2c7..7feea3e 100644
--- a/logictest/testdata/exec/filter/filter
+++ b/logictest/testdata/exec/filter/filter
@@ -101,3 +101,14 @@ select labels, stacktrace, timestamp, value where labels.label4 = 'value4' or (l
 ----
 value2  value2  value3  null    stack1  2       2
 value3  value2  null    value4  stack1  3       3
+
+exec
+select labels, stacktrace, timestamp, value where labels.label4 = null
+----
+value1  value2  null    null    stack1  1       1
+value2  value2  value3  null    stack1  2       2
+
+exec
+select labels, stacktrace, timestamp, value where labels.label4 != null
+----
+value3  value2  null    value4  stack1  3       3