Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,36 @@ SELECT * FROM cte WHERE d = 2;`,
Query: "SELECT 100 NOT IN (SELECT i2 FROM niltable)",
Expected: []sql.Row{{nil}},
},
// Tuple IN (SELECT ...) three-valued membership (NULL stragglers after #3651)
{
Query: "SELECT (1, 5) IN (SELECT 1, NULL FROM dual)",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT (1, 5) IN (SELECT * FROM (SELECT 1, NULL FROM dual UNION ALL SELECT 2, 3 FROM dual) t)",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT (1, NULL) IN (SELECT 1, 2 FROM dual)",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT (NULL, NULL) IN (SELECT 1, 2 FROM dual)",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT (1, 5) NOT IN (SELECT 1, NULL FROM dual)",
Expected: []sql.Row{{nil}},
},
{
// Definite FALSE: non-null left component mismatches despite right NULL
Query: "SELECT (1, NULL) IN (SELECT 2, NULL FROM dual)",
Expected: []sql.Row{{false}},
},
{
Query: "SELECT (1, 5) IN (SELECT 1, 5 FROM dual)",
Expected: []sql.Row{{true}},
},
{
Query: "SELECT 1 IN (2,3,4,null)",
Expected: []sql.Row{{nil}},
Expand Down Expand Up @@ -9721,14 +9751,6 @@ FROM mytable;`,
{"DECIMAL"},
},
},
{
Query: "SELECT (1, 5) IN (SELECT 1, NULL FROM dual)",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT (1, 5) IN (SELECT * FROM (SELECT 1, NULL FROM dual UNION ALL SELECT 2, 3 FROM dual) t)",
Expected: []sql.Row{{nil}},
},
}

var VersionedQueries = []QueryTest{
Expand Down
67 changes: 66 additions & 1 deletion sql/plan/insubquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ func (in *InSubquery) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
// convert left to right's type
nLeft, _, err := rTyp.Convert(ctx, left)
if err != nil {
// SELECT 1, NULL yields tuple(..., null). Converting a non-NULL left
// component into a null-typed column fails, but membership is still
// three-valued — scan rather than treating it as definite FALSE.
if tup, ok := rTyp.(types.TupleType); ok {
return evalTupleInSubquery(ctx, right, row, left, tup, rTyp)
}
return false, nil
}

Expand All @@ -105,6 +111,12 @@ func (in *InSubquery) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {

val, notFoundErr := values.Get(key)
if notFoundErr != nil {
// Hash miss: scalars only need a whole-NULL probe. Tuples need a
// three-valued scan — right rows with NULL components hash away from
// non-NULL lefts (and lefts with NULL components never hash-hit).
if tup, ok := rTyp.(types.TupleType); ok {
return evalTupleInSubquery(ctx, right, row, nLeft, tup, rTyp)
}
if _, nilValNotFoundErr := values.Get(nilKey); nilValNotFoundErr == nil {
return nil, nil
}
Expand All @@ -117,7 +129,7 @@ func (in *InSubquery) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
}

if tup, ok := rTyp.(types.TupleType); ok {
lVals, lok := left.([]interface{})
lVals, lok := nLeft.([]interface{})
rVals, rok := val.([]interface{})
if lok && rok && len(lVals) == len(rVals) && len(lVals) == len(tup) {
cmp, hasNil, cErr := expression.NullUnsafeCompareTuples(ctx, lVals, rVals, tup)
Expand Down Expand Up @@ -191,6 +203,59 @@ func (in *InSubquery) Dispose(ctx *sql.Context) {
}
}

// evalTupleInSubquery performs three-valued membership for row constructors
// against a subquery result set. Definite TRUE wins; otherwise any comparison
// that is unknown (hasNil && cmp == 0) yields NULL; else FALSE.
func evalTupleInSubquery(ctx *sql.Context, sq *Subquery, row sql.Row, left interface{}, tup types.TupleType, rTyp sql.Type) (interface{}, error) {
lVals, lok := left.([]interface{})
if !lok {
return false, nil
}

allVals, err := sq.EvalMultiple(ctx, row)
if err != nil {
return nil, err
}

var hasNullCmp bool
for _, rv := range allVals {
if rv == nil {
hasNullCmp = true
continue
}
rVal, _, cErr := rTyp.Convert(ctx, rv)
if cErr != nil {
// Fall back to the raw row when type conversion fails (null-typed cols).
rVal = rv
}
if rVal == nil {
hasNullCmp = true
continue
}
rVals, rok := rVal.([]interface{})
if !rok || len(rVals) != len(lVals) || len(rVals) != len(tup) {
continue
}
cmp, hasNil, cmpErr := expression.NullUnsafeCompareTuples(ctx, lVals, rVals, tup)
if cmpErr != nil {
return nil, cmpErr
}
if hasNil {
if cmp == 0 {
hasNullCmp = true
}
continue
}
if cmp == 0 {
return true, nil
}
}
if hasNullCmp {
return nil, nil
}
return false, nil
}

// NewNotInSubquery creates a new NotInSubquery expression.
func NewNotInSubquery(ctx *sql.Context, left sql.Expression, right sql.Expression) sql.Expression {
return expression.NewNot(NewInSubquery(ctx, left, right))
Expand Down
Loading