Repository: src-d/go-mysql-server
Pull request #784 (2019-07-03 19:38:33)

Zachmu/bug fixes to contribute
Several bug fixes and improvements:

1) Float comparisons were broken because they were being truncated to integers. Fixed and added many tests with float values.
2) Negative float values were getting optimized away to 0 by the parser, fixed and added tests.
3) Added integration tests of IS NULL, IS NOT NULL
4) Implemented IS TRUE, IS FALSE, and NOT versions of each
5) Fixed some typos in the source
6) Allow conversion of string > bool to match MySQL semantics
7) Fixed bug where some non-column filter expressions were getting effectively optimized away by being force-converted to a boolean

zachmu: Sorry for the relatively messy diff (includes several recent changes from your master), not sure what caused it exactly. Open to suggestions for how to make it cleaner.
zachmu: Cleaned this up with the help of my colleague, ready to be reviewed.
juanjux: Only a couple easy to fix things, thanks for the PR!

Patch:
diff --git a/sql/analyzer/optimization_rules.go b/sql/analyzer/optimization_rules.go
index 9baa824..0037d9b 100644
--- a/sql/analyzer/optimization_rules.go
+++ b/sql/analyzer/optimization_rules.go
@@ -380,27 +380,20 @@ func evalFilter(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, error)
 					return e.Left, nil
 				}
 
+				return e, nil
+			case *expression.Literal, expression.Tuple:
 				return e, nil
 			default:
 				if !isEvaluable(e) {
 					return e, nil
 				}
 
-				if _, ok := e.(*expression.Literal); ok {
-					return e, nil
-				}
-
+				// All other expressions types can be evaluated once and turned into literals for the rest of query execution
 				val, err := e.Eval(ctx, nil)
 				if err != nil {
 					return e, nil
 				}
-
-				val, err = sql.Boolean.Convert(val)
-				if err != nil {
-					return e, nil
-				}
-
-				return expression.NewLiteral(val.(bool), sql.Boolean), nil
+				return expression.NewLiteral(val, e.Type()), nil
 			}
 		})
 		if err != nil {
diff --git a/sql/expression/istrue.go b/sql/expression/istrue.go
new file mode 100644
index 0000000..ba3e0c7
--- /dev/null
+++ b/sql/expression/istrue.go
@@ -0,0 +1,78 @@
+package expression
+
+import (
+	"errors"
+	"github.com/src-d/go-mysql-server/sql"
+)
+
+// IsTrue is an expression that checks if an expression is true.
+type IsTrue struct {
+	UnaryExpression
+	invert bool
+}
+
+const IsTrueStr = "IS TRUE"
+const IsFalseStr = "IS FALSE"
+
+// NewIsTrue creates a new IsTrue expression.
+func NewIsTrue(child sql.Expression) *IsTrue {
+	return &IsTrue{UnaryExpression: UnaryExpression{child}}
+}
+
+// NewIsFalse creates a new IsTrue expression with its boolean sense inverted (IsFalse, effectively).
+func NewIsFalse(child sql.Expression) *IsTrue {
+	return &IsTrue{UnaryExpression: UnaryExpression{child}, invert: true}
+}
+
+// Type implements the Expression interface.
+func (*IsTrue) Type() sql.Type {
+	return sql.Boolean
+}
+
+// IsNullable implements the Expression interface.
+func (*IsTrue) IsNullable() bool {
+	return false
+}
+
+// Eval implements the Expression interface.
+func (e *IsTrue) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
+	v, err := e.Child.Eval(ctx, row)
+	if err != nil {
+		return nil, err
+	}
+
+	var boolVal interface{}
+	if v == nil {
+		return false, nil
+	} else {
+		boolVal, err = sql.Boolean.Convert(v)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if e.invert {
+		return !boolVal.(bool), nil
+	}
+	return boolVal, nil
+}
+
+func (e *IsTrue) String() string {
+	isStr := IsTrueStr
+	if e.invert {
+		isStr = IsFalseStr
+	}
+	return e.Child.String() + " " + isStr
+}
+
+// WithChildren implements the Expression interface.
+func (e *IsTrue) WithChildren(children ...sql.Expression) (sql.Expression, error) {
+	if len(children) != 1 {
+		return nil, errors.New("incorrect number of children")
+	}
+
+	if e.invert {
+		return NewIsFalse(children[0]), nil
+	}
+	return NewIsTrue(children[0]), nil
+}
diff --git a/sql/parse/parse.go b/sql/parse/parse.go
index 407ca05..53da95a 100644
--- a/sql/parse/parse.go
+++ b/sql/parse/parse.go
@@ -951,6 +951,14 @@ func isExprToExpression(c *sqlparser.IsExpr) (sql.Expression, error) {
 		return expression.NewIsNull(e), nil
 	case sqlparser.IsNotNullStr:
 		return expression.NewNot(expression.NewIsNull(e)), nil
+	case sqlparser.IsTrueStr:
+		return expression.NewIsTrue(e), nil
+	case sqlparser.IsFalseStr:
+		return expression.NewIsFalse(e), nil
+	case sqlparser.IsNotTrueStr:
+		return expression.NewNot(expression.NewIsTrue(e)), nil
+	case sqlparser.IsNotFalseStr:
+		return expression.NewNot(expression.NewIsFalse(e)), nil
 	default:
 		return nil, ErrUnsupportedSyntax.New(c)
 	}
diff --git a/sql/type.go b/sql/type.go
index c2285c3..f8e47fe 100644
--- a/sql/type.go
+++ b/sql/type.go
@@ -183,15 +183,15 @@ var (
 	Uint16 = numberT{t: sqltypes.Uint16}
 	// Int32 is an integer of 32 bits.
 	Int32 = numberT{t: sqltypes.Int32}
+	// Uint32 is an unsigned integer of 32 bits.
+	Uint32 = numberT{t: sqltypes.Uint32}
 	// Int64 is an integer of 64 bytes.
 	Int64 = numberT{t: sqltypes.Int64}
-	// Uint32 is an unsigned integer of 32 bytes.
-	Uint32 = numberT{t: sqltypes.Uint32}
-	// Uint64 is an unsigned integer of 64 bytes.
+	// Uint64 is an unsigned integer of 64 bits.
 	Uint64 = numberT{t: sqltypes.Uint64}
-	// Float32 is a floating point number of 32 bytes.
+	// Float32 is a floating point number of 32 bits.
 	Float32 = numberT{t: sqltypes.Float32}
-	// Float64 is a floating point number of 64 bytes.
+	// Float64 is a floating point number of 64 bits.
 	Float64 = numberT{t: sqltypes.Float64}
 
 	// Timestamp is an UNIX timestamp.
@@ -363,15 +363,42 @@ func (t numberT) Convert(v interface{}) (interface{}, error) {
 // Compare implements Type interface.
 func (t numberT) Compare(a interface{}, b interface{}) (int, error) {
 	if IsUnsigned(t) {
-		return compareUnsigned(a, b)
+		// only int types are unsigned
+		return compareUnsignedInts(a, b)
 	}
 
-	return compareSigned(a, b)
+	switch t.t {
+	case sqltypes.Float64, sqltypes.Float32:
+		return compareFloats(a, b)
+	default:
+		return compareSignedInts(a, b)
+	}
 }
 
 func (t numberT) String() string { return t.t.String() }
 
-func compareSigned(a interface{}, b interface{}) (int, error) {
+func compareFloats(a interface{}, b interface{}) (int, error) {
+	ca, err := cast.ToFloat64E(a)
+	if err != nil {
+		return 0, err
+	}
+	cb, err := cast.ToFloat64E(b)
+	if err != nil {
+		return 0, err
+	}
+
+	if ca == cb {
+		return 0, nil
+	}
+
+	if ca < cb {
+		return -1, nil
+	}
+
+	return +1, nil
+}
+
+func compareSignedInts(a interface{}, b interface{}) (int, error) {
 	ca, err := cast.ToInt64E(a)
 	if err != nil {
 		return 0, err
@@ -392,7 +419,7 @@ func compareSigned(a interface{}, b interface{}) (int, error) {
 	return +1, nil
 }
 
-func compareUnsigned(a interface{}, b interface{}) (int, error) {
+func compareUnsignedInts(a interface{}, b interface{}) (int, error) {
 	ca, err := cast.ToUint64E(a)
 	if err != nil {
 		return 0, err
@@ -681,7 +708,7 @@ func (t booleanT) Convert(v interface{}) (interface{}, error) {
 	case float32, float64:
 		return int(math.Round(v.(float64))) != 0, nil
 	case string:
-		return false, fmt.Errorf("unable to cast string to bool")
+		return false, nil
 
 	case nil:
 		return nil, fmt.Errorf("unable to cast nil to bool")
@@ -965,7 +992,7 @@ func IsSigned(t Type) bool {
 
 // IsUnsigned checks if t is an unsigned type.
 func IsUnsigned(t Type) bool {
-	return t == Uint32 || t == Uint64
+	return t == Uint64 || t == Uint32
 }
 
 // IsInteger checks if t is a (U)Int32/64 type.


Test patch:
diff --git a/engine_test.go b/engine_test.go
index bf6a540..f50ab5f 100644
--- a/engine_test.go
+++ b/engine_test.go
@@ -32,14 +32,90 @@ var queries = []struct {
 		"SELECT i FROM mytable;",
 		[]sql.Row{{int64(1)}, {int64(2)}, {int64(3)}},
 	},
+	{
+		"SELECT i + 1 FROM mytable;",
+		[]sql.Row{{int64(2)}, {int64(3)}, {int64(4)}},
+	},
+	{
+		"SELECT -i FROM mytable;",
+		[]sql.Row{{int64(-1)}, {int64(-2)}, {int64(-3)}},
+	},
+	{
+		"SELECT i FROM mytable where -i = -2;",
+		[]sql.Row{{int64(2)}},
+	},
 	{
 		"SELECT i FROM mytable WHERE i = 2;",
 		[]sql.Row{{int64(2)}},
 	},
+	{
+		"SELECT i FROM mytable WHERE i > 2;",
+		[]sql.Row{{int64(3)}},
+	},
+	{
+		"SELECT i FROM mytable WHERE i < 2;",
+		[]sql.Row{{int64(1)}},
+	},
+	{
+		"SELECT i FROM mytable WHERE i <> 2;",
+		[]sql.Row{{int64(1)}, {int64(3)}},
+	},
+	{
+		"SELECT f32 FROM floattable WHERE f64 = 2.0;",
+		[]sql.Row{{float32(2.0)}},
+	},
+	{
+		"SELECT f32 FROM floattable WHERE f64 < 2.0;",
+		[]sql.Row{{float32(-1.0)}, {float32(-1.5)}, {float32(1.0)}, {float32(1.5)}},
+	},
+	{
+		"SELECT f32 FROM floattable WHERE f64 > 2.0;",
+		[]sql.Row{{float32(2.5)}},
+	},
+	{
+		"SELECT f32 FROM floattable WHERE f64 <> 2.0;",
+		[]sql.Row{{float32(-1.0)}, {float32(-1.5)}, {float32(1.0)}, {float32(1.5)}, {float32(2.5)}},
+	},
+	{
+		"SELECT f64 FROM floattable WHERE f32 = 2.0;",
+		[]sql.Row{{float64(2.0)}},
+	},
+	{
+		"SELECT f64 FROM floattable WHERE f32 = -1.5;",
+		[]sql.Row{{float64(-1.5)}},
+	},
+	{
+		"SELECT f64 FROM floattable WHERE -f32 = -2.0;",
+		[]sql.Row{{float64(2.0)}},
+	},
+	{
+		"SELECT f64 FROM floattable WHERE f32 < 2.0;",
+		[]sql.Row{{float64(-1.0)}, {float64(-1.5)}, {float64(1.0)}, {float64(1.5)}},
+	},
+	{
+		"SELECT f64 FROM floattable WHERE f32 > 2.0;",
+		[]sql.Row{{float64(2.5)}},
+	},
+	{
+		"SELECT f64 FROM floattable WHERE f32 <> 2.0;",
+		[]sql.Row{{float64(-1.0)}, {float64(-1.5)}, {float64(1.0)}, {float64(1.5)}, {float64(2.5)}},
+	},
+	{
+		"SELECT f32 FROM floattable ORDER BY f64;",
+		[]sql.Row{{float32(-1.5)}, {float32(-1.0)}, {float32(1.0)}, {float32(1.5)}, {float32(2.0)}, {float32(2.5)}},
+	},
 	{
 		"SELECT i FROM mytable ORDER BY i DESC;",
 		[]sql.Row{{int64(3)}, {int64(2)}, {int64(1)}},
 	},
+	{
+		"SELECT i FROM mytable WHERE 'hello';",
+		[]sql.Row{},
+	},
+	{
+		"SELECT i FROM mytable WHERE not 'hello';",
+		[]sql.Row{{int64(1)}, {int64(2)}, {int64(3)}},
+	},
 	{
 		"SELECT i FROM mytable WHERE s = 'first row' ORDER BY i DESC;",
 		[]sql.Row{{int64(1)}},
@@ -64,6 +140,38 @@ var queries = []struct {
 		"SELECT i FROM mytable ORDER BY i LIMIT 2,100;",
 		[]sql.Row{{int64(3)}},
 	},
+	{
+		"SELECT i FROM niltable WHERE b IS NULL",
+		[]sql.Row{{int64(2)}, {nil}},
+	},
+	{
+		"SELECT i FROM niltable WHERE b IS NOT NULL",
+		[]sql.Row{{int64(1)}, {nil}, {int64(4)}},
+	},
+	{
+		"SELECT i FROM niltable WHERE b",
+		[]sql.Row{{int64(1)}, {int64(4)}},
+	},
+	{
+		"SELECT i FROM niltable WHERE NOT b",
+		[]sql.Row{{nil}},
+	},
+	{
+		"SELECT i FROM niltable WHERE b IS TRUE",
+		[]sql.Row{{int64(1)}, {int64(4)}},
+	},
+	{
+		"SELECT i FROM niltable WHERE b IS NOT TRUE",
+		[]sql.Row{{int64(2)}, {nil}, {nil}},
+	},
+	{
+		"SELECT f FROM niltable WHERE b IS FALSE",
+		[]sql.Row{{3.0}},
+	},
+	{
+		"SELECT i FROM niltable WHERE b IS NOT FALSE",
+		[]sql.Row{{int64(1)}, {int64(2)}, {int64(4)}, {nil}},
+	},
 	{
 		"SELECT COUNT(*) FROM mytable;",
 		[]sql.Row{{int64(3)}},
@@ -593,6 +701,8 @@ var queries = []struct {
 			{"othertable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 			{"tabletest", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 			{"bigtable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
+			{"floattable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
+			{"niltable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 		},
 	},
 	{
@@ -601,6 +711,8 @@ var queries = []struct {
 			{"mytable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 			{"othertable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 			{"bigtable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
+			{"floattable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
+			{"niltable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 		},
 	},
 	{
@@ -741,6 +853,8 @@ var queries = []struct {
 			{"othertable"},
 			{"tabletest"},
 			{"bigtable"},
+			{"floattable"},
+			{"niltable"},
 		},
 	},
 	{
@@ -766,6 +880,10 @@ var queries = []struct {
 			{"i2"},
 			{"t"},
 			{"n"},
+			{"f32"},
+			{"f64"},
+			{"b"},
+			{"f"},
 		},
 	},
 	{
@@ -781,6 +899,10 @@ var queries = []struct {
 			{"i2"},
 			{"t"},
 			{"n"},
+			{"f32"},
+			{"f64"},
+			{"b"},
+			{"f"},
 		},
 	},
 	{
@@ -796,6 +918,10 @@ var queries = []struct {
 			{"i2"},
 			{"t"},
 			{"n"},
+			{"f32"},
+			{"f64"},
+			{"b"},
+			{"f"},
 		},
 	},
 	{
@@ -830,6 +956,8 @@ var queries = []struct {
 			{"othertable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 			{"tabletest", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 			{"bigtable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
+			{"floattable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
+			{"niltable", "InnoDB", "10", "Fixed", int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), nil, nil, nil, "utf8_bin", nil, nil},
 		},
 	},
 	{
@@ -941,6 +1069,8 @@ var queries = []struct {
 			{"othertable"},
 			{"tabletest"},
 			{"bigtable"},
+			{"floattable"},
+			{"niltable"},
 		},
 	},
 	{
@@ -950,6 +1080,8 @@ var queries = []struct {
 			{"othertable", "BASE TABLE"},
 			{"tabletest", "BASE TABLE"},
 			{"bigtable", "BASE TABLE"},
+			{"floattable", "BASE TABLE"},
+			{"niltable", "BASE TABLE"},
 		},
 	},
 	{
@@ -964,6 +1096,8 @@ var queries = []struct {
 			{"mytable"},
 			{"othertable"},
 			{"bigtable"},
+			{"floattable"},
+			{"niltable"},
 		},
 	},
 	{
@@ -1873,9 +2007,9 @@ func TestInnerNestedInNaturalJoins(t *testing.T) {
 
 	insertRows(
 		t, table3,
-		sql.NewRow(int32(1), float64(2.3), "table3"),
-		sql.NewRow(int32(2), float64(2.3), "table3"),
-		sql.NewRow(int32(30), float64(2.3), "table3"),
+		sql.NewRow(int32(1), float64(2.2), "table3"),
+		sql.NewRow(int32(2), float64(2.2), "table3"),
+		sql.NewRow(int32(30), float64(2.2), "table3"),
 	)
 
 	db := mem.NewDatabase("mydb")
@@ -2002,11 +2136,44 @@ func newEngineWithParallelism(t *testing.T, parallelism int) *sqle.Engine {
 		sql.NewRow("b", int64(9)),
 	)
 
+	floatTable := mem.NewPartitionedTable("floattable", sql.Schema{
+		{Name: "i", Type: sql.Int64, Source: "floattable"},
+		{Name: "f32", Type: sql.Float32, Source: "floattable"},
+		{Name: "f64", Type: sql.Float64, Source: "floattable"},
+	}, testNumPartitions)
+
+	insertRows(
+		t, floatTable,
+		sql.NewRow(1, float32(1.0), float64(1.0)),
+		sql.NewRow(2, float32(1.5), float64(1.5)),
+		sql.NewRow(3, float32(2.0), float64(2.0)),
+		sql.NewRow(4, float32(2.5), float64(2.5)),
+		sql.NewRow(-1, float32(-1.0), float64(-1.0)),
+		sql.NewRow(-2, float32(-1.5), float64(-1.5)),
+	)
+
+	nilTable := mem.NewPartitionedTable("niltable", sql.Schema{
+		{Name: "i", Type: sql.Int64, Source: "niltable", Nullable: true},
+		{Name: "b", Type: sql.Boolean, Source: "niltable", Nullable: true},
+		{Name: "f", Type: sql.Float64, Source: "niltable", Nullable: true},
+	}, testNumPartitions)
+
+	insertRows(
+		t, nilTable,
+		sql.NewRow(int64(1), true, float64(1.0)),
+		sql.NewRow(int64(2), nil, float64(2.0)),
+		sql.NewRow(nil, false, float64(3.0)),
+		sql.NewRow(int64(4), true, nil),
+		sql.NewRow(nil, nil, nil),
+	)
+
 	db := mem.NewDatabase("mydb")
 	db.AddTable("mytable", table)
 	db.AddTable("othertable", table2)
 	db.AddTable("tabletest", table3)
 	db.AddTable("bigtable", bigtable)
+	db.AddTable("floattable", floatTable)
+	db.AddTable("niltable", nilTable)
 
 	db2 := mem.NewDatabase("foo")
 	db2.AddTable("other_table", table4)
diff --git a/sql/expression/istrue_test.go b/sql/expression/istrue_test.go
new file mode 100644
index 0000000..83fe1e7
--- /dev/null
+++ b/sql/expression/istrue_test.go
@@ -0,0 +1,85 @@
+package expression
+
+import (
+	"github.com/src-d/go-mysql-server/sql"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestIsTrue(t *testing.T) {
+	require := require.New(t)
+
+	boolF := NewGetField(0, sql.Boolean, "col1", true)
+	e := NewIsTrue(boolF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(true, eval(t, e, sql.NewRow(true)))
+	require.Equal(false, eval(t, e, sql.NewRow(false)))
+
+	intF := NewGetField(0, sql.Int64, "col1", true)
+	e = NewIsTrue(intF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(true, eval(t, e, sql.NewRow(100)))
+	require.Equal(true, eval(t, e, sql.NewRow(-1)))
+	require.Equal(false, eval(t, e, sql.NewRow(0)))
+
+	floatF := NewGetField(0, sql.Float64, "col1", true)
+	e = NewIsTrue(floatF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(true, eval(t, e, sql.NewRow(1.5)))
+	require.Equal(true, eval(t, e, sql.NewRow(-1.5)))
+	require.Equal(false, eval(t, e, sql.NewRow(0)))
+
+	stringF := NewGetField(0, sql.Text, "col1", true)
+	e = NewIsTrue(stringF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(false, eval(t, e, sql.NewRow("")))
+	require.Equal(false, eval(t, e, sql.NewRow("false")))
+	require.Equal(false, eval(t, e, sql.NewRow("true")))
+}
+
+func TestIsFalse(t *testing.T) {
+	require := require.New(t)
+
+	boolF := NewGetField(0, sql.Boolean, "col1", true)
+	e := NewIsFalse(boolF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(false, eval(t, e, sql.NewRow(true)))
+	require.Equal(true, eval(t, e, sql.NewRow(false)))
+
+	intF := NewGetField(0, sql.Int64, "col1", true)
+	e = NewIsFalse(intF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(false, eval(t, e, sql.NewRow(100)))
+	require.Equal(false, eval(t, e, sql.NewRow(-1)))
+	require.Equal(true, eval(t, e, sql.NewRow(0)))
+
+	floatF := NewGetField(0, sql.Float64, "col1", true)
+	e = NewIsFalse(floatF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(false, eval(t, e, sql.NewRow(1.5)))
+	require.Equal(false, eval(t, e, sql.NewRow(-1.5)))
+	require.Equal(true, eval(t, e, sql.NewRow(0)))
+
+	stringF := NewGetField(0, sql.Text, "col1", true)
+	e = NewIsFalse(stringF)
+	require.Equal(sql.Boolean, e.Type())
+	require.False(e.IsNullable())
+	require.Equal(false, eval(t, e, sql.NewRow(nil)))
+	require.Equal(true, eval(t, e, sql.NewRow("")))
+	require.Equal(true, eval(t, e, sql.NewRow("false")))
+	require.Equal(true, eval(t, e, sql.NewRow("true")))
+}
\ No newline at end of file
diff --git a/sql/parse/parse_test.go b/sql/parse/parse_test.go
index 849f9cf..42f4123 100644
--- a/sql/parse/parse_test.go
+++ b/sql/parse/parse_test.go
@@ -62,6 +62,13 @@ var fixtures = map[string]sql.Node{
 		},
 		plan.NewUnresolvedTable("foo", ""),
 	),
+	`SELECT foo IS TRUE, bar IS NOT FALSE FROM foo;`: plan.NewProject(
+		[]sql.Expression{
+			expression.NewIsTrue(expression.NewUnresolvedColumn("foo")),
+			expression.NewNot(expression.NewIsFalse(expression.NewUnresolvedColumn("bar"))),
+		},
+		plan.NewUnresolvedTable("foo", ""),
+	),
 	`SELECT foo AS bar FROM foo;`: plan.NewProject(
 		[]sql.Expression{
 			expression.NewAlias(
diff --git a/sql/type_test.go b/sql/type_test.go
index e2c7c7c..10f3553 100644
--- a/sql/type_test.go
+++ b/sql/type_test.go
@@ -30,6 +30,22 @@ func TestText(t *testing.T) {
 	convert(t, Text, var3, "abc")
 }
 
+func TestBoolean(t *testing.T) {
+	convert(t, Boolean, "", false)
+	convert(t, Boolean, "true", false)
+	convert(t, Boolean, 0, false)
+	convert(t, Boolean, 1, true)
+	convert(t, Boolean, -1, true)
+	convert(t, Boolean, 0.0, false)
+	convert(t, Boolean, 0.4, false)
+	convert(t, Boolean, 0.5, true)
+	convert(t, Boolean, 1.0, true)
+	convert(t, Boolean, -1.0, true)
+
+	eq(t, Boolean, true, true)
+	eq(t, Boolean, false, false)
+}
+
 func TestInt32(t *testing.T) {
 	convert(t, Int32, int32(1), int32(1))
 	convert(t, Int32, 1, int32(1))
@@ -54,6 +70,74 @@ func TestNumberComparison(t *testing.T) {
 	gt(t, Uint32, int64(5), uint32(1))
 	gt(t, Uint32, uint32(5), int64(1))
 	lt(t, Uint32, uint64(1), int32(5))
+
+	eq(t, Uint8, uint8(255), uint8(255))
+	eq(t, Uint8, uint8(255), int32(255))
+	eq(t, Uint8, uint8(255), int64(255))
+	eq(t, Uint8, uint8(255), int64(255))
+	gt(t, Uint8, uint8(255), int32(1))
+	gt(t, Uint8, uint8(255), int64(1))
+	lt(t, Uint8, uint8(255), int16(256))
+
+	// Exhaustive numeric type equality test
+	type typeAndValue struct {
+		t numberT
+		v interface{}
+	}
+
+	allTypes := []typeAndValue{
+		{Int8, int8(42)},
+		{Uint8, uint8(42)},
+		{Int16, int16(42)},
+		{Uint16, uint16(42)},
+		{Int32, int32(42)},
+		{Uint32, uint32(42)},
+		{Int64, int64(42)},
+		{Uint64, uint64(42)},
+		{Float32, float32(42)},
+		{Float64, float64(42)},
+	}
+	for _, a := range allTypes {
+		for _, b := range allTypes {
+			eq(t, a.t, a.v, b.v)
+		}
+	}
+
+	// Float comparisons against other floats
+	greaterFloat := 7.5
+	lesserFloat := 7.4
+	gt(t, Float64, float64(greaterFloat), float64(lesserFloat))
+	lt(t, Float64, float64(lesserFloat), float64(greaterFloat))
+	eq(t, Float64, float64(greaterFloat), float64(greaterFloat))
+	gt(t, Float64, float64(greaterFloat), float32(lesserFloat))
+	lt(t, Float64, float64(lesserFloat), float32(greaterFloat))
+	eq(t, Float64, float64(greaterFloat), float32(greaterFloat))
+	gt(t, Float32, float32(greaterFloat), float32(lesserFloat))
+	lt(t, Float32, float32(lesserFloat), float32(greaterFloat))
+	eq(t, Float32, float32(greaterFloat), float32(greaterFloat))
+	gt(t, Float32, float32(greaterFloat), float64(lesserFloat))
+	lt(t, Float32, float32(lesserFloat), float64(greaterFloat))
+	eq(t, Float32, float32(greaterFloat), float64(greaterFloat))
+
+	// Float comparisons against other types, testing comparison and truncation (when an int type is the left side of a
+	// comparison with a float type)
+	lessInt := 7
+	floatComps := []typeAndValue{
+		{Int8, int8(lessInt)},
+		{Uint8, uint8(lessInt)},
+		{Int16, int16(lessInt)},
+		{Uint16, uint16(lessInt)},
+		{Int32, int32(lessInt)},
+		{Uint32, uint32(lessInt)},
+		{Int64, int64(lessInt)},
+		{Uint64, uint64(lessInt)},
+	}
+	for _, a := range floatComps {
+		gt(t, Float64, float64(greaterFloat), a.v)
+		eq(t, a.t, float64(greaterFloat), a.v)
+		gt(t, Float32, float32(greaterFloat), a.v)
+		eq(t, a.t, float32(greaterFloat), a.v)
+	}
 }
 
 func TestInt64(t *testing.T) {