diff --git a/sql3/errors.go b/sql3/errors.go index d5647166c..9a3a10da9 100644 --- a/sql3/errors.go +++ b/sql3/errors.go @@ -46,6 +46,8 @@ const ( ErrLiteralEmptySetNotAllowed errors.Code = "ErrLiteralEmptySetNotAllowed" ErrLiteralEmptyTupleNotAllowed errors.Code = "ErrLiteralEmptyTupleNotAllowed" ErrSetLiteralMustContainIntOrString errors.Code = "ErrSetLiteralMustContainIntOrString" + ErrInvalidColumnInFilterExpression errors.Code = "ErrInvalidColumnInFilterExpression" + ErrInvalidTypeInFilterExpression errors.Code = "ErrInvalidTypeInFilterExpression" ErrTypeAssignmentIncompatible errors.Code = "ErrTypeAssignmentIncompatible" @@ -198,6 +200,20 @@ func NewErrSetLiteralMustContainIntOrString(line, col int) error { ) } +func NewErrInvalidColumnInFilterExpression(line, col int, column string, op string) error { + return errors.New( + ErrInvalidColumnInFilterExpression, + fmt.Sprintf("[%d:%d] '%s' column cannot be used in a %s filter expression", line, col, column, op), + ) +} + +func NewErrInvalidTypeInFilterExpression(line, col int, typeName string, op string) error { + return errors.New( + ErrInvalidTypeInFilterExpression, + fmt.Sprintf("[%d:%d] unsupported type '%s' for %s filter expression", line, col, typeName, op), + ) +} + func NewErrLiteralEmptyTupleNotAllowed(line, col int) error { return errors.New( ErrLiteralEmptyTupleNotAllowed, diff --git a/sql3/planner/expressionpql.go b/sql3/planner/expressionpql.go index 2279edd01..cf2f7023b 100644 --- a/sql3/planner/expressionpql.go +++ b/sql3/planner/expressionpql.go @@ -193,6 +193,38 @@ func (p *ExecutionPlanner) generatePQLCallFromBinaryExpr(ctx context.Context, ex case parser.IN, parser.NOTIN: return nil, sql3.NewErrInternal("IN operator is not supported") + case parser.IS, parser.ISNOT: + lhs, ok := expr.lhs.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected lhs %T", expr.lhs) + } + + pqlOp := pql.EQ + if op == parser.ISNOT { + pqlOp = pql.NEQ + } + switch typ := expr.lhs.Type().(type) { + case *parser.DataTypeID: + if strings.EqualFold(lhs.columnName, "_id") { + return nil, sql3.NewErrInvalidColumnInFilterExpression(0, 0, "_id", "is/is not null") + } + return nil, sql3.NewErrInvalidTypeInFilterExpression(0, 0, typ.TypeName(), "is/is not null") + + case *parser.DataTypeInt, *parser.DataTypeDecimal, *parser.DataTypeTimestamp: + return &pql.Call{ + Name: "Row", + Args: map[string]interface{}{ + lhs.columnName: &pql.Condition{ + Op: pqlOp, + Value: nil, + }, + }, + }, nil + + default: + return nil, sql3.NewErrInvalidTypeInFilterExpression(0, 0, typ.TypeName(), "is/is not null") + } + case parser.BETWEEN, parser.NOTBETWEEN: return nil, sql3.NewErrInternal("BETWEEN operator is not supported") diff --git a/sql3/planner/opgroupby.go b/sql3/planner/opgroupby.go index 65deb4f6c..0cc1e09f8 100644 --- a/sql3/planner/opgroupby.go +++ b/sql3/planner/opgroupby.go @@ -6,7 +6,6 @@ import ( "context" "fmt" "hash/maphash" - "log" "github.com/molecula/featurebase/v3/errors" "github.com/molecula/featurebase/v3/sql3" @@ -336,6 +335,5 @@ func groupingKeyHash(ctx context.Context, groupByExprs []types.PlanExpression, r rowKeys[i] = v } result := hash.Sum64() - log.Printf("Hash %v, %v", result, rowKeys) return result, rowKeys, nil } diff --git a/sql3/sql_definitions_test.go b/sql3/sql_definitions_test.go index 3ac5055d6..100723dc1 100644 --- a/sql3/sql_definitions_test.go +++ b/sql3/sql_definitions_test.go @@ -316,6 +316,9 @@ var tableTests []tableTest = []tableTest{ nullTests, notNullTests, + //null filter tests + nullFilterTests, + //between tests betweenTests, notBetweenTests, diff --git a/sql3/sql_defs_null_test.go b/sql3/sql_defs_null_test.go index 89a4c4476..c55b2cda6 100644 --- a/sql3/sql_defs_null_test.go +++ b/sql3/sql_defs_null_test.go @@ -1,6 +1,6 @@ package sql3_test -//NULL tests +// NULL tests var nullTests = tableTest{ table: tbl( "null_all_types", @@ -144,7 +144,7 @@ var nullTests = tableTest{ }, } -//NOT NULL tests +// NOT NULL tests var notNullTests = tableTest{ table: tbl( "not_null_all_types", @@ -275,3 +275,172 @@ var notNullTests = tableTest{ }, }, } + +// NULL filter condition tests +var nullFilterTests = tableTest{ + table: tbl( + "null_filter_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i", fldTypeInt, "min 0", "max 1000"), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1), nil, nil, nil, nil, nil, nil, nil, nil), + srcRow(int64(2), int64(1), int64(10), bool(true), float64(10), int64(20), []int64{101}, string("foo"), []string{"GET", "POST"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id from null_filter_all_types where _id is null", + ), + expErr: "'_id' column cannot be used in a is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where _id is not null", + ), + expErr: "'_id' column cannot be used in a is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where i1 is null", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id from null_filter_all_types where i1 is not null", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + ), + expRows: rows( + row(int64(2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id from null_filter_all_types where b1 is null", + ), + expErr: "unsupported type 'BOOL' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where b1 is not null", + ), + expErr: "unsupported type 'BOOL' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where d1 is null", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id from null_filter_all_types where d1 is not null", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + ), + expRows: rows( + row(int64(2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id from null_filter_all_types where id1 is null", + ), + expErr: "unsupported type 'ID' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where id1 is not null", + ), + expErr: "unsupported type 'ID' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where ids1 is null", + ), + expErr: "unsupported type 'IDSET' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where ids1 is not null", + ), + expErr: "unsupported type 'IDSET' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where s1 is null", + ), + expErr: "unsupported type 'STRING' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where s1 is not null", + ), + expErr: "unsupported type 'STRING' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where ss1 is null", + ), + expErr: "unsupported type 'STRINGSET' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where ss1 is not null", + ), + expErr: "unsupported type 'STRINGSET' for is/is not null filter expression", + }, + { + sqls: sqls( + "select _id from null_filter_all_types where t1 is null", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id from null_filter_all_types where t1 is not null", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + ), + expRows: rows( + row(int64(2)), + ), + compare: compareExactUnordered, + }, + }, +}