From 3f7ae75e17233bdec5cdc7ffb69f87d93d539e03 Mon Sep 17 00:00:00 2001 From: Seebs Date: Tue, 21 Mar 2023 15:15:19 -0500 Subject: [PATCH] increase parser test coverage significantly We now test the tuple-assignment at all, although it's perhaps confusing because we expect a ()-list of columns to go with a {}-list of values. We also test a lot more errors and some more successes, and additional literal types in mustParseLiteral. --- sql3/parser/ast.go | 9 ++++- sql3/parser/parser.go | 17 ++++---- sql3/parser/parser_test.go | 80 +++++++++++++++++++++++++++++++++++++ sql3/parser/scanner_test.go | 6 +++ 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/sql3/parser/ast.go b/sql3/parser/ast.go index 8ee7377ed..587fd936c 100644 --- a/sql3/parser/ast.go +++ b/sql3/parser/ast.go @@ -299,6 +299,8 @@ func CloneExpr(expr Expr) Expr { return expr.Clone() case *StringLit: return expr.Clone() + case *TupleLiteralExpr: + return expr.Clone() case *UnaryExpr: return expr.Clone() case *Variable: @@ -1795,6 +1797,7 @@ func (t *Type) String() string { type StringLit struct { ValuePos Pos // literal position Value string // literal value (without quotes) + IsBlob bool // are we a blob? } func (expr *StringLit) IsLiteral() bool { return true } @@ -1831,7 +1834,11 @@ func (lit *StringLit) Clone() *StringLit { // String returns the string representation of the expression. func (lit *StringLit) String() string { - return `'` + strings.Replace(lit.Value, `'`, `''`, -1) + `'` + if lit.IsBlob { + return `x'` + strings.Replace(lit.Value, `'`, `''`, -1) + `'` + } else { + return `'` + strings.Replace(lit.Value, `'`, `''`, -1) + `'` + } } type IntegerLit struct { diff --git a/sql3/parser/parser.go b/sql3/parser/parser.go index 1427ab02f..dfc292f4b 100644 --- a/sql3/parser/parser.go +++ b/sql3/parser/parser.go @@ -414,6 +414,9 @@ func (p *Parser) parseCreateDatabaseStatement(createPos Pos) (_ *CreateDatabaseS if stmt.Options, err = p.parseDatabaseOptions(); err != nil { return &stmt, err } + if len(stmt.Options) == 0 { + return &stmt, p.errorExpected(stmt.With, p.peek(), "at least one option after WITH") + } } return &stmt, nil @@ -443,19 +446,17 @@ func (p *Parser) parseDatabaseOptions() (_ []DatabaseOption, err error) { func (p *Parser) parseDatabaseOption() (_ DatabaseOption, err error) { assert(isDatabaseOptionStartToken(p.peek())) - var optionPos Pos - // Parse database options. switch p.peek() { case UNITS: - return p.parseUnitsOption(optionPos) + return p.parseUnitsOption() default: assert(p.peek() == COMMENT) - return p.parseCommentOption(optionPos) + return p.parseCommentOption() } } -func (p *Parser) parseUnitsOption(optionPos Pos) (_ *UnitsOption, err error) { +func (p *Parser) parseUnitsOption() (_ *UnitsOption, err error) { assert(p.peek() == UNITS) var opt UnitsOption @@ -562,11 +563,11 @@ func (p *Parser) parseTableOption() (_ TableOption, err error) { return p.parseKeyPartitionsOption(optionPos) default: assert(p.peek() == COMMENT) - return p.parseCommentOption(optionPos) + return p.parseCommentOption() } } -func (p *Parser) parseCommentOption(optionPos Pos) (_ *CommentOption, err error) { +func (p *Parser) parseCommentOption() (_ *CommentOption, err error) { assert(p.peek() == COMMENT) var opt CommentOption @@ -2731,7 +2732,7 @@ func (p *Parser) mustParseLiteral() Expr { case TRUE, FALSE: return &BoolLit{ValuePos: pos, Value: tok == TRUE} case BLOB: - return &StringLit{ValuePos: pos, Value: lit} + return &StringLit{ValuePos: pos, IsBlob: true, Value: lit} default: assert(tok == NULL) return &NullLit{ValuePos: pos} diff --git a/sql3/parser/parser_test.go b/sql3/parser/parser_test.go index 90f58b495..5f422184e 100644 --- a/sql3/parser/parser_test.go +++ b/sql3/parser/parser_test.go @@ -460,6 +460,11 @@ func TestParser_ParseAlterStatement(t *testing.T) { AssertParseStatementError(t, `ALTER TABLE tbl ADD`, `1:19: expected COLUMN keyword or column name, found 'EOF'`) AssertParseStatementError(t, `ALTER TABLE tbl ADD COLUMN`, `1:26: expected column name, found 'EOF'`) }) + t.Run("AlterView", func(t *testing.T) { + AssertParseStatementError(t, `ALTER VIEW`, `1:10: expected view name, found 'EOF'`) + AssertParseStatementError(t, `ALTER VIEW vw 23`, `1:15: expected AS, found 23`) + AssertParseStatementError(t, `ALTER VIEW vw AS 23`, `1:18: expected SELECT, found 23`) + }) } func TestParser_ParseFunctionStatement(t *testing.T) { @@ -900,11 +905,63 @@ func TestParser_ParseStatement(t *testing.T) { }, }, }) + AssertParseStatement(t, `CREATE DATABASE db WITH COMMENT 'foo' COMMENT 23.5 COMMENT true COMMENT x'foo' COMMENT NULL`, &parser.CreateDatabaseStatement{ + Create: pos(0), + Database: pos(7), + Name: &parser.Ident{ + Name: "db", + NamePos: pos(16), + }, + With: pos(19), + Options: []parser.DatabaseOption{ + &parser.CommentOption{ + Comment: pos(24), + Expr: &parser.StringLit{ + ValuePos: pos(32), + Value: "foo", + }, + }, + &parser.CommentOption{ + Comment: pos(38), + Expr: &parser.FloatLit{ + ValuePos: pos(46), + Value: "23.5", + }, + }, + &parser.CommentOption{ + Comment: pos(51), + Expr: &parser.BoolLit{ + ValuePos: pos(59), + Value: true, + }, + }, + &parser.CommentOption{ + Comment: pos(64), + Expr: &parser.StringLit{ + IsBlob: true, + ValuePos: pos(72), + Value: "foo", + }, + }, + &parser.CommentOption{ + Comment: pos(79), + Expr: &parser.NullLit{ + ValuePos: pos(87), + }, + }, + }, + }) + AssertParseStatementError(t, `CREATE`, `1:1: expected DATABASE, TABLE, VIEW or FUNCTION`) AssertParseStatementError(t, `CREATE DATABASE`, `1:15: expected database name, found 'EOF'`) + AssertParseStatementError(t, `CREATE DATABASE IF`, `1:18: expected NOT, found 'EOF'`) + AssertParseStatementError(t, `CREATE DATABASE IF NOT`, `1:22: expected EXISTS, found 'EOF'`) AssertParseStatementError(t, `CREATE DATABASE db (`, `1:20: expected semicolon or EOF, found '('`) AssertParseStatementError(t, `CREATE DATABASE db extra`, `1:20: expected semicolon or EOF, found extra`) + AssertParseStatementError(t, `CREATE DATABASE db WITH`, `1:20: expected at least one option after WITH`) AssertParseStatementError(t, `CREATE DATABASE db WITH UNITS`, `1:29: expected literal, found 'EOF'`) + AssertParseStatementError(t, `CREATE DATABASE db WITH COMMENT`, `1:31: expected literal, found 'EOF'`) + }) t.Run("CreateTable", func(t *testing.T) { @@ -3769,6 +3826,29 @@ func TestParser_ParseStatement(t *testing.T) { }, }, }) + AssertParseStatement(t, `UPDATE tbl SET (x, y) = {1, 2}`, &parser.UpdateStatement{ + Update: pos(0), + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(7), Name: "tbl"}, + }, + Set: pos(11), + Assignments: []*parser.Assignment{ + { + Lparen: pos(15), + Rparen: pos(20), + Columns: []*parser.Ident{{NamePos: pos(16), Name: "x"}, {NamePos: pos(19), Name: "y"}}, + Eq: pos(22), + Expr: &parser.TupleLiteralExpr{ + Lbrace: pos(24), + Rbrace: pos(29), + Members: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(25), Value: "1"}, + &parser.IntegerLit{ValuePos: pos(28), Value: "2"}, + }, + }, + }, + }, + }) AssertParseStatement(t, `UPDATE tbl SET x = 1 WHERE y = 2`, &parser.UpdateStatement{ Update: pos(0), Table: &parser.QualifiedTableName{ diff --git a/sql3/parser/scanner_test.go b/sql3/parser/scanner_test.go index 1f07c51bf..05fe75eca 100644 --- a/sql3/parser/scanner_test.go +++ b/sql3/parser/scanner_test.go @@ -41,6 +41,9 @@ func TestScanner_Scan(t *testing.T) { t.Run("NoEndQuote", func(t *testing.T) { AssertScan(t, `'unfinished`, parser.ILLEGAL, `'unfinished`) }) + t.Run("NoEndQuoteNL", func(t *testing.T) { + AssertScan(t, "'unfinished\n", parser.UNTERMSTRING, `'unfinished`) + }) }) t.Run("BLOB", func(t *testing.T) { t.Run("LowerX", func(t *testing.T) { @@ -52,6 +55,9 @@ func TestScanner_Scan(t *testing.T) { t.Run("NoEndQuote", func(t *testing.T) { AssertScan(t, `x'0123`, parser.ILLEGAL, `x'0123`) }) + t.Run("QuotedQuote", func(t *testing.T) { + AssertScan(t, `x'01''23'`, parser.BLOB, `01'23`) + }) }) t.Run("INTEGER", func(t *testing.T) {