Repository: tomarrell/lbadd
Pull request #77 (2020-03-11 03:18:59)

Implements Begin, Commit, Rollback statements.
This PR completes the implementation of BEGIN, COMMIT and ROLLBACK statements (https://sqlite.org/lang_transaction.html) and is a part of #66


Patch:
diff --git a/internal/parser/simple_parser_rules.go b/internal/parser/simple_parser_rules.go
index a5e91f7..f6b269a 100644
--- a/internal/parser/simple_parser_rules.go
+++ b/internal/parser/simple_parser_rules.go
@@ -30,7 +30,7 @@ func (p *simpleParser) parseSQLStatement(r reporter) (stmt *ast.SQLStmt) {
 	}
 
 	// according to the grammar, these are the tokens that initiate a statement
-	p.searchNext(r, token.StatementSeparator, token.EOF, token.KeywordAlter, token.KeywordAnalyze, token.KeywordAttach, token.KeywordBegin, token.KeywordCommit, token.KeywordCreate, token.KeywordDelete, token.KeywordDetach, token.KeywordDrop, token.KeywordInsert, token.KeywordPragma, token.KeywordReindex, token.KeywordRelease, token.KeywordRollback, token.KeywordSavepoint, token.KeywordSelect, token.KeywordUpdate, token.KeywordVacuum)
+	p.searchNext(r, token.StatementSeparator, token.EOF, token.KeywordAlter, token.KeywordAnalyze, token.KeywordAttach, token.KeywordBegin, token.KeywordCommit, token.KeywordCreate, token.KeywordDelete, token.KeywordDetach, token.KeywordDrop, token.KeywordEnd, token.KeywordInsert, token.KeywordPragma, token.KeywordReindex, token.KeywordRelease, token.KeywordRollback, token.KeywordSavepoint, token.KeywordSelect, token.KeywordUpdate, token.KeywordVacuum)
 
 	next, ok := p.unsafeLowLevelLookahead()
 	if !ok {
@@ -42,14 +42,22 @@ func (p *simpleParser) parseSQLStatement(r reporter) (stmt *ast.SQLStmt) {
 	switch next.Type() {
 	case token.KeywordAlter:
 		stmt.AlterTableStmt = p.parseAlterTableStmt(r)
+	case token.KeywordAnalyze:
+		stmt.AnalyzeStmt = p.parseAnalyzeStmt(r)
 	case token.KeywordAttach:
 		stmt.AttachStmt = p.parseAttachDatabaseStmt(r)
+	case token.KeywordBegin:
+		stmt.BeginStmt = p.parseBeginStmt(r)
+	case token.KeywordCommit:
+		stmt.CommitStmt = p.parseCommitStmt(r)
 	case token.KeywordDetach:
 		stmt.DetachStmt = p.parseDetachDatabaseStmt(r)
+	case token.KeywordEnd:
+		stmt.CommitStmt = p.parseCommitStmt(r)
+	case token.KeywordRollback:
+		stmt.RollbackStmt = p.parseRollbackStmt(r)
 	case token.KeywordVacuum:
 		stmt.VacuumStmt = p.parseVacuumStmt(r)
-	case token.KeywordAnalyze:
-		stmt.AnalyzeStmt = p.parseAnalyzeStmt(r)
 	case token.StatementSeparator:
 		r.incompleteStatement()
 		p.consumeToken()
@@ -634,7 +642,7 @@ func (p *simpleParser) parseExpression(r reporter) (expr *ast.Expr) {
 	return
 }
 
-// parseAttachDatabaseStmt parses statments as defined in the spec:
+// parseAttachDatabaseStmt parses a single ATTACH statement as defined in the spec:
 // https://sqlite.org/lang_attach.html
 func (p *simpleParser) parseAttachDatabaseStmt(r reporter) (stmt *ast.AttachStmt) {
 	stmt = &ast.AttachStmt{}
@@ -650,7 +658,6 @@ func (p *simpleParser) parseAttachDatabaseStmt(r reporter) (stmt *ast.AttachStmt
 	if !ok {
 		return
 	}
-
 	if next.Type() == token.KeywordDatabase {
 		stmt.Database = next
 		p.consumeToken()
@@ -661,7 +668,6 @@ func (p *simpleParser) parseAttachDatabaseStmt(r reporter) (stmt *ast.AttachStmt
 	if !ok {
 		return
 	}
-
 	if next.Type() == token.KeywordAs {
 		stmt.As = next
 		p.consumeToken()
@@ -683,7 +689,7 @@ func (p *simpleParser) parseAttachDatabaseStmt(r reporter) (stmt *ast.AttachStmt
 	return
 }
 
-// parseDetachDatabaseStmt parses statements as defined in spec:
+// parseDetachDatabaseStmt parses a single DETACH statement as defined in spec:
 // https://sqlite.org/lang_detach.html
 func (p *simpleParser) parseDetachDatabaseStmt(r reporter) (stmt *ast.DetachStmt) {
 	stmt = &ast.DetachStmt{}
@@ -699,7 +705,6 @@ func (p *simpleParser) parseDetachDatabaseStmt(r reporter) (stmt *ast.DetachStmt
 	if !ok {
 		return
 	}
-
 	if next.Type() == token.KeywordDatabase {
 		stmt.Database = next
 		p.consumeToken()
@@ -718,7 +723,7 @@ func (p *simpleParser) parseDetachDatabaseStmt(r reporter) (stmt *ast.DetachStmt
 	return
 }
 
-// parseVacuumStmt parses the staments as defined in the spec:
+// parseVacuumStmt parses a single VACUUM statement as defined in the spec:
 // https://sqlite.org/lang_vacuum.html
 func (p *simpleParser) parseVacuumStmt(r reporter) (stmt *ast.VacuumStmt) {
 	stmt = &ast.VacuumStmt{}
@@ -739,7 +744,6 @@ func (p *simpleParser) parseVacuumStmt(r reporter) (stmt *ast.VacuumStmt) {
 	if !ok {
 		return
 	}
-
 	if next.Type() == token.Literal {
 		stmt.SchemaName = next
 		p.consumeToken()
@@ -749,7 +753,6 @@ func (p *simpleParser) parseVacuumStmt(r reporter) (stmt *ast.VacuumStmt) {
 	if !ok {
 		return
 	}
-
 	if next.Type() == token.KeywordInto {
 		stmt.Into = next
 		p.consumeToken()
@@ -767,7 +770,7 @@ func (p *simpleParser) parseVacuumStmt(r reporter) (stmt *ast.VacuumStmt) {
 	return
 }
 
-// parseAnalyzeStmt parses the statments as defined in the spec:
+// parseAnalyzeStmt parses a single ANALYZE statement as defined in the spec:
 // https://sqlite.org/lang_analyze.html
 func (p *simpleParser) parseAnalyzeStmt(r reporter) (stmt *ast.AnalyzeStmt) {
 	stmt = &ast.AnalyzeStmt{}
@@ -781,11 +784,10 @@ func (p *simpleParser) parseAnalyzeStmt(r reporter) (stmt *ast.AnalyzeStmt) {
 
 	// optionalLookahead is used, because ANALYZE alone is a valid statement
 	next, ok = p.optionalLookahead(r)
-	if next.Type() == token.EOF {
+	if !ok || next.Type() == token.EOF {
 		return
 	}
-
-	if !ok || next.Type() == token.Literal {
+	if next.Type() == token.Literal {
 		stmt.SchemaName = next
 		stmt.TableOrIndexName = next
 		p.consumeToken()
@@ -809,3 +811,121 @@ func (p *simpleParser) parseAnalyzeStmt(r reporter) (stmt *ast.AnalyzeStmt) {
 	p.consumeToken()
 	return
 }
+
+// parseBeginStmt parses a single BEGIN statement as defined in the spec:
+// https://sqlite.org/lang_transaction.html
+func (p *simpleParser) parseBeginStmt(r reporter) (stmt *ast.BeginStmt) {
+	stmt = &ast.BeginStmt{}
+	p.searchNext(r, token.KeywordBegin)
+	next, ok := p.lookahead(r)
+	if !ok {
+		return
+	}
+	stmt.Begin = next
+	p.consumeToken()
+
+	next, ok = p.optionalLookahead(r)
+	if !ok || next.Type() == token.EOF {
+		return
+	}
+	if next.Type() == token.KeywordDeferred {
+		stmt.Deferred = next
+		p.consumeToken()
+	} else if next.Type() == token.KeywordImmediate {
+		stmt.Immediate = next
+		p.consumeToken()
+	} else if next.Type() == token.KeywordExclusive {
+		stmt.Exclusive = next
+		p.consumeToken()
+	}
+
+	next, ok = p.optionalLookahead(r)
+	if !ok || next.Type() == token.EOF {
+		return
+	}
+	if next.Type() == token.KeywordTransaction {
+		stmt.Transaction = next
+		p.consumeToken()
+	}
+	return
+}
+
+// parseCommitStmt parses a single COMMIT statement as defined in the spec:
+// https://sqlite.org/lang_transaction.html
+func (p *simpleParser) parseCommitStmt(r reporter) (stmt *ast.CommitStmt) {
+	stmt = &ast.CommitStmt{}
+	p.searchNext(r, token.KeywordCommit, token.KeywordEnd)
+	next, ok := p.lookahead(r)
+	if !ok {
+		return
+	}
+	if next.Type() == token.KeywordCommit {
+		stmt.Commit = next
+	} else if next.Type() == token.KeywordEnd {
+		stmt.End = next
+	}
+	p.consumeToken()
+
+	next, ok = p.optionalLookahead(r)
+	if !ok || next.Type() == token.EOF {
+		return
+	}
+	if next.Type() == token.KeywordTransaction {
+		stmt.Transaction = next
+		p.consumeToken()
+	}
+	return
+}
+
+// parseRollbackStmt parses a single ROLLBACK statement as defined in the spec:
+// https://sqlite.org/lang_transaction.html
+func (p *simpleParser) parseRollbackStmt(r reporter) (stmt *ast.RollbackStmt) {
+	stmt = &ast.RollbackStmt{}
+	p.searchNext(r, token.KeywordRollback)
+	next, ok := p.lookahead(r)
+	if !ok {
+		return
+	}
+	stmt.Rollback = next
+	p.consumeToken()
+
+	next, ok = p.optionalLookahead(r)
+	if !ok || next.Type() == token.EOF {
+		return
+	}
+	if next.Type() == token.KeywordTransaction {
+		stmt.Transaction = next
+		p.consumeToken()
+	}
+
+	// If the keyword TRANSACTION exists in the statement, we need to
+	// check whether TO also exists. Out of TRANSACTION and TO, each not
+	// existing and existing, we have the following logic.
+	next, ok = p.optionalLookahead(r)
+	if !ok || next.Type() == token.EOF {
+		return
+	}
+	if next.Type() == token.KeywordTo {
+		stmt.To = next
+		p.consumeToken()
+	}
+
+	next, ok = p.lookahead(r)
+	if !ok {
+		return
+	}
+	if next.Type() == token.KeywordSavepoint {
+		stmt.Savepoint = next
+		p.consumeToken()
+	}
+
+	next, ok = p.lookahead(r)
+	if !ok {
+		return
+	}
+	if next.Type() == token.Literal {
+		stmt.SavepointName = next
+	}
+	p.consumeToken()
+	return
+}


Test patch:
diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go
index ac3dfb9..4c5b193 100644
--- a/internal/parser/parser_test.go
+++ b/internal/parser/parser_test.go
@@ -265,6 +265,193 @@ func TestSingleStatementParse(t *testing.T) {
 				},
 			},
 		},
+		{
+			"begin",
+			"BEGIN",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin: token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+				},
+			},
+		},
+		{
+			"BEGIN with DEFERRED",
+			"BEGIN DEFERRED",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin:    token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+					Deferred: token.New(1, 7, 6, 8, token.KeywordDeferred, "DEFERRED"),
+				},
+			},
+		},
+		{
+			"BEGIN with IMMEDIATE",
+			"BEGIN IMMEDIATE",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin:     token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+					Immediate: token.New(1, 7, 6, 9, token.KeywordImmediate, "IMMEDIATE"),
+				},
+			},
+		},
+		{
+			"BEGIN with EXCLUSIVE",
+			"BEGIN EXCLUSIVE",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin:     token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+					Exclusive: token.New(1, 7, 6, 9, token.KeywordExclusive, "EXCLUSIVE"),
+				},
+			},
+		},
+		{
+			"BEGIN with TRANSACTION",
+			"BEGIN TRANSACTION",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin:       token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+					Transaction: token.New(1, 7, 6, 11, token.KeywordTransaction, "TRANSACTION"),
+				},
+			},
+		},
+		{
+			"BEGIN with DEFERRED and TRANSACTION",
+			"BEGIN DEFERRED TRANSACTION",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin:       token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+					Deferred:    token.New(1, 7, 6, 8, token.KeywordDeferred, "DEFERRED"),
+					Transaction: token.New(1, 16, 15, 11, token.KeywordTransaction, "TRANSACTION"),
+				},
+			},
+		},
+		{
+			"BEGIN with IMMEDIATE and TRANSACTION",
+			"BEGIN IMMEDIATE TRANSACTION",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin:       token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+					Immediate:   token.New(1, 7, 6, 9, token.KeywordImmediate, "IMMEDIATE"),
+					Transaction: token.New(1, 17, 16, 11, token.KeywordTransaction, "TRANSACTION"),
+				},
+			},
+		},
+		{
+			"BEGIN with EXCLUSIVE and TRANSACTION",
+			"BEGIN EXCLUSIVE TRANSACTION",
+			&ast.SQLStmt{
+				BeginStmt: &ast.BeginStmt{
+					Begin:       token.New(1, 1, 0, 5, token.KeywordBegin, "BEGIN"),
+					Exclusive:   token.New(1, 7, 6, 9, token.KeywordExclusive, "EXCLUSIVE"),
+					Transaction: token.New(1, 17, 16, 11, token.KeywordTransaction, "TRANSACTION"),
+				},
+			},
+		},
+		{
+			"commit",
+			"COMMIT",
+			&ast.SQLStmt{
+				CommitStmt: &ast.CommitStmt{
+					Commit: token.New(1, 1, 0, 6, token.KeywordCommit, "COMMIT"),
+				},
+			},
+		},
+		{
+			"end",
+			"END",
+			&ast.SQLStmt{
+				CommitStmt: &ast.CommitStmt{
+					End: token.New(1, 1, 0, 3, token.KeywordEnd, "END"),
+				},
+			},
+		},
+		{
+			"COMMIT with TRANSACTION",
+			"COMMIT TRANSACTION",
+			&ast.SQLStmt{
+				CommitStmt: &ast.CommitStmt{
+					Commit:      token.New(1, 1, 0, 6, token.KeywordCommit, "COMMIT"),
+					Transaction: token.New(1, 8, 7, 11, token.KeywordTransaction, "TRANSACTION"),
+				},
+			},
+		},
+		{
+			"END with TRANSACTION",
+			"END TRANSACTION",
+			&ast.SQLStmt{
+				CommitStmt: &ast.CommitStmt{
+					End:         token.New(1, 1, 0, 3, token.KeywordEnd, "END"),
+					Transaction: token.New(1, 5, 4, 11, token.KeywordTransaction, "TRANSACTION"),
+				},
+			},
+		},
+		{
+			"rollback",
+			"ROLLBACK",
+			&ast.SQLStmt{
+				RollbackStmt: &ast.RollbackStmt{
+					Rollback: token.New(1, 1, 0, 8, token.KeywordRollback, "ROLLBACK"),
+				},
+			},
+		},
+		{
+			"ROLLBACK with TRANSACTION",
+			"ROLLBACK TRANSACTION",
+			&ast.SQLStmt{
+				RollbackStmt: &ast.RollbackStmt{
+					Rollback:    token.New(1, 1, 0, 8, token.KeywordRollback, "ROLLBACK"),
+					Transaction: token.New(1, 10, 9, 11, token.KeywordTransaction, "TRANSACTION"),
+				},
+			},
+		},
+		{
+			"ROLLBACK with TRANSACTION and TO",
+			"ROLLBACK TRANSACTION TO mySavePoint",
+			&ast.SQLStmt{
+				RollbackStmt: &ast.RollbackStmt{
+					Rollback:      token.New(1, 1, 0, 8, token.KeywordRollback, "ROLLBACK"),
+					Transaction:   token.New(1, 10, 9, 11, token.KeywordTransaction, "TRANSACTION"),
+					To:            token.New(1, 22, 21, 2, token.KeywordTo, "TO"),
+					SavepointName: token.New(1, 25, 24, 11, token.Literal, "mySavePoint"),
+				},
+			},
+		},
+		{
+			"ROLLBACK with TRANSACTION, TO and SAVEPOINT",
+			"ROLLBACK TRANSACTION TO SAVEPOINT mySavePoint",
+			&ast.SQLStmt{
+				RollbackStmt: &ast.RollbackStmt{
+					Rollback:      token.New(1, 1, 0, 8, token.KeywordRollback, "ROLLBACK"),
+					Transaction:   token.New(1, 10, 9, 11, token.KeywordTransaction, "TRANSACTION"),
+					To:            token.New(1, 22, 21, 2, token.KeywordTo, "TO"),
+					Savepoint:     token.New(1, 25, 24, 9, token.KeywordSavepoint, "SAVEPOINT"),
+					SavepointName: token.New(1, 35, 34, 11, token.Literal, "mySavePoint"),
+				},
+			},
+		},
+		{
+			"ROLLBACK with TO",
+			"ROLLBACK TO mySavePoint",
+			&ast.SQLStmt{
+				RollbackStmt: &ast.RollbackStmt{
+					Rollback:      token.New(1, 1, 0, 8, token.KeywordRollback, "ROLLBACK"),
+					To:            token.New(1, 10, 9, 2, token.KeywordTo, "TO"),
+					SavepointName: token.New(1, 13, 12, 11, token.Literal, "mySavePoint"),
+				},
+			},
+		},
+		{
+			"ROLLBACK with TO and SAVEPOINT",
+			"ROLLBACK TO SAVEPOINT mySavePoint",
+			&ast.SQLStmt{
+				RollbackStmt: &ast.RollbackStmt{
+					Rollback:      token.New(1, 1, 0, 8, token.KeywordRollback, "ROLLBACK"),
+					To:            token.New(1, 10, 9, 2, token.KeywordTo, "TO"),
+					Savepoint:     token.New(1, 13, 12, 9, token.KeywordSavepoint, "SAVEPOINT"),
+					SavepointName: token.New(1, 23, 22, 11, token.Literal, "mySavePoint"),
+				},
+			},
+		},
 	}
 	for _, input := range inputs {
 		t.Run(input.Name, func(t *testing.T) {