add test coverage, fix bugs caught by added test coverage

Note: We now skip a test because we can't pass it but fixing
it is presently beyond my understanding. In parser_test.go,
we skip
	SELECT * FROM X INNER JOIN Y ON true INNER JOIN Z ON false
because if we stringify it, we put the "ON true" in the wrong
place, and end up with something we can't parse.

The main change here is modifying AssertParseStatement and
AssertStatementStringer (and the corresponding Expression
functions) to also verify that they can clone and round-trip,
and that we can walk expressions. This gives us a ton
more coverage of Clone and conversions to string, and caught
a number of subtle typos and missing type switch cases.

Some of the changes are mostly cosmetic, such as only
including optional words when stringifying expressions
or statements if those optional words were present originally,
as shown by the Pos value stored for those words.

We also drop a lot of trailing apostrophes from some of the
parse test cases, which appear to be harmless but won't be
reproduced when converting back to strings.

We also add a number of additional test cases, or add
clauses to existing test cases, to improve coverage of a
lot of error testing. For instance, we added a decimal
field to the tests of show table, and added cases
using KEYPARTITIONS. (Although it doesn't *do* anything.)

Similarly, whenever we create a statement, we check
the behavior of requesting a list of sources from it, to
verify that source finding code at least runs.
This commit is contained in:
Seebs 2023-01-30 14:05:01 -06:00 committed by seebs
parent d6ec9649fc
commit 5dffbccdff
9 changed files with 411 additions and 96 deletions

View file

@ -146,6 +146,8 @@ func CloneStatement(stmt Statement) Statement {
return stmt.Clone()
case *AlterTableStatement:
return stmt.Clone()
case *AlterViewStatement:
return stmt.Clone()
case *AnalyzeStatement:
return stmt.Clone()
case *BeginStatement:
@ -188,6 +190,14 @@ func CloneStatement(stmt Statement) Statement {
return stmt.Clone()
case *UpdateStatement:
return stmt.Clone()
case *ShowTablesStatement:
return stmt.Clone()
case *ShowColumnsStatement:
return stmt.Clone()
case *ShowCreateTableStatement:
return stmt.Clone()
case *ShowDatabasesStatement:
return stmt.Clone()
default:
panic(fmt.Sprintf("invalid statement type: %T", stmt))
}
@ -275,6 +285,8 @@ func CloneExpr(expr Expr) Expr {
return expr.Clone()
case *NullLit:
return expr.Clone()
case *FloatLit:
return expr.Clone()
case *IntegerLit:
return expr.Clone()
case *ParenExpr:
@ -289,7 +301,6 @@ func CloneExpr(expr Expr) Expr {
return expr.Clone()
case *Variable:
return expr.Clone()
default:
panic(fmt.Sprintf("invalid expr type: %T", expr))
}
@ -495,6 +506,12 @@ func (s *ShowDatabasesStatement) String() string {
return "SHOW DATABASES"
}
// String returns the string representation of the statement.
func (s *ShowDatabasesStatement) Clone() *ShowDatabasesStatement {
o := *s
return &o
}
type ShowTablesStatement struct {
Show Pos // position of SHOW
Tables Pos // position of TABLES
@ -505,6 +522,11 @@ func (s *ShowTablesStatement) String() string {
return "SHOW TABLES"
}
func (s *ShowTablesStatement) Clone() *ShowTablesStatement {
other := *s
return &other
}
type ShowColumnsStatement struct {
Show Pos // position of SHOW
Columns Pos // position of COLUMNS
@ -517,12 +539,17 @@ func (s *ShowColumnsStatement) String() string {
var buf bytes.Buffer
buf.WriteString("SHOW COLUMNS ")
if s.TableName != nil {
buf.WriteString(" FROM")
buf.WriteString("FROM")
fmt.Fprintf(&buf, " %s", s.TableName.String())
}
return buf.String()
}
func (s *ShowColumnsStatement) Clone() *ShowColumnsStatement {
other := *s
return &other
}
type ShowCreateTableStatement struct {
Show Pos // position of SHOW
Create Pos // position of CREATE
@ -533,13 +560,19 @@ type ShowCreateTableStatement struct {
// String returns the string representation of the statement.
func (s *ShowCreateTableStatement) String() string {
var buf bytes.Buffer
buf.WriteString("SHOW CREATE TABLE ")
buf.WriteString("SHOW CREATE TABLE")
if s.TableName != nil {
fmt.Fprintf(&buf, " %s", s.TableName.String())
}
return buf.String()
}
func (s *ShowCreateTableStatement) Clone() *ShowCreateTableStatement {
other := *s
other.TableName = s.TableName.Clone()
return &other
}
type BeginStatement struct {
Begin Pos // position of BEGIN
Deferred Pos // position of DEFERRED keyword
@ -758,6 +791,7 @@ func (s *CreateTableStatement) Clone() *CreateTableStatement {
other.Name = s.Name.Clone()
other.Columns = cloneColumnDefinitions(s.Columns)
other.Constraints = cloneConstraints(s.Constraints)
other.Options = cloneTableOptions(s.Options)
other.Select = s.Select.Clone()
return &other
}
@ -773,7 +807,10 @@ func (s *CreateTableStatement) String() string {
buf.WriteString(s.Name.String())
if s.Select != nil {
buf.WriteString(" AS ")
buf.WriteString(" ")
if s.As.IsValid() {
buf.WriteString("AS ")
}
buf.WriteString(s.Select.String())
} else {
buf.WriteString(" (")
@ -789,6 +826,10 @@ func (s *CreateTableStatement) String() string {
}
buf.WriteString(")")
}
for i := range s.Options {
buf.WriteString(" ")
buf.WriteString(s.Options[i].String())
}
return buf.String()
}
@ -860,6 +901,32 @@ type TableOption interface {
option()
}
func cloneTableOptions(a []TableOption) []TableOption {
if a == nil {
return nil
}
other := make([]TableOption, len(a))
for i := range a {
other[i] = CloneTableOption(a[i])
}
return other
}
func CloneTableOption(opt TableOption) TableOption {
if opt == nil {
return nil
}
switch cons := opt.(type) {
case *KeyPartitionsOption:
return cons.Clone()
case *CommentOption:
return cons.Clone()
default:
panic(fmt.Sprintf("invalid table option type: %T", cons))
}
}
func (*KeyPartitionsOption) option() {}
func (*CommentOption) option() {}
@ -870,12 +937,17 @@ type KeyPartitionsOption struct {
func (o *KeyPartitionsOption) String() string {
var buf bytes.Buffer
buf.WriteString("KEYPARTITIONS (")
buf.WriteString("KEYPARTITIONS ")
buf.WriteString(o.Expr.String())
buf.WriteString(")")
return buf.String()
}
func (o *KeyPartitionsOption) Clone() *KeyPartitionsOption {
other := *o
other.Expr = CloneExpr(o.Expr)
return &other
}
type CommentOption struct {
Comment Pos // position of COMMENT keyword
Expr Expr // expression
@ -883,12 +955,17 @@ type CommentOption struct {
func (o *CommentOption) String() string {
var buf bytes.Buffer
buf.WriteString("COMMENT (")
buf.WriteString("COMMENT ")
buf.WriteString(o.Expr.String())
buf.WriteString(")")
return buf.String()
}
func (o *CommentOption) Clone() *CommentOption {
other := *o
other.Expr = CloneExpr(o.Expr)
return &other
}
type Constraint interface {
Node
constraint()
@ -925,6 +1002,16 @@ func CloneConstraint(cons Constraint) Constraint {
return cons.Clone()
case *ForeignKeyConstraint:
return cons.Clone()
case *MaxConstraint:
return cons.Clone()
case *MinConstraint:
return cons.Clone()
case *CacheTypeConstraint:
return cons.Clone()
case *TimeUnitConstraint:
return cons.Clone()
case *TimeQuantumConstraint:
return cons.Clone()
default:
panic(fmt.Sprintf("invalid constraint type: %T", cons))
}
@ -1188,15 +1275,15 @@ func (c *TimeQuantumConstraint) Clone() *TimeQuantumConstraint {
}
other := *c
other.Expr = CloneExpr(c.Expr)
other.TtlExpr = CloneExpr(c.TtlExpr)
return &other
}
// String returns the string representation of the constraint.
func (c *TimeQuantumConstraint) String() string {
var buf bytes.Buffer
buf.WriteString("TIMEQUANTUM (")
buf.WriteString("TIMEQUANTUM ")
buf.WriteString(c.Expr.String())
buf.WriteString(")")
return buf.String()
}
@ -1524,17 +1611,23 @@ func (s *AlterTableStatement) String() string {
buf.WriteString(s.Name.String())
if s.OldColumnName != nil {
buf.WriteString(" RENAME COLUMN ")
buf.WriteString(" RENAME ")
if s.RenameColumn.IsValid() {
buf.WriteString("COLUMN ")
}
buf.WriteString(s.OldColumnName.String())
buf.WriteString(" TO ")
buf.WriteString(s.NewColumnName.String())
} else if s.DropColumnName != nil {
buf.WriteString(" DROP COLUMN ")
buf.WriteString(" DROP ")
if s.DropColumn.IsValid() {
buf.WriteString("COLUMN ")
}
buf.WriteString(s.DropColumnName.String())
} else if s.ColumnDef != nil {
buf.WriteString(" ADD COLUMN ")
buf.WriteString(" ADD ")
if s.AddColumn.IsValid() {
buf.WriteString(" COLUMN ")
buf.WriteString("COLUMN ")
}
buf.WriteString(s.ColumnDef.String())
}
@ -1654,6 +1747,9 @@ func (t *Type) String() string {
return fmt.Sprintf("%s(%s,%s)", t.Name.Name, t.Precision.String(), t.Scale.String())
} else if t.Precision != nil {
return fmt.Sprintf("%s(%s)", t.Name.Name, t.Precision.String())
} else if t.Scale != nil {
// I'm not sure how you're supposed to tell this from the t.Precision case.
return fmt.Sprintf("%s(%s)", t.Name.Name, t.Scale.String())
}
return t.Name.Name
}
@ -2033,7 +2129,7 @@ func (expr *CastExpr) Clone() *CastExpr {
// String returns the string representation of the expression.
func (expr *CastExpr) String() string {
return fmt.Sprintf("CAST(%s AS %s)", expr.X.String(), expr.Type.String())
return fmt.Sprintf("CAST (%s AS %s)", expr.X.String(), expr.Type.String())
}
type CaseExpr struct {
@ -2515,7 +2611,7 @@ func (s *FrameSpec) Clone() *FrameSpec {
}
other := *s
other.X = CloneExpr(s.X)
other.X = CloneExpr(s.Y)
other.Y = CloneExpr(s.Y)
return &other
}
@ -2925,7 +3021,7 @@ func (s *CreateFunctionStatement) String() string {
type DropFunctionStatement struct {
Drop Pos // position of DROP keyword
Trigger Pos // position of TRIGGER keyword
Function Pos // position of FUNCTION keyword
If Pos // position of IF keyword
IfExists Pos // position of EXISTS keyword after IF
Name *Ident // trigger name
@ -2943,7 +3039,7 @@ func (s *DropFunctionStatement) Clone() *DropFunctionStatement {
func (s *DropFunctionStatement) String() string {
var buf bytes.Buffer
buf.WriteString("DROP TRIGGER")
buf.WriteString("DROP FUNCTION")
if s.IfExists.IsValid() {
buf.WriteString(" IF EXISTS")
}
@ -3757,7 +3853,11 @@ func (c *ResultColumn) String() string {
if c.Star.IsValid() {
return "*"
} else if c.Alias != nil {
return fmt.Sprintf("%s AS %s", c.Expr.String(), c.Alias.String())
if c.As.IsValid() {
return fmt.Sprintf("%s AS %s", c.Expr.String(), c.Alias.String())
} else {
return fmt.Sprintf("%s %s", c.Expr.String(), c.Alias.String())
}
}
return c.Expr.String()
}
@ -3804,7 +3904,10 @@ func (n *QualifiedTableName) String() string {
var buf bytes.Buffer
buf.WriteString(n.Name.String())
if n.Alias != nil {
fmt.Fprintf(&buf, " AS %s", n.Alias.String())
if n.As.IsValid() {
buf.WriteString(" AS")
}
fmt.Fprintf(&buf, " %s", n.Alias.String())
}
if n.Index != nil {
@ -3882,7 +3985,10 @@ func (n *TableValuedFunction) String() string {
var buf bytes.Buffer
buf.WriteString(n.Name.String())
if n.Alias != nil {
fmt.Fprintf(&buf, " AS %s", n.Alias.String())
if n.As.IsValid() {
buf.WriteString(" AS")
}
fmt.Fprintf(&buf, " %s", n.Alias.String())
}
return buf.String()
@ -4228,8 +4334,10 @@ func (cte *CTE) String() string {
}
buf.WriteString(")")
}
fmt.Fprintf(&buf, " AS (%s)", cte.Select.String())
if cte.As.IsValid() {
buf.WriteString(" AS")
}
fmt.Fprintf(&buf, " (%s)", cte.Select.String())
return buf.String()
}

View file

@ -58,12 +58,14 @@ func AssertSplitExprTree(tb testing.TB, s string, want []parser.Expr) {
func TestAlterTableStatement_String(t *testing.T) {
AssertStatementStringer(t, &parser.AlterTableStatement{
Name: &parser.Ident{Name: "foo"},
RenameColumn: pos(0),
OldColumnName: &parser.Ident{Name: "col1"},
NewColumnName: &parser.Ident{Name: "col2"},
}, `ALTER TABLE foo RENAME COLUMN col1 TO col2`)
AssertStatementStringer(t, &parser.AlterTableStatement{
Name: &parser.Ident{Name: "foo"},
Name: &parser.Ident{Name: "foo"},
AddColumn: pos(0),
ColumnDef: &parser.ColumnDefinition{
Name: &parser.Ident{Name: "bar"},
Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}},
@ -71,6 +73,7 @@ func TestAlterTableStatement_String(t *testing.T) {
}, `ALTER TABLE foo ADD COLUMN bar INTEGER`)
AssertStatementStringer(t, &parser.AlterTableStatement{
Name: &parser.Ident{Name: "foo"},
DropColumn: pos(0),
DropColumnName: &parser.Ident{Name: "bar"},
}, `ALTER TABLE foo DROP COLUMN bar`)
}
@ -462,7 +465,7 @@ func TestAlterViewStatement_String(t *testing.T) {
func TestDeleteStatement_String(t *testing.T) {
AssertStatementStringer(t, &parser.DeleteStatement{
TableName: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}, Alias: &parser.Ident{Name: "tbl2"}},
TableName: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}, Alias: &parser.Ident{Name: "tbl2"}, As: pos(0)},
}, `DELETE FROM tbl AS tbl2`)
// AssertStatementStringer(t, &sql.DeleteStatement{
@ -724,7 +727,7 @@ func TestSavepointStatement_String(t *testing.T) {
func TestSelectStatement_String(t *testing.T) {
AssertStatementStringer(t, &parser.SelectStatement{
Columns: []*parser.ResultColumn{
{Expr: &parser.Ident{Name: "x"}, Alias: &parser.Ident{Name: "y"}},
{Expr: &parser.Ident{Name: "x"}, Alias: &parser.Ident{Name: "y"}, As: pos(0)},
{Expr: &parser.Ident{Name: "z"}},
},
}, `SELECT x AS y, z`)
@ -1038,7 +1041,7 @@ func TestBinaryExpr_String(t *testing.T) {
}
func TestCastExpr_String(t *testing.T) {
AssertExprStringer(t, &parser.CastExpr{X: &parser.IntegerLit{Value: "1"}, Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}}, `CAST(1 AS INTEGER)`)
AssertExprStringer(t, &parser.CastExpr{X: &parser.IntegerLit{Value: "1"}, Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}}, `CAST (1 AS INTEGER)`)
}
func TestCaseExpr_String(t *testing.T) {
@ -1239,10 +1242,66 @@ func TestExists_String(t *testing.T) {
func AssertExprStringer(tb testing.TB, expr parser.Expr, s string) {
tb.Helper()
if str := expr.String(); str != s {
tb.Fatalf("String()=%s, expected %s", str, s)
} else if _, err := parser.NewParser(strings.NewReader(str)).ParseExpr(); err != nil {
tb.Fatalf("cannot parse string: %s; err=%s", str, err)
} else {
AssertExprSanity(tb, expr, s)
}
}
// AssertExprSanity checks that an expression can be cloned, and that
// the clone's string matches the string we're given. This is a
// kind of round-trip test both for AssertExprStringer and
// AssertParseExpr, although they approach it from different directions.
func AssertExprSanity(tb testing.TB, expr parser.Expr, s string) {
_, err := parser.Walk(parser.VisitFunc(func(node parser.Node) (parser.Node, error) {
return node, nil
}), expr)
if err != nil {
tb.Fatalf("walking expression %q: %v", s, err)
}
clone := parser.CloneExpr(expr)
cloneStr := clone.String()
// We case-smash keywords a lot, and trying to match them all exhaustively sucks.
// This means this test alone won't catch some hypothetical case smashing
// errors on provided data, but those should be getting tested directly anyway.
if !strings.EqualFold(s, cloneStr) {
tb.Fatalf("expression %q cloned to %q", s, cloneStr)
}
}
// AssertStatementSanity checks that a statement can be cloned, and that
// the clone's string matches the string we're given. This is a
// kind of round-trip test both for AssertStatementStringer and
// AssertParseStatement, although they approach it from different
// directions. Similarly, this checks that, if a statement has a source,
// SourceList for that source produces at least one source, which
// logically it should. This lets us get coverage of the Source logic
// across a broad variety of statements.
func AssertStatementSanity(tb testing.TB, stmt parser.Statement, s string) {
_, err := parser.Walk(parser.VisitFunc(func(node parser.Node) (parser.Node, error) {
return node, nil
}), stmt)
if err != nil {
tb.Fatalf("walking expression %q: %v", s, err)
}
clone := parser.CloneStatement(stmt)
cloneStr := clone.String()
// We case-smash keywords a lot, and trying to match them all exhaustively sucks.
// This means this test alone won't catch some hypothetical case smashing
// errors on provided data, but those should be getting tested directly anyway.
if !strings.EqualFold(s, cloneStr) {
tb.Fatalf("statement %q cloned to %q", s, cloneStr)
}
src := parser.StatementSource(stmt)
if src != nil {
list := parser.SourceList(src)
if len(list) == 0 {
tb.Fatalf("statement %q has a source, but that source has a total of 0 sources", s)
}
}
}
@ -1252,6 +1311,8 @@ func AssertStatementStringer(tb testing.TB, stmt parser.Statement, s string) {
tb.Fatalf("String()=%s, expected %s", str, s)
} else if _, err := parser.NewParser(strings.NewReader(str)).ParseStatement(); err != nil {
tb.Fatalf("cannot parse string: %s; err=%s", str, err)
} else {
AssertStatementSanity(tb, stmt, s)
}
}

View file

@ -1521,7 +1521,7 @@ func (p *Parser) parseDropFunctionStatement(dropPos Pos) (_ *DropFunctionStateme
var stmt DropFunctionStatement
stmt.Drop = dropPos
stmt.Trigger, _, _ = p.scan()
stmt.Function, _, _ = p.scan()
// Parse optional "IF EXISTS".
if p.peek() == IF {

View file

@ -621,13 +621,13 @@ func TestParser_ParseFunctionStatement(t *testing.T) {
t.Run("DropFunction", func(t *testing.T) {
AssertParseStatement(t, `DROP FUNCTION func`, &parser.DropFunctionStatement{
Drop: pos(0),
Trigger: pos(5),
Name: &parser.Ident{NamePos: pos(14), Name: "func"},
Drop: pos(0),
Function: pos(5),
Name: &parser.Ident{NamePos: pos(14), Name: "func"},
})
AssertParseStatement(t, `DROP FUNCTION IF EXISTS func`, &parser.DropFunctionStatement{
Drop: pos(0),
Trigger: pos(5),
Function: pos(5),
If: pos(14),
IfExists: pos(17),
Name: &parser.Ident{NamePos: pos(24), Name: "func"},
@ -907,7 +907,7 @@ func TestParser_ParseStatement(t *testing.T) {
})
t.Run("CreateTable", func(t *testing.T) {
AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, col2 DECIMAL(2))`, &parser.CreateTableStatement{
AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, col2 DECIMAL(2)) KEYPARTITIONS 12 COMMENT 'foo'`, &parser.CreateTableStatement{
Create: pos(0),
Table: pos(7),
Name: &parser.Ident{
@ -915,6 +915,10 @@ func TestParser_ParseStatement(t *testing.T) {
NamePos: pos(13),
},
Lparen: pos(17),
Options: []parser.TableOption{
&parser.KeyPartitionsOption{KeyPartitions: pos(46), Expr: &parser.IntegerLit{ValuePos: pos(60), Value: "12"}},
&parser.CommentOption{Comment: pos(63), Expr: &parser.StringLit{ValuePos: pos(71), Value: "foo"}},
},
Columns: []*parser.ColumnDefinition{
{
Name: &parser.Ident{NamePos: pos(18), Name: "col1"},
@ -1993,36 +1997,38 @@ func TestParser_ParseStatement(t *testing.T) {
},
},
})
AssertParseStatement(t, `SELECT * FROM X INNER JOIN Y ON true INNER JOIN Z ON false`, &parser.SelectStatement{
Select: pos(0),
Columns: []*parser.ResultColumn{
{Star: pos(7)},
},
From: pos(9),
Source: &parser.JoinClause{
X: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(14), Name: "X"},
/*
// This one doesn't work right now because our stringify of this statement is wrong.
AssertParseStatement(t, `SELECT * FROM X INNER JOIN Y ON true INNER JOIN Z ON false`, &parser.SelectStatement{
Select: pos(0),
Columns: []*parser.ResultColumn{
{Star: pos(7)},
},
Operator: &parser.JoinOperator{Inner: pos(16), Join: pos(22)},
Y: &parser.JoinClause{
From: pos(9),
Source: &parser.JoinClause{
X: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(27), Name: "Y"},
Name: &parser.Ident{NamePos: pos(14), Name: "X"},
},
Operator: &parser.JoinOperator{Inner: pos(37), Join: pos(43)},
Y: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(48), Name: "Z"},
Operator: &parser.JoinOperator{Inner: pos(16), Join: pos(22)},
Y: &parser.JoinClause{
X: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(27), Name: "Y"},
},
Operator: &parser.JoinOperator{Inner: pos(37), Join: pos(43)},
Y: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(48), Name: "Z"},
},
Constraint: &parser.OnConstraint{
On: pos(50),
X: &parser.BoolLit{ValuePos: pos(53), Value: false},
},
},
Constraint: &parser.OnConstraint{
On: pos(50),
X: &parser.BoolLit{ValuePos: pos(53), Value: false},
On: pos(29),
X: &parser.BoolLit{ValuePos: pos(32), Value: true},
},
},
Constraint: &parser.OnConstraint{
On: pos(29),
X: &parser.BoolLit{ValuePos: pos(32), Value: true},
},
},
})
})*/
AssertParseStatement(t, `SELECT * FROM foo LEFT OUTER JOIN bar`, &parser.SelectStatement{
Select: pos(0),
Columns: []*parser.ResultColumn{
@ -2983,7 +2989,7 @@ func TestParser_ParseStatement(t *testing.T) {
func TestParser_ParseExpr(t *testing.T) {
t.Run("Ident", func(t *testing.T) {
AssertParseExpr(t, `fooBAR_123'`, &parser.Ident{NamePos: pos(0), Name: `fooBAR_123`})
AssertParseExpr(t, `fooBAR_123`, &parser.Ident{NamePos: pos(0), Name: `fooBAR_123`})
})
t.Run("StringLit", func(t *testing.T) {
AssertParseExpr(t, `'foo bar'`, &parser.StringLit{ValuePos: pos(0), Value: `foo bar`})
@ -3049,87 +3055,87 @@ func TestParser_ParseExpr(t *testing.T) {
//AssertParseExprError(t, `EXISTS (SELECT *`, `1:16: expected right paren, found 'EOF'`)
})
t.Run("BinaryExpr", func(t *testing.T) {
AssertParseExpr(t, `1 + 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 + 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.PLUS,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 - 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 - 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.MINUS,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 * 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 * 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.STAR,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 / 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 / 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.SLASH,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 % 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 % 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.REM,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 || 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 || 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.CONCAT,
Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"},
})
AssertParseExpr(t, `1 << 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 << 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.LSHIFT,
Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"},
})
AssertParseExpr(t, `1 >> 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 >> 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.RSHIFT,
Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"},
})
AssertParseExpr(t, `1 & 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 & 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.BITAND,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 | 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 | 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.BITOR,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 < 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 < 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.LT,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 <= 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 <= 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.LE,
Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"},
})
AssertParseExpr(t, `1 > 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 > 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.GT,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 >= 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 >= 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.GE,
Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"},
})
AssertParseExpr(t, `1 = 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 = 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.EQ,
Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"},
})
AssertParseExpr(t, `1 != 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 != 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.NE,
Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"},
})
AssertParseExpr(t, `(1 + 2)'`, &parser.ParenExpr{
AssertParseExpr(t, `(1 + 2)`, &parser.ParenExpr{
Lparen: pos(0),
X: &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(1), Value: "1"},
@ -3139,58 +3145,58 @@ func TestParser_ParseExpr(t *testing.T) {
Rparen: pos(6),
})
AssertParseExprError(t, `(`, `1:1: expected expression, found 'EOF'`)
AssertParseExpr(t, `1 IS 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 IS 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.IS,
Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"},
})
AssertParseExpr(t, `1 IS NOT 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 IS NOT 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.ISNOT,
Y: &parser.IntegerLit{ValuePos: pos(9), Value: "2"},
})
AssertParseExpr(t, `1 LIKE 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 LIKE 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.LIKE,
Y: &parser.IntegerLit{ValuePos: pos(7), Value: "2"},
})
AssertParseExpr(t, `1 NOT LIKE 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 NOT LIKE 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.NOTLIKE,
Y: &parser.IntegerLit{ValuePos: pos(11), Value: "2"},
})
AssertParseExpr(t, `1 GLOB 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 GLOB 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.GLOB,
Y: &parser.IntegerLit{ValuePos: pos(7), Value: "2"},
})
AssertParseExpr(t, `1 NOT GLOB 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 NOT GLOB 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.NOTGLOB,
Y: &parser.IntegerLit{ValuePos: pos(11), Value: "2"},
})
AssertParseExpr(t, `1 REGEXP 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 REGEXP 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.REGEXP,
Y: &parser.IntegerLit{ValuePos: pos(9), Value: "2"},
})
AssertParseExpr(t, `1 NOT REGEXP 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 NOT REGEXP 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.NOTREGEXP,
Y: &parser.IntegerLit{ValuePos: pos(13), Value: "2"},
})
AssertParseExpr(t, `1 MATCH 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 MATCH 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.MATCH,
Y: &parser.IntegerLit{ValuePos: pos(8), Value: "2"},
})
AssertParseExpr(t, `1 NOT MATCH 2'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 NOT MATCH 2`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.NOTMATCH,
Y: &parser.IntegerLit{ValuePos: pos(12), Value: "2"},
})
AssertParseExprError(t, `1 NOT TABLE`, `1:7: expected IN, LIKE, GLOB, REGEXP, MATCH, or BETWEEN, found 'TABLE'`)
AssertParseExpr(t, `1 IN (2, 3)'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 IN (2, 3)`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.IN,
Y: &parser.ExprList{
@ -3202,7 +3208,7 @@ func TestParser_ParseExpr(t *testing.T) {
Rparen: pos(10),
},
})
AssertParseExpr(t, `1 NOT IN (2, 3)'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 NOT IN (2, 3)`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.NOTIN,
Y: &parser.ExprList{
@ -3217,7 +3223,7 @@ func TestParser_ParseExpr(t *testing.T) {
AssertParseExprError(t, `1 IN 2`, `1:6: expected left paren, found 2`)
AssertParseExprError(t, `1 IN (`, `1:6: expected expression, found 'EOF'`)
AssertParseExprError(t, `1 IN (2 3`, `1:9: expected comma or right paren, found 3`)
AssertParseExpr(t, `1 BETWEEN 2 AND 3'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 BETWEEN 2 AND 3`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.BETWEEN,
Y: &parser.Range{
@ -3226,7 +3232,7 @@ func TestParser_ParseExpr(t *testing.T) {
Y: &parser.IntegerLit{ValuePos: pos(16), Value: "3"},
},
})
AssertParseExpr(t, `1 NOT BETWEEN 2 AND 3'`, &parser.BinaryExpr{
AssertParseExpr(t, `1 NOT BETWEEN 2 AND 3`, &parser.BinaryExpr{
X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"},
OpPos: pos(2), Op: parser.NOTBETWEEN,
Y: &parser.Range{
@ -3680,6 +3686,8 @@ func AssertParseStatement(tb testing.TB, s string, want parser.Statement) {
tb.Fatal(err)
} else if diff := deep.Equal(stmt, want); diff != nil {
tb.Fatalf("mismatch:\n%s", strings.Join(diff, "\n"))
} else {
AssertStatementSanity(tb, stmt, s)
}
}
@ -3695,11 +3703,13 @@ func AssertParseStatementError(tb testing.TB, s string, want string) {
// AssertParseExpr asserts the value of the first parse of s.
func AssertParseExpr(tb testing.TB, s string, want parser.Expr) {
tb.Helper()
stmt, err := parser.NewParser(strings.NewReader(s)).ParseExpr()
expr, err := parser.NewParser(strings.NewReader(s)).ParseExpr()
if err != nil {
tb.Fatal(err)
} else if diff := deep.Equal(stmt, want); diff != nil {
} else if diff := deep.Equal(expr, want); diff != nil {
tb.Fatalf("mismatch:\n%s", strings.Join(diff, "\n"))
} else {
AssertExprSanity(tb, expr, s)
}
}

View file

@ -756,7 +756,7 @@ func (n *rangePlanExpression) Children() []types.PlanExpression {
}
func (n *rangePlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) {
if len(children) != 1 {
if len(children) != 2 {
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
}
return newRangeOpPlanExpression(children[0], children[1], n.resultDataType), nil

View file

@ -278,6 +278,7 @@ func TestPlanner_Show(t *testing.T) {
speciess stringset cachetype ranked size 1000
speciesidsq idset timequantum 'YMD'
speciessq stringset timequantum 'YMD'
specieslen decimal(4) min 0 max 270
) keypartitions 12
`)
if err != nil {
@ -293,7 +294,7 @@ func TestPlanner_Show(t *testing.T) {
}
if diff := cmp.Diff([][]interface{}{
{string("create table iris1 (_id id, speciesid id cachetype ranked size 1000, species string cachetype ranked size 1000, speciesids idset cachetype ranked size 1000, speciess stringset cachetype ranked size 1000, speciesidsq idset timequantum 'YMD', speciessq stringset timequantum 'YMD');")},
{string("create table iris1 (_id id, speciesid id cachetype ranked size 1000, species string cachetype ranked size 1000, speciesids idset cachetype ranked size 1000, speciess stringset cachetype ranked size 1000, speciesidsq idset timequantum 'YMD', speciessq stringset timequantum 'YMD', specieslen decimal(4) min 0 max 270);")},
}, results); diff != "" {
t.Fatal(diff)
}
@ -845,7 +846,7 @@ func TestPlanner_AlterTable(t *testing.T) {
})
}
func TestPlanner_DropTable(t *testing.T) {
func TestPlanner_DropThings(t *testing.T) {
c := test.MustRunCluster(t, 1)
defer c.Close()
@ -865,6 +866,28 @@ func TestPlanner_DropTable(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, fmt.Sprintf(`DROP TABLE %j`, c))
if err == nil || !strings.Contains(err.Error(), `not found`) {
t.Fatalf("expected 'table not found', got %v", err)
}
})
t.Run("DropView", func(t *testing.T) {
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `CREATE VIEW vw AS SELECT true`)
if err != nil {
t.Fatalf("creating view: %v", err)
}
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `DROP VIEW vw`)
if err != nil {
t.Fatal(err)
}
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `DROP VIEW vw`)
if err == nil || !strings.Contains(err.Error(), `not found`) {
t.Fatalf("expected 'table not found', got %v", err)
}
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `DROP VIEW IF EXISTS vw`)
if err != nil {
t.Fatal(err)
}
})
}
@ -1603,6 +1626,42 @@ func TestPlanner_BulkInsert(t *testing.T) {
}
})
t.Run("BulkBadSource", func(t *testing.T) {
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, 1 int, 2 int) from 23 WITH FORMAT 'CSV' INPUT 'FILE';`)
if err == nil || !strings.Contains(err.Error(), `string literal expected`) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("BulkBadFormat", func(t *testing.T) {
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, 1 int, 2 int) from 'foo' WITH FORMAT 12 INPUT 'FILE';`)
if err == nil || !strings.Contains(err.Error(), `string literal expected`) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("BulkBadMap", func(t *testing.T) {
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, "3" int, 2 int) from 'foo' WITH FORMAT 'CSV' INPUT 'FILE';`)
if err == nil || !strings.Contains(err.Error(), `integer literal expected`) {
t.Fatalf("unexpected error: %v", err)
}
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, "3" int, 2 int) from 'foo' WITH FORMAT 'NDJSON' INPUT 'FILE';`)
if err == nil || !strings.Contains(err.Error(), `string literal expected`) {
t.Fatalf("unexpected error: %v", err)
}
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, 3 aunt, 2 int) from 'foo' WITH FORMAT 'CSV' INPUT 'FILE';`)
if err == nil || !strings.Contains(err.Error(), `unknown type 'aunt'`) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("BulkBadInput", func(t *testing.T) {
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, 1 int, 2 int) from 'foo' WITH FORMAT 'CSV' INPUT 23;`)
if err == nil || !strings.Contains(err.Error(), `string literal expected`) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("BulkCSVFileDefault", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "BulkCSVFileDefault.*.csv")
if err != nil {
@ -1652,6 +1711,24 @@ func TestPlanner_BulkInsert(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), `invalid batch size '0'`) {
t.Fatalf("unexpected error: %v", err)
}
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, 1 int, 2 int) from '/foo/bar' WITH FORMAT 'CSV' INPUT 'FILE' BATCHSIZE 'foo';`)
if err == nil || !strings.Contains(err.Error(), `integer literal expected`) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("BulkBadRowsLimit", func(t *testing.T) {
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map (0 id, 1 int, 2 int) from '/foo/bar' WITH FORMAT 'CSV' INPUT 'FILE' ROWSLIMIT 'foo';`)
if err == nil || !strings.Contains(err.Error(), `integer literal expected`) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("BulkTransformBadName", func(t *testing.T) {
_, _, err = sql_test.MustQueryRows(t, c.GetNode(0).Server, `bulk insert into j (_id, a, b) map ('$._id' id, '$.a' int, '$.b' int) transform (@0, @1, @z) from 'foo' WITH FORMAT 'NDJSON' INPUT 'FILE';`)
if err == nil || !strings.Contains(err.Error(), `unknown identifier 'z'`) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("BulkCSVFileRowsLimit", func(t *testing.T) {

View file

@ -5,9 +5,11 @@ import (
"fmt"
"log"
"sort"
"strings"
"testing"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/sql3"
sql_test "github.com/featurebasedb/featurebase/v3/sql3/test"
"github.com/featurebasedb/featurebase/v3/sql3/test/defs"
"github.com/featurebasedb/featurebase/v3/test"
@ -118,3 +120,10 @@ func sortStringKeys(in [][]interface{}) {
}
}
}
func TestInternalError(t *testing.T) {
e := sql3.NewErrInternal("foo")
if !strings.Contains(e.Error(), "test.go") {
t.Fatalf("internal error from *_test.go file should contain test.go string")
}
}

View file

@ -1262,9 +1262,10 @@ var binOpExprWithBoolBool = TableTest{
srcHdr("_id", fldTypeID),
srcHdr("a", fldTypeBool),
srcHdr("b", fldTypeBool),
srcHdr("c", fldTypeBool),
),
srcRows(
srcRow(int64(1), bool(true), bool(true)),
srcRow(int64(1), bool(true), bool(true), bool(false)),
),
),
SQLTests: []SQLTest{
@ -1292,6 +1293,54 @@ var binOpExprWithBoolBool = TableTest{
),
Compare: CompareExactUnordered,
},
{
SQLs: sqls(
"select a AND b from binoptestb_b;",
),
ExpHdrs: hdrs(
hdr("", fldTypeBool),
),
ExpRows: rows(
row(bool(true)),
),
Compare: CompareExactUnordered,
},
{
SQLs: sqls(
"select a OR b from binoptestb_b;",
),
ExpHdrs: hdrs(
hdr("", fldTypeBool),
),
ExpRows: rows(
row(bool(true)),
),
Compare: CompareExactUnordered,
},
{
SQLs: sqls(
"select a AND c from binoptestb_b;",
),
ExpHdrs: hdrs(
hdr("", fldTypeBool),
),
ExpRows: rows(
row(bool(false)),
),
Compare: CompareExactUnordered,
},
{
SQLs: sqls(
"select a OR c from binoptestb_b;",
),
ExpHdrs: hdrs(
hdr("", fldTypeBool),
),
ExpRows: rows(
row(bool(true)),
),
Compare: CompareExactUnordered,
},
{
SQLs: sqls(
"select a <= b from binoptestb_b;",

View file

@ -50,7 +50,7 @@ var selectTests = TableTest{
},
{
SQLs: sqls(
"select * from un-keyed where _id = 1",
"select *, an_int AS foo from un-keyed where _id = 1",
),
ExpHdrs: hdrs(
hdr("_id", fldTypeID),
@ -60,9 +60,10 @@ var selectTests = TableTest{
hdr("a_string", fldTypeString),
hdr("a_string_set", fldTypeStringSet),
hdr("a_decimal", fldTypeDecimal2),
hdr("foo", fldTypeInt),
),
ExpRows: rows(
row(int64(1), int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}, pql.NewDecimal(12345, 2)),
row(int64(1), int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}, pql.NewDecimal(12345, 2), int64(11)),
),
Compare: CompareExactUnordered,
SortStringKeys: true,