From 4e15c2e0a2ebb13400b652c4208bba9b9fe9d446 Mon Sep 17 00:00:00 2001 From: LissaGreense Date: Tue, 10 Dec 2024 23:27:42 +0100 Subject: [PATCH 1/3] Add apostrophe validation errors --- parser/errors.go | 18 +++++ parser/parser.go | 104 ++++++++++++++++----------- parser/parser_error_handling_test.go | 20 +++++- parser/parser_test.go | 20 +++--- 4 files changed, 113 insertions(+), 49 deletions(-) diff --git a/parser/errors.go b/parser/errors.go index dbd71e4..8bc04fd 100644 --- a/parser/errors.go +++ b/parser/errors.go @@ -107,3 +107,21 @@ type IllegalPeriodInIdentParserError struct { func (m *IllegalPeriodInIdentParserError) Error() string { return "syntax error, {" + m.name + "} shouldn't contain '.'" } + +// NoApostropheOnRightParserError - error thrown when parser found no apostrophe on right of ident +type NoApostropheOnRightParserError struct { + ident string +} + +func (m *NoApostropheOnRightParserError) Error() string { + return "syntax error, Identifier: {" + m.ident + "} has no apostrophe on right" +} + +// NoApostropheOnLeftParserError - error thrown when parser found no apostrophe on left of ident +type NoApostropheOnLeftParserError struct { + ident string +} + +func (m *NoApostropheOnLeftParserError) Error() string { + return "syntax error, Identifier: {" + m.ident + "} has no apostrophe on left" +} diff --git a/parser/parser.go b/parser/parser.go index 18a90f8..ce5e5d4 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -138,10 +138,12 @@ func (parser *Parser) parseCreateCommand() (ast.Command, error) { return createCommand, nil } -func (parser *Parser) skipIfCurrentTokenIsApostrophe() { +func (parser *Parser) skipIfCurrentTokenIsApostrophe() bool { if parser.currentToken.Type == token.APOSTROPHE { parser.nextToken() + return true } + return false } func (parser *Parser) skipIfCurrentTokenIsSemicolon() { @@ -184,17 +186,24 @@ func (parser *Parser) parseInsertCommand() (ast.Command, error) { } for parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { - parser.skipIfCurrentTokenIsApostrophe() + startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.LITERAL, token.NULL}) if err != nil { return nil, err } - insertCommand.Values = append(insertCommand.Values, parser.currentToken) + value := parser.currentToken + insertCommand.Values = append(insertCommand.Values, value) // Ignore token.IDENT, token.LITERAL or token.NULL parser.nextToken() - parser.skipIfCurrentTokenIsApostrophe() + finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + + err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, value) + + if err != nil { + return nil, err + } if parser.currentToken.Type != token.COMMA { break @@ -217,6 +226,15 @@ func (parser *Parser) parseInsertCommand() (ast.Command, error) { return insertCommand, nil } +func validateApostropheWrapping(startedWithApostrophe bool, finishedWithApostrophe bool, value token.Token) error { + if startedWithApostrophe && !finishedWithApostrophe { + return &NoApostropheOnRightParserError{ident: value.Literal} + } else if !startedWithApostrophe && finishedWithApostrophe { + return &NoApostropheOnLeftParserError{ident: value.Literal} + } + return nil +} + // parseSelectCommand - Return ast.SelectCommand created from tokens and validate the syntax // // Example of input parsable to the ast.SelectCommand: @@ -648,7 +666,7 @@ func (parser *Parser) parseUpdateCommand() (ast.Command, error) { // skip token.TO parser.nextToken() - parser.skipIfCurrentTokenIsApostrophe() + startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.LITERAL, token.NULL}) if err != nil { return nil, err @@ -657,7 +675,13 @@ func (parser *Parser) parseUpdateCommand() (ast.Command, error) { // skip token.IDENT, token.LITERAL or token.NULL parser.nextToken() - parser.skipIfCurrentTokenIsApostrophe() + finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + + err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, updateCommand.Changes[colKey].GetToken()) + + if err != nil { + return nil, err + } if parser.currentToken.Type != token.COMMA { break @@ -780,51 +804,45 @@ func (parser *Parser) getConditionalExpression() (bool, *ast.ConditionExpression } conditionalExpression.Condition = parser.peekToken - switch parser.currentToken.Type { - case token.IDENT: - conditionalExpression.Left = ast.Identifier{Token: parser.currentToken} - parser.nextToken() - case token.APOSTROPHE: - parser.skipIfCurrentTokenIsApostrophe() - conditionalExpression.Left = ast.Anonymitifier{Token: parser.currentToken} + if parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { + startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + + if !startedWithApostrophe && parser.currentToken.Type == token.IDENT { + conditionalExpression.Left = ast.Identifier{Token: parser.currentToken} + } else { + conditionalExpression.Left = ast.Anonymitifier{Token: parser.currentToken} + } parser.nextToken() - err := validateTokenAndSkip(parser, []token.Type{token.APOSTROPHE}) + + finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + err := validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, conditionalExpression.Left.GetToken()) if err != nil { return false, nil, err } - case token.NULL: - conditionalExpression.Left = ast.Anonymitifier{Token: parser.currentToken} - parser.nextToken() - case token.LITERAL: - conditionalExpression.Left = ast.Anonymitifier{Token: parser.currentToken} - parser.nextToken() - default: + } else { return false, conditionalExpression, nil } // skip EQUAL or NOT parser.nextToken() - switch parser.currentToken.Type { - case token.IDENT: - conditionalExpression.Right = ast.Identifier{Token: parser.currentToken} - parser.nextToken() - case token.APOSTROPHE: - parser.skipIfCurrentTokenIsApostrophe() - conditionalExpression.Right = ast.Anonymitifier{Token: parser.currentToken} + if parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { + startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + + if !startedWithApostrophe && parser.currentToken.Type == token.IDENT { + conditionalExpression.Right = ast.Identifier{Token: parser.currentToken} + } else { + conditionalExpression.Right = ast.Anonymitifier{Token: parser.currentToken} + } parser.nextToken() - err := validateTokenAndSkip(parser, []token.Type{token.APOSTROPHE}) + + finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, conditionalExpression.Right.GetToken()) if err != nil { return false, nil, err } - case token.NULL: - conditionalExpression.Right = ast.Anonymitifier{Token: parser.currentToken} - parser.nextToken() - case token.LITERAL: - conditionalExpression.Right = ast.Anonymitifier{Token: parser.currentToken} - parser.nextToken() - default: - return false, nil, &SyntaxError{expecting: []string{token.APOSTROPHE, token.IDENT, token.LITERAL, token.NULL}, got: parser.currentToken.Literal} + } else { + return false, conditionalExpression, &SyntaxError{expecting: []string{token.APOSTROPHE, token.IDENT, token.LITERAL, token.NULL}, got: parser.currentToken.Literal} } return true, conditionalExpression, nil @@ -861,17 +879,23 @@ func (parser *Parser) getContainExpression() (bool, *ast.ContainExpression, erro } for parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { - parser.skipIfCurrentTokenIsApostrophe() + startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.LITERAL, token.NULL}) if err != nil { return false, nil, err } - containExpression.Right = append(containExpression.Right, ast.Anonymitifier{Token: parser.currentToken}) + currentAnonymitifier := ast.Anonymitifier{Token: parser.currentToken} + containExpression.Right = append(containExpression.Right, currentAnonymitifier) // Ignore token.IDENT, token.LITERAL or token.NULL parser.nextToken() - parser.skipIfCurrentTokenIsApostrophe() + finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + + err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, currentAnonymitifier.GetToken()) + if err != nil { + return false, nil, err + } if parser.currentToken.Type != token.COMMA { if parser.currentToken.Type != token.RPAREN { diff --git a/parser/parser_error_handling_test.go b/parser/parser_error_handling_test.go index abb10d7..cfd17c0 100644 --- a/parser/parser_error_handling_test.go +++ b/parser/parser_error_handling_test.go @@ -56,6 +56,8 @@ func TestParseInsertCommandErrorHandling(t *testing.T) { noValue := SyntaxError{[]string{token.IDENT, token.LITERAL, token.NULL}, token.APOSTROPHE} noRightParen := SyntaxError{[]string{token.RPAREN}, token.SEMICOLON} noSemicolon := SyntaxError{[]string{token.SEMICOLON}, ""} + noLeftApostrophe := NoApostropheOnLeftParserError{ident: "hello"} + noRightApostrophe := NoApostropheOnRightParserError{ident: "hello, 10)"} tests := []errorHandlingTestSuite{ {"INSERT tbl VALUES( 'hello', 10);", noIntoKeyword.Error()}, @@ -64,6 +66,8 @@ func TestParseInsertCommandErrorHandling(t *testing.T) { {"INSERT INTO tl VALUES ('', 10);", noValue.Error()}, {"INSERT INTO tl VALUES ('hello', 10;", noRightParen.Error()}, {"INSERT INTO tl VALUES ('hello', 10)", noSemicolon.Error()}, + {"INSERT INTO tl VALUES (hello', 10)", noLeftApostrophe.Error()}, + {"INSERT INTO tl VALUES ('hello, 10)", noRightApostrophe.Error()}, } runParserErrorHandlingSuite(t, tests) @@ -78,6 +82,8 @@ func TestParseUpdateCommandErrorHandling(t *testing.T) { noSecondIdentOrLiteralForValue := SyntaxError{expecting: []string{token.IDENT, token.LITERAL, token.NULL}, got: token.SEMICOLON} noCommaBetweenValues := SyntaxError{expecting: []string{token.SEMICOLON, token.WHERE}, got: token.IDENT} noWhereOrSemicolon := SyntaxError{expecting: []string{token.SEMICOLON, token.WHERE}, got: token.SELECT} + noLeftApostrophe := NoApostropheOnLeftParserError{ident: "new_value_1"} + noRightApostrophe := NoApostropheOnRightParserError{ident: "new_value_1"} tests := []errorHandlingTestSuite{ {"UPDATE;", notableName.Error()}, @@ -87,6 +93,8 @@ func TestParseUpdateCommandErrorHandling(t *testing.T) { {"UPDATE table SET column_name_1 TO;", noSecondIdentOrLiteralForValue.Error()}, {"UPDATE table SET column_name_1 TO 2 column_name_1 TO 3;", noCommaBetweenValues.Error()}, {"UPDATE table SET column_name_1 TO 'new_value_1' SELECT;", noWhereOrSemicolon.Error()}, + {"UPDATE table SET column_name_1 TO new_value_1'", noLeftApostrophe.Error()}, + {"UPDATE table SET column_name_1 TO 'new_value_1", noRightApostrophe.Error()}, } runParserErrorHandlingSuite(t, tests) @@ -131,6 +139,10 @@ func TestParseWhereCommandErrorHandling(t *testing.T) { noLeftParGotNumber := SyntaxError{expecting: []string{token.LPAREN}, got: token.LITERAL} noComma := SyntaxError{expecting: []string{token.COMMA, token.RPAREN}, got: token.LITERAL} noInKeywordException := LogicalExpressionParsingError{} + noLeftApostropheGoodbye := NoApostropheOnLeftParserError{ident: "goodbye"} + noLeftApostropheFive := NoApostropheOnLeftParserError{ident: "5"} + noRightApostropheGoodbye := NoApostropheOnRightParserError{ident: "goodbye"} + noRightApostropheFive := NoApostropheOnRightParserError{ident: "5"} tests := []errorHandlingTestSuite{ {"WHERE col1 NOT 'goodbye' OR col2 EQUAL 3;", noPredecessorError.Error()}, @@ -143,11 +155,17 @@ func TestParseWhereCommandErrorHandling(t *testing.T) { {selectCommandPrefix + "WHERE one IN ;", noLeftParGotSemicolon.Error()}, {selectCommandPrefix + "WHERE one IN 5;", noLeftParGotNumber.Error()}, {selectCommandPrefix + "WHERE one IN (5 6);", noComma.Error()}, + {selectCommandPrefix + "WHERE one IN ('5", noRightApostropheFive.Error()}, + {selectCommandPrefix + "WHERE one IN (5');", noLeftApostropheFive.Error()}, {selectCommandPrefix + "WHERE one (5, 6);", noInKeywordException.Error()}, + {selectCommandPrefix + "WHERE one EQUAL goodbye';", noLeftApostropheGoodbye.Error()}, + {selectCommandPrefix + "WHERE one EQUAL 'goodbye", noRightApostropheGoodbye.Error()}, + // TODO: Add after fix apostrophe on left side of condition + //{selectCommandPrefix + "WHERE 'goodbye EQUAL two", noRightApostropheGoodbye.Error()}, + //{selectCommandPrefix + "WHERE goodbye' EQUAL two", noLeftApostropheGoodbye.Error()}, } runParserErrorHandlingSuite(t, tests) - } func TestParseOrderByCommandErrorHandling(t *testing.T) { diff --git a/parser/parser_test.go b/parser/parser_test.go index ae46a99..c887986 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -214,6 +214,10 @@ func TestParseWhereCommand(t *testing.T) { input: "SELECT * FROM TBL WHERE colName5 EQUAL NULL;", expectedExpression: fifthExpression, }, + { + input: "SELECT * FROM TBL WHERE colName5 EQUAL NULL;", + expectedExpression: fifthExpression, + }, } for testIndex, tt := range tests { @@ -712,20 +716,20 @@ func TestParseUpdateCommand(t *testing.T) { }{ { input: "UPDATE tbl SET colName TO 5;", expectedTableName: "tbl", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, - }, + {Type: token.IDENT, Literal: "colName"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, + }, }, { input: "UPDATE tbl1 SET colName1 TO 'hi hello', colName2 TO 5;", expectedTableName: "tbl1", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.IDENT, Literal: "hi hello"}}, - {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, - }, + {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.IDENT, Literal: "hi hello"}}, + {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, + }, }, { input: "UPDATE tbl1 SET colName1 TO NULL, colName2 TO 'NULL';", expectedTableName: "tbl1", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.NULL, Literal: "NULL"}}, - {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "NULL"}}, - }, + {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.NULL, Literal: "NULL"}}, + {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "NULL"}}, + }, }, } From 16a3b0b8faf049c39942864a565cbaad4c0ebfd1 Mon Sep 17 00:00:00 2001 From: LissaGreense Date: Thu, 16 Jan 2025 22:15:30 +0100 Subject: [PATCH 2/3] refactor --- parser/parser_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/parser/parser_test.go b/parser/parser_test.go index c887986..564302c 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -716,20 +716,20 @@ func TestParseUpdateCommand(t *testing.T) { }{ { input: "UPDATE tbl SET colName TO 5;", expectedTableName: "tbl", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, - }, + {Type: token.IDENT, Literal: "colName"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, + }, }, { input: "UPDATE tbl1 SET colName1 TO 'hi hello', colName2 TO 5;", expectedTableName: "tbl1", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.IDENT, Literal: "hi hello"}}, - {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, - }, + {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.IDENT, Literal: "hi hello"}}, + {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, + }, }, { input: "UPDATE tbl1 SET colName1 TO NULL, colName2 TO 'NULL';", expectedTableName: "tbl1", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.NULL, Literal: "NULL"}}, - {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "NULL"}}, - }, + {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.NULL, Literal: "NULL"}}, + {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "NULL"}}, + }, }, } From a851761a8da1949a5d7552936fd96cd0a6ea9316 Mon Sep 17 00:00:00 2001 From: LissaGreense Date: Fri, 17 Jan 2025 00:54:45 +0100 Subject: [PATCH 3/3] Rewrite getExpression logic --- .../1_select_with_where_expected_output | 7 + e2e/test_files/1_select_with_where_test | 1 + parser/parser.go | 186 ++++++++---------- parser/parser_error_handling_test.go | 9 +- parser/parser_test.go | 25 ++- 5 files changed, 117 insertions(+), 111 deletions(-) diff --git a/e2e/expected_outputs/1_select_with_where_expected_output b/e2e/expected_outputs/1_select_with_where_expected_output index ba58bca..3a89f6a 100644 --- a/e2e/expected_outputs/1_select_with_where_expected_output +++ b/e2e/expected_outputs/1_select_with_where_expected_output @@ -33,3 +33,10 @@ Data Inserted | one | two | three | four | +-----+-----+-------+------+ +-----+-----+-------+------+ ++-----------+------+-------+------+ +| one | two | three | four | ++-----------+------+-------+------+ +| 'hello' | 1 | 11 | 'q' | +| 'goodbye' | 1 | 22 | 'w' | +| 'byebye' | NULL | 33 | 'e' | ++-----------+------+-------+------+ diff --git a/e2e/test_files/1_select_with_where_test b/e2e/test_files/1_select_with_where_test index cd8b6ce..00156cb 100644 --- a/e2e/test_files/1_select_with_where_test +++ b/e2e/test_files/1_select_with_where_test @@ -10,3 +10,4 @@ SELECT * FROM tbl WHERE one NOT 'goodbye' AND two EQUAL NULL; SELECT * FROM tbl WHERE one IN ('goodbye', 'byebye'); SELECT * FROM tbl WHERE one NOTIN ('goodbye', 'byebye'); SELECT * FROM tbl WHERE FALSE; +SELECT * FROM tbl WHERE 'colName1 EQUAL;' EQUAL 'colName1 EQUAL;'; diff --git a/parser/parser.go b/parser/parser.go index ce5e5d4..3cc7e0e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -707,126 +707,118 @@ func (parser *Parser) parseUpdateCommand() (ast.Command, error) { // - ast.ConditionExpression // - ast.ContainExpression func (parser *Parser) getExpression() (bool, ast.Expression, error) { - booleanExpressionExists, booleanExpression := parser.getBooleanExpression() - conditionalExpressionExists, conditionalExpression, err := parser.getConditionalExpression() - if err != nil { - return false, nil, err - } + if parser.currentToken.Type == token.IDENT || + parser.currentToken.Type == token.LITERAL || + parser.currentToken.Type == token.NULL || + parser.currentToken.Type == token.APOSTROPHE || + parser.currentToken.Type == token.TRUE || + parser.currentToken.Type == token.FALSE { - containExpressionExists, containExpression, err := parser.getContainExpression() - if err != nil { - return false, nil, err - } + leftSide, isAnonymitifier, err := parser.getExpressionLeftSideValue() + if err != nil { + return false, nil, err + } - operationExpressionExists, operationExpression, err := parser.getOperationExpression(booleanExpressionExists, conditionalExpressionExists, containExpressionExists, booleanExpression, conditionalExpression, containExpression) - if err != nil { - return false, nil, err - } + isValidExpression := false + var expression ast.Expression + + if parser.currentToken.Type == token.EQUAL || parser.currentToken.Type == token.NOT { + isValidExpression, expression, err = parser.getConditionalExpression(leftSide, isAnonymitifier) + } else if parser.currentToken.Type == token.IN || parser.currentToken.Type == token.NOTIN { + isValidExpression, expression, err = parser.getContainExpression(leftSide, isAnonymitifier) + } else if leftSide.Type == token.TRUE || leftSide.Type == token.FALSE { + expression = &ast.BooleanExpression{Boolean: leftSide} + isValidExpression = true + err = nil + } - if operationExpressionExists { - return true, operationExpression, err - } + if err != nil { + return false, nil, err + } - if conditionalExpressionExists { - return true, conditionalExpression, err - } + if (parser.currentToken.Type == token.AND || parser.currentToken.Type == token.OR) && isValidExpression { + isValidExpression, expression, err = parser.getOperationExpression(expression) + } - if containExpressionExists { - return true, containExpression, err - } + if err != nil { + return false, nil, err + } - if booleanExpressionExists { - return true, booleanExpression, err + if isValidExpression { + return true, expression, nil + } } - - return false, nil, err + return false, nil, nil } -// getOperationExpression - Return ast.OperationExpression created from tokens and validate the syntax -func (parser *Parser) getOperationExpression(booleanExpressionExists bool, conditionalExpressionExists bool, containExpressionExists bool, booleanExpression *ast.BooleanExpression, conditionalExpression *ast.ConditionExpression, containExpression *ast.ContainExpression) (bool, *ast.OperationExpression, error) { - operationExpression := &ast.OperationExpression{} - - if (booleanExpressionExists || conditionalExpressionExists || containExpressionExists) && (parser.currentToken.Type == token.OR || parser.currentToken.Type == token.AND) { - if booleanExpressionExists { - operationExpression.Left = booleanExpression - } +func (parser *Parser) getExpressionLeftSideValue() (token.Token, bool, error) { + var leftSide token.Token + isAnonymitifier := false + startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() - if conditionalExpressionExists { - operationExpression.Left = conditionalExpression + if startedWithApostrophe { + isAnonymitifier = true + value := "" + for parser.currentToken.Type != token.EOF && parser.currentToken.Type != token.APOSTROPHE { + value += parser.currentToken.Literal + parser.nextToken() } - if containExpressionExists { - operationExpression.Left = containExpression - } + leftSide = token.Token{Type: token.IDENT, Literal: value} - operationExpression.Operation = parser.currentToken - parser.nextToken() + finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() - expressionIsValid, expression, err := parser.getExpression() + err := validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, leftSide) if err != nil { - return false, nil, err - } - if !expressionIsValid { - return false, nil, &LogicalExpressionParsingError{afterToken: &operationExpression.Operation.Literal} + return token.Token{}, isAnonymitifier, err } - - operationExpression.Right = expression - - return true, operationExpression, nil + } else { + leftSide = parser.currentToken + parser.nextToken() } - - return false, operationExpression, nil + return leftSide, isAnonymitifier, nil } -// getBooleanExpression - Return ast.BooleanExpression created from tokens and validate the syntax -func (parser *Parser) getBooleanExpression() (bool, *ast.BooleanExpression) { - booleanExpression := &ast.BooleanExpression{} - isValid := false +// getOperationExpression - Return ast.OperationExpression created from tokens and validate the syntax +func (parser *Parser) getOperationExpression(expression ast.Expression) (bool, *ast.OperationExpression, error) { + operationExpression := &ast.OperationExpression{} + operationExpression.Left = expression - if parser.currentToken.Type == token.TRUE || parser.currentToken.Type == token.FALSE { - booleanExpression.Boolean = parser.currentToken - parser.nextToken() - isValid = true - } + operationExpression.Operation = parser.currentToken + parser.nextToken() - return isValid, booleanExpression -} + expressionIsValid, expression, err := parser.getExpression() -// getConditionalExpression - Return ast.ConditionExpression created from tokens and validate the syntax -func (parser *Parser) getConditionalExpression() (bool, *ast.ConditionExpression, error) { - conditionalExpression := &ast.ConditionExpression{} - - err := validateToken(parser.peekToken.Type, []token.Type{token.EQUAL, token.NOT}) if err != nil { - return false, nil, nil + return false, nil, err } - conditionalExpression.Condition = parser.peekToken - if parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { - startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() + if !expressionIsValid { + return false, nil, &LogicalExpressionParsingError{afterToken: &operationExpression.Operation.Literal} + } - if !startedWithApostrophe && parser.currentToken.Type == token.IDENT { - conditionalExpression.Left = ast.Identifier{Token: parser.currentToken} - } else { - conditionalExpression.Left = ast.Anonymitifier{Token: parser.currentToken} - } - parser.nextToken() + operationExpression.Right = expression - finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() - err := validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, conditionalExpression.Left.GetToken()) - if err != nil { - return false, nil, err - } + return true, operationExpression, nil +} + +// getConditionalExpression - Return ast.ConditionExpression created from tokens and validate the syntax +func (parser *Parser) getConditionalExpression(leftSide token.Token, isAnonymitifier bool) (bool, *ast.ConditionExpression, error) { + conditionalExpression := &ast.ConditionExpression{Condition: parser.currentToken} + + if isAnonymitifier { + conditionalExpression.Left = ast.Anonymitifier{Token: leftSide} } else { - return false, conditionalExpression, nil + conditionalExpression.Left = ast.Identifier{Token: leftSide} } // skip EQUAL or NOT parser.nextToken() - if parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { + if parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || + parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() if !startedWithApostrophe && parser.currentToken.Type == token.IDENT { @@ -837,7 +829,7 @@ func (parser *Parser) getConditionalExpression() (bool, *ast.ConditionExpression parser.nextToken() finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() - err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, conditionalExpression.Right.GetToken()) + err := validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, conditionalExpression.Right.GetToken()) if err != nil { return false, nil, err } @@ -849,31 +841,25 @@ func (parser *Parser) getConditionalExpression() (bool, *ast.ConditionExpression } // getContainExpression - Return ast.ContainExpression created from tokens and validate the syntax -func (parser *Parser) getContainExpression() (bool, *ast.ContainExpression, error) { +func (parser *Parser) getContainExpression(leftSide token.Token, isAnonymitifier bool) (bool, *ast.ContainExpression, error) { containExpression := &ast.ContainExpression{} - err := validateToken(parser.peekToken.Type, []token.Type{token.IN, token.NOTIN}) - if err != nil { - return false, nil, nil + if isAnonymitifier { + return false, nil, &SyntaxError{expecting: []string{token.IDENT}, got: "'" + leftSide.Literal + "'"} } - if parser.peekToken.Type == token.IN { + + containExpression.Left = ast.Identifier{Token: leftSide} + + if parser.currentToken.Type == token.IN { containExpression.Contains = true } else { containExpression.Contains = false } - err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) - if err != nil { - return false, nil, nil - } - containExpression.Left = ast.Identifier{Token: parser.currentToken} - - parser.nextToken() - // skip IN or NOTIN parser.nextToken() - err = validateTokenAndSkip(parser, []token.Type{token.LPAREN}) + err := validateTokenAndSkip(parser, []token.Type{token.LPAREN}) if err != nil { return false, nil, err } diff --git a/parser/parser_error_handling_test.go b/parser/parser_error_handling_test.go index cfd17c0..a823169 100644 --- a/parser/parser_error_handling_test.go +++ b/parser/parser_error_handling_test.go @@ -129,6 +129,7 @@ func TestParseWhereCommandErrorHandling(t *testing.T) { selectCommandPrefix := "SELECT * FROM tbl " noPredecessorError := NoPredecessorParserError{command: token.WHERE} noColName := LogicalExpressionParsingError{} + noLeftAphostrophe := LogicalExpressionParsingError{} noOperatorInsideWhereStatementException := LogicalExpressionParsingError{} valueIsMissing := SyntaxError{expecting: []string{token.APOSTROPHE, token.IDENT, token.LITERAL, token.NULL}, got: token.SEMICOLON} tokenAnd := token.AND @@ -138,10 +139,12 @@ func TestParseWhereCommandErrorHandling(t *testing.T) { noLeftParGotSemicolon := SyntaxError{expecting: []string{token.LPAREN}, got: ";"} noLeftParGotNumber := SyntaxError{expecting: []string{token.LPAREN}, got: token.LITERAL} noComma := SyntaxError{expecting: []string{token.COMMA, token.RPAREN}, got: token.LITERAL} + anonymitifierInContains := SyntaxError{expecting: []string{token.IDENT}, got: "'one'"} noInKeywordException := LogicalExpressionParsingError{} noLeftApostropheGoodbye := NoApostropheOnLeftParserError{ident: "goodbye"} noLeftApostropheFive := NoApostropheOnLeftParserError{ident: "5"} noRightApostropheGoodbye := NoApostropheOnRightParserError{ident: "goodbye"} + noRightApostropheGoodbyeBigger := NoApostropheOnRightParserError{ident: "goodbye EQUAL two"} noRightApostropheFive := NoApostropheOnRightParserError{ident: "5"} tests := []errorHandlingTestSuite{ @@ -157,12 +160,12 @@ func TestParseWhereCommandErrorHandling(t *testing.T) { {selectCommandPrefix + "WHERE one IN (5 6);", noComma.Error()}, {selectCommandPrefix + "WHERE one IN ('5", noRightApostropheFive.Error()}, {selectCommandPrefix + "WHERE one IN (5');", noLeftApostropheFive.Error()}, + {selectCommandPrefix + "WHERE 'one' IN (5);", anonymitifierInContains.Error()}, {selectCommandPrefix + "WHERE one (5, 6);", noInKeywordException.Error()}, {selectCommandPrefix + "WHERE one EQUAL goodbye';", noLeftApostropheGoodbye.Error()}, {selectCommandPrefix + "WHERE one EQUAL 'goodbye", noRightApostropheGoodbye.Error()}, - // TODO: Add after fix apostrophe on left side of condition - //{selectCommandPrefix + "WHERE 'goodbye EQUAL two", noRightApostropheGoodbye.Error()}, - //{selectCommandPrefix + "WHERE goodbye' EQUAL two", noLeftApostropheGoodbye.Error()}, + {selectCommandPrefix + "WHERE 'goodbye EQUAL two", noRightApostropheGoodbyeBigger.Error()}, + {selectCommandPrefix + "WHERE goodbye' EQUAL two", noLeftAphostrophe.Error()}, } runParserErrorHandlingSuite(t, tests) diff --git a/parser/parser_test.go b/parser/parser_test.go index c887986..0823da4 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -716,20 +716,20 @@ func TestParseUpdateCommand(t *testing.T) { }{ { input: "UPDATE tbl SET colName TO 5;", expectedTableName: "tbl", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, - }, + {Type: token.IDENT, Literal: "colName"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, + }, }, { input: "UPDATE tbl1 SET colName1 TO 'hi hello', colName2 TO 5;", expectedTableName: "tbl1", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.IDENT, Literal: "hi hello"}}, - {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, - }, + {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.IDENT, Literal: "hi hello"}}, + {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "5"}}, + }, }, { input: "UPDATE tbl1 SET colName1 TO NULL, colName2 TO 'NULL';", expectedTableName: "tbl1", expectedChanges: map[token.Token]ast.Anonymitifier{ - {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.NULL, Literal: "NULL"}}, - {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "NULL"}}, - }, + {Type: token.IDENT, Literal: "colName1"}: {Token: token.Token{Type: token.NULL, Literal: "NULL"}}, + {Type: token.IDENT, Literal: "colName2"}: {Token: token.Token{Type: token.LITERAL, Literal: "NULL"}}, + }, }, } @@ -834,6 +834,11 @@ func TestParseLogicOperatorsInCommand(t *testing.T) { Boolean: token.Token{Type: token.TRUE, Literal: "TRUE"}, } + fourthExpression := ast.ConditionExpression{ + Left: ast.Anonymitifier{Token: token.Token{Type: token.IDENT, Literal: "colName1 EQUAL;"}}, + Right: ast.Anonymitifier{Token: token.Token{Type: token.IDENT, Literal: "colName1 EQUAL;"}}, + Condition: token.Token{Type: token.EQUAL, Literal: "EQUAL"}} + tests := []struct { input string expectedExpression ast.Expression @@ -850,6 +855,10 @@ func TestParseLogicOperatorsInCommand(t *testing.T) { input: "SELECT * FROM TBL WHERE TRUE;", expectedExpression: thirdExpression, }, + { + input: "SELECT * FROM TBL WHERE 'colName1 EQUAL;' EQUAL 'colName1 EQUAL;';", + expectedExpression: fourthExpression, + }, } for testIndex, tt := range tests {