diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..1c2395b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,64 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['go'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ef4ac8b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,34 @@ +name: Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + tests: + name: Test code + runs-on: ubuntu-latest + strategy: + matrix: + go: + - ^1.14 + - ^1.15 + - ^1.16 + - ^1 + steps: + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + with: + fetch-depth: 200 + + - name: Test + run: go test ./... -v diff --git a/README.md b/README.md index e340217..6d47013 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -# sqlparser - meant for querying csv files -[![Build Status](https://img.shields.io/travis/marianogappa/sqlparser.svg)](https://travis-ci.org/marianogappa/sqlparser) [![Coverage Status](https://coveralls.io/repos/github/marianogappa/sqlparser/badge.svg?branch=master)](https://coveralls.io/github/MarianoGappa/sqlparser?branch=master) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/marianogappa/sqlparser/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/marianogappa/sqlparser?style=flat-square)](https://goreportcard.com/report/github.com/marianogappa/sqlparser) [![GoDoc](https://godoc.org/github.com/marianogappa/sqlparser?status.svg)](https://godoc.org/github.com/marianogappa/sqlparser) +# sqlparser - simple SQL parser + +Based on https://github.com/marianogappa/sqlparser + +Documentation on https://godoc.org/github.com/msaf1980/sqlparser ### Usage ``` @@ -9,7 +12,7 @@ import ( "fmt" "log" - "github.com/marianogappa/sqlparser" + "github.com/msaf1980/sqlparser" ) func main() { @@ -22,6 +25,21 @@ func main() { ``` +### Example: SELECT version() as version + +``` +query, err := sqlparser.Parse(`SELECT version() as version`) + +query.Query { + Type: Select + TableName: + Conditions: [] + Updates: map[] + Inserts: [] + Fields: [version()] +} +``` + ### Example: SELECT works ``` @@ -37,6 +55,36 @@ query.Query { } ``` +### Example: SELECT with alias works + +``` +query, err := sqlparser.Parse(`SELECT a AS text FROM 'b'`) + +query.Query { + Type: Select + TableName: b + Conditions: [] + Updates: map[] + Inserts: [] + Fields: [a] +} +``` + +### Example: SELECT with alias works + +``` +query, err := sqlparser.Parse(`SELECT version(a) AS version FROM 'b'`) + +query.Query { + Type: Select + TableName: b + Conditions: [] + Updates: map[] + Inserts: [] + Fields: [version(a)] +} +``` + ### Example: SELECT works with lowercase ``` @@ -78,10 +126,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: , - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -100,10 +148,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Lt, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -122,10 +170,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Lte, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -144,10 +192,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Gt, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -166,10 +214,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Gte, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -188,10 +236,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Ne, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -210,10 +258,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Ne, Operand2: b, - Operand2IsField: true, + Operand2Type: 1, }] Updates: map[] Inserts: [] @@ -262,17 +310,17 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Ne, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, } { Operand1: b, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: 2, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -291,10 +339,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[b:hello] Inserts: [] @@ -313,10 +361,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[b:hello\'world] Inserts: [] @@ -335,10 +383,10 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[b:hello c:bye] Inserts: [] @@ -357,17 +405,17 @@ query.Query { Conditions: [ { Operand1: a, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, } { Operand1: b, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: 789, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[b:hello c:bye] Inserts: [] @@ -386,10 +434,10 @@ query.Query { Conditions: [ { Operand1: b, - Operand1IsField: true, + Operand1Type: 1, Operator: Eq, Operand2: 1, - Operand2IsField: false, + Operand2Type: 2, }] Updates: map[] Inserts: [] @@ -476,6 +524,14 @@ query, err := sqlparser.Parse(`SELECT b, FROM 'a'`) at SELECT: expected field to SELECT ``` +### Example: SELECT with incomplete alias fails + +``` +query, err := sqlparser.Parse(`SELECT a AS`) + +at AS: expected alias for a +``` + ### Example: SELECT with empty WHERE fails ``` diff --git a/README.template b/README.template index 0e35490..95c295b 100644 --- a/README.template +++ b/README.template @@ -1,7 +1,10 @@ {{- $types := .Types -}} {{- $operators := .Operators -}} -# sqlparser - meant for querying csv files -[![Build Status](https://img.shields.io/travis/marianogappa/sqlparser.svg)](https://travis-ci.org/marianogappa/sqlparser) [![Coverage Status](https://coveralls.io/repos/github/marianogappa/sqlparser/badge.svg?branch=master)](https://coveralls.io/github/MarianoGappa/sqlparser?branch=master) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/marianogappa/sqlparser/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/marianogappa/sqlparser?style=flat-square)](https://goreportcard.com/report/github.com/marianogappa/sqlparser) [![GoDoc](https://godoc.org/github.com/marianogappa/sqlparser?status.svg)](https://godoc.org/github.com/marianogappa/sqlparser) +# sqlparser - simple SQL parser + +Based on https://github.com/marianogappa/sqlparser + +Documentation on https://godoc.org/github.com/msaf1980/sqlparser ### Usage ``` @@ -11,7 +14,7 @@ import ( "fmt" "log" - "github.com/marianogappa/sqlparser" + "github.com/msaf1980/sqlparser" ) func main() { @@ -35,10 +38,10 @@ query.Query { Conditions: [{{range .Expected.Conditions}} { Operand1: {{.Operand1}}, - Operand1IsField: {{.Operand1IsField}}, + Operand1Type: {{.Operand1Type}}, Operator: {{index $operators .Operator}}, Operand2: {{.Operand2}}, - Operand2IsField: {{.Operand2IsField}}, + Operand2Type: {{.Operand2Type}}, }{{end -}}] Updates: {{.Expected.Updates}} Inserts: {{.Expected.Inserts}} diff --git a/go.mod b/go.mod index b6e0dd3..f6e2d96 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/marianogappa/sqlparser +module github.com/msaf1980/sqlparser go 1.14 diff --git a/query/query.go b/query/query.go index 4cb28c4..61a476d 100644 --- a/query/query.go +++ b/query/query.go @@ -8,6 +8,7 @@ type Query struct { Updates map[string]string Inserts [][]string Fields []string // Used for SELECT (i.e. SELECTed field names) and INSERT (INSERTEDed field names) + Aliases []string // Used for SELECT (i.e. SELECTed field_name AS alias_name) } // Type is the type of SQL query, e.g. SELECT/UPDATE @@ -66,16 +67,25 @@ var OperatorString = []string{ "Lte", } +type OperandType int + +const ( + OpUnknown OperandType = iota + OpField + OpQuoted + OpNumber +) + // Condition is a single boolean condition in a WHERE clause type Condition struct { // Operand1 is the left hand side operand Operand1 string // Operand1IsField determines if Operand1 is a literal or a field name - Operand1IsField bool + Operand1Type OperandType // Operator is e.g. "=", ">" Operator Operator // Operand1 is the right hand side operand Operand2 string // Operand2IsField determines if Operand2 is a literal or a field name - Operand2IsField bool + Operand2Type OperandType } diff --git a/sql.go b/sql.go index 7da4ea9..0267896 100644 --- a/sql.go +++ b/sql.go @@ -2,19 +2,53 @@ package sqlparser import ( "fmt" - "regexp" + "io" "strings" - "github.com/marianogappa/sqlparser/query" + "github.com/msaf1980/sqlparser/query" ) -// Parse takes a string representing a SQL query and parses it into a query.Query struct. It may fail. -func Parse(sqls string) (query.Query, error) { - qs, err := ParseMany([]string{sqls}) - if len(qs) == 0 { - return query.Query{}, err +type ErrorWithPos struct { + msg string + pos int +} + +func newError(pos int, msg string) *ErrorWithPos { + return &ErrorWithPos{ + msg: msg, + pos: pos, + } +} + +func newErrorf(pos int, format string, a ...interface{}) *ErrorWithPos { + return &ErrorWithPos{ + msg: fmt.Sprintf(format, a...), + pos: pos, } - return qs[0], err +} + +func (e *ErrorWithPos) Error() string { + return e.msg +} + +func (e *ErrorWithPos) Pos() int { + return e.pos +} + +func (e *ErrorWithPos) PrintPosError(sql string, w io.Writer) { + fmt.Fprintln(w, sql) + fmt.Fprintln(w, strings.Repeat(" ", e.pos)+"^") + fmt.Println(e.msg) +} + +// Parse takes a string representing a SQL query and parses it into a query.Query struct. It may fail. +func Parse(sql string) (query.Query, error) { + sql = strings.TrimSpace(sql) + return (&parser{ + sql: sql, + sqlUpper: strings.ToUpper(sql), + step: stepType, + }).parse() } // ParseMany takes a string slice representing many SQL queries and parses them into a query.Query struct slice. @@ -22,7 +56,7 @@ func Parse(sqls string) (query.Query, error) { func ParseMany(sqls []string) ([]query.Query, error) { qs := []query.Query{} for _, sql := range sqls { - q, err := parse(sql) + q, err := Parse(sql) if err != nil { return qs, err } @@ -31,10 +65,6 @@ func ParseMany(sqls []string) ([]query.Query, error) { return qs, nil } -func parse(sql string) (query.Query, error) { - return (&parser{0, strings.TrimSpace(sql), stepType, query.Query{}, nil, ""}).parse() -} - type step int const ( @@ -68,7 +98,11 @@ const ( type parser struct { i int + len int + peeked string + peekQuoted bool sql string + sqlUpper string step step query query.Query err error @@ -81,7 +115,6 @@ func (p *parser) parse() (query.Query, error) { if p.err == nil { p.err = p.validate() } - p.logError() return q, p.err } @@ -92,209 +125,179 @@ func (p *parser) doParse() (query.Query, error) { } switch p.step { case stepType: - switch strings.ToUpper(p.peek()) { + s := p.peek(true) + switch s { case "SELECT": p.query.Type = query.Select - p.pop() p.step = stepSelectField - case "INSERT INTO": - p.query.Type = query.Insert + case "INSERT": p.pop() + s = p.peek(true) + if s != "INTO" { + return p.query, newErrorf(p.i, "at INSERT: expected INTO, got %s", s) + } + p.query.Type = query.Insert p.step = stepInsertTable case "UPDATE": p.query.Type = query.Update p.query.Updates = map[string]string{} - p.pop() p.step = stepUpdateTable - case "DELETE FROM": - p.query.Type = query.Delete + case "DELETE": p.pop() + s = p.peek(true) + if s != "FROM" { + return p.query, newErrorf(p.i, "at DELETE: expected FROM, got %s", s) + } + p.query.Type = query.Delete p.step = stepDeleteFromTable default: - return p.query, fmt.Errorf("invalid query type") + return p.query, newError(p.i, "invalid query type") } + p.pop() case stepSelectField: - identifier := p.peek() - if !isIdentifierOrAsterisk(identifier) { - return p.query, fmt.Errorf("at SELECT: expected field to SELECT") + identifier := p.peek(false) + if isId, _ := isIdentifierOrAsterisk(identifier); !isId { + return p.query, newError(p.i, "at SELECT: expected field to SELECT") } p.query.Fields = append(p.query.Fields, identifier) p.pop() - maybeFrom := p.peek() - if strings.ToUpper(maybeFrom) == "FROM" { + maybeFrom := p.peek(true) + if maybeFrom == "AS" { + // alias + p.pop() + alias := p.peek(false) + if isId, _ := isIdentifierOrAsterisk(alias); !isId { + return p.query, newErrorf(p.i, "at AS: expected alias for %s", identifier) + } + p.query.Aliases = append(p.query.Aliases, alias) + p.pop() + maybeFrom = p.peek(true) + } else { + p.query.Aliases = append(p.query.Aliases, "") + } + if maybeFrom == "FROM" { p.step = stepSelectFrom continue } p.step = stepSelectComma case stepSelectComma: - commaRWord := p.peek() + commaRWord := p.peek(false) if commaRWord != "," { - return p.query, fmt.Errorf("at SELECT: expected comma or FROM") + return p.query, newError(p.i, "at SELECT: expected comma or FROM") } p.pop() p.step = stepSelectField case stepSelectFrom: - fromRWord := p.peek() - if strings.ToUpper(fromRWord) != "FROM" { - return p.query, fmt.Errorf("at SELECT: expected FROM") + fromRWord := p.peek(true) + if fromRWord != "FROM" { + return p.query, newError(p.i, "at SELECT: expected FROM") } p.pop() p.step = stepSelectFromTable case stepSelectFromTable: - tableName := p.peek() + tableName := p.peek(false) if len(tableName) == 0 { - return p.query, fmt.Errorf("at SELECT: expected quoted table name") + return p.query, newError(p.i, "at SELECT: expected quoted table name") } p.query.TableName = tableName p.pop() p.step = stepWhere case stepInsertTable: - tableName := p.peek() + tableName := p.peek(false) if len(tableName) == 0 { - return p.query, fmt.Errorf("at INSERT INTO: expected quoted table name") + return p.query, newError(p.i, "at INSERT INTO: expected quoted table name") } p.query.TableName = tableName p.pop() p.step = stepInsertFieldsOpeningParens case stepDeleteFromTable: - tableName := p.peek() + tableName := p.peek(false) if len(tableName) == 0 { - return p.query, fmt.Errorf("at DELETE FROM: expected quoted table name") + return p.query, newError(p.i, "at DELETE FROM: expected quoted table name") } p.query.TableName = tableName p.pop() p.step = stepWhere case stepUpdateTable: - tableName := p.peek() + tableName := p.peek(false) if len(tableName) == 0 { - return p.query, fmt.Errorf("at UPDATE: expected quoted table name") + return p.query, newError(p.i, "at UPDATE: expected quoted table name") } p.query.TableName = tableName p.pop() p.step = stepUpdateSet case stepUpdateSet: - setRWord := p.peek() + setRWord := p.peek(true) if setRWord != "SET" { - return p.query, fmt.Errorf("at UPDATE: expected 'SET'") + return p.query, newError(p.i, "at UPDATE: expected 'SET'") } p.pop() p.step = stepUpdateField case stepUpdateField: - identifier := p.peek() - if !isIdentifier(identifier) { - return p.query, fmt.Errorf("at UPDATE: expected at least one field to update") + identifier := p.peek(false) + if isId, _ := isIdentifier(identifier); !isId { + return p.query, newError(p.i, "at UPDATE: expected at least one field to update") } p.nextUpdateField = identifier p.pop() p.step = stepUpdateEquals case stepUpdateEquals: - equalsRWord := p.peek() + equalsRWord := p.peek(false) if equalsRWord != "=" { - return p.query, fmt.Errorf("at UPDATE: expected '='") + return p.query, newError(p.i, "at UPDATE: expected '='") } p.pop() p.step = stepUpdateValue case stepUpdateValue: - quotedValue, ln := p.peekQuotedStringWithLength() - if ln == 0 { - return p.query, fmt.Errorf("at UPDATE: expected quoted value") + quotedValue := p.peekQuotedString(false) + if p.len == 0 { + return p.query, newError(p.i, "at UPDATE: expected quoted value") } p.query.Updates[p.nextUpdateField] = quotedValue p.nextUpdateField = "" p.pop() - maybeWhere := p.peek() - if strings.ToUpper(maybeWhere) == "WHERE" { + maybeWhere := p.peek(true) + if maybeWhere == "WHERE" { p.step = stepWhere continue } p.step = stepUpdateComma case stepUpdateComma: - commaRWord := p.peek() + commaRWord := p.peek(false) if commaRWord != "," { - return p.query, fmt.Errorf("at UPDATE: expected ','") + return p.query, newError(p.i, "at UPDATE: expected ','") } p.pop() p.step = stepUpdateField case stepWhere: - whereRWord := p.peek() - if strings.ToUpper(whereRWord) != "WHERE" { - return p.query, fmt.Errorf("expected WHERE") + whereRWord := p.peek(true) + if whereRWord != "WHERE" { + return p.query, newError(p.i, "expected WHERE") } p.pop() p.step = stepWhereField - case stepWhereField: - identifier := p.peek() - if !isIdentifier(identifier) { - return p.query, fmt.Errorf("at WHERE: expected field") + if ended, err := p.parseWhere(); ended || err != nil { + return p.query, err } - p.query.Conditions = append(p.query.Conditions, query.Condition{Operand1: identifier, Operand1IsField: true}) - p.pop() - p.step = stepWhereOperator - case stepWhereOperator: - operator := p.peek() - currentCondition := p.query.Conditions[len(p.query.Conditions)-1] - switch operator { - case "=": - currentCondition.Operator = query.Eq - case ">": - currentCondition.Operator = query.Gt - case ">=": - currentCondition.Operator = query.Gte - case "<": - currentCondition.Operator = query.Lt - case "<=": - currentCondition.Operator = query.Lte - case "!=": - currentCondition.Operator = query.Ne - default: - return p.query, fmt.Errorf("at WHERE: unknown operator") - } - p.query.Conditions[len(p.query.Conditions)-1] = currentCondition - p.pop() - p.step = stepWhereValue - case stepWhereValue: - currentCondition := p.query.Conditions[len(p.query.Conditions)-1] - identifier := p.peek() - if isIdentifier(identifier) { - currentCondition.Operand2 = identifier - currentCondition.Operand2IsField = true - } else { - quotedValue, ln := p.peekQuotedStringWithLength() - if ln == 0 { - return p.query, fmt.Errorf("at WHERE: expected quoted value") - } - currentCondition.Operand2 = quotedValue - currentCondition.Operand2IsField = false - } - p.query.Conditions[len(p.query.Conditions)-1] = currentCondition - p.pop() - p.step = stepWhereAnd - case stepWhereAnd: - andRWord := p.peek() - if strings.ToUpper(andRWord) != "AND" { - return p.query, fmt.Errorf("expected AND") - } - p.pop() - p.step = stepWhereField case stepInsertFieldsOpeningParens: - openingParens := p.peek() + openingParens := p.peek(false) if len(openingParens) != 1 || openingParens != "(" { - return p.query, fmt.Errorf("at INSERT INTO: expected opening parens") + return p.query, newError(p.i, "at INSERT INTO: expected opening parens") } p.pop() p.step = stepInsertFields case stepInsertFields: - identifier := p.peek() - if !isIdentifier(identifier) { - return p.query, fmt.Errorf("at INSERT INTO: expected at least one field to insert") + identifier := p.peek(false) + if isId, _ := isIdentifier(identifier); !isId { + return p.query, newError(p.i, "at INSERT INTO: expected at least one field to insert") } p.query.Fields = append(p.query.Fields, identifier) p.pop() p.step = stepInsertFieldsCommaOrClosingParens case stepInsertFieldsCommaOrClosingParens: - commaOrClosingParens := p.peek() + commaOrClosingParens := p.peek(false) if commaOrClosingParens != "," && commaOrClosingParens != ")" { - return p.query, fmt.Errorf("at INSERT INTO: expected comma or closing parens") + return p.query, newError(p.i, "at INSERT INTO: expected comma or closing parens") } p.pop() if commaOrClosingParens == "," { @@ -303,32 +306,32 @@ func (p *parser) doParse() (query.Query, error) { } p.step = stepInsertValuesRWord case stepInsertValuesRWord: - valuesRWord := p.peek() - if strings.ToUpper(valuesRWord) != "VALUES" { - return p.query, fmt.Errorf("at INSERT INTO: expected 'VALUES'") + valuesRWord := p.peek(true) + if valuesRWord != "VALUES" { + return p.query, newError(p.i, "at INSERT INTO: expected 'VALUES'") } p.pop() p.step = stepInsertValuesOpeningParens case stepInsertValuesOpeningParens: - openingParens := p.peek() + openingParens := p.peek(false) if openingParens != "(" { - return p.query, fmt.Errorf("at INSERT INTO: expected opening parens") + return p.query, newError(p.i, "at INSERT INTO: expected opening parens") } p.query.Inserts = append(p.query.Inserts, []string{}) p.pop() p.step = stepInsertValues case stepInsertValues: - quotedValue, ln := p.peekQuotedStringWithLength() - if ln == 0 { - return p.query, fmt.Errorf("at INSERT INTO: expected quoted value") + quotedValue := p.peekQuotedString(false) + if p.len == 0 { + return p.query, newError(p.i, "at INSERT INTO: expected quoted value") } p.query.Inserts[len(p.query.Inserts)-1] = append(p.query.Inserts[len(p.query.Inserts)-1], quotedValue) p.pop() p.step = stepInsertValuesCommaOrClosingParens case stepInsertValuesCommaOrClosingParens: - commaOrClosingParens := p.peek() + commaOrClosingParens := p.peek(false) if commaOrClosingParens != "," && commaOrClosingParens != ")" { - return p.query, fmt.Errorf("at INSERT INTO: expected comma or closing parens") + return p.query, newError(p.i, "at INSERT INTO: expected comma or closing parens") } p.pop() if commaOrClosingParens == "," { @@ -337,13 +340,13 @@ func (p *parser) doParse() (query.Query, error) { } currentInsertRow := p.query.Inserts[len(p.query.Inserts)-1] if len(currentInsertRow) < len(p.query.Fields) { - return p.query, fmt.Errorf("at INSERT INTO: value count doesn't match field count") + return p.query, newError(p.i, "at INSERT INTO: value count doesn't match field count") } p.step = stepInsertValuesCommaBeforeOpeningParens case stepInsertValuesCommaBeforeOpeningParens: - commaRWord := p.peek() - if strings.ToUpper(commaRWord) != "," { - return p.query, fmt.Errorf("at INSERT INTO: expected comma") + commaRWord := p.peek(false) + if commaRWord != "," { + return p.query, newError(p.i, "at INSERT INTO: expected comma") } p.pop() p.step = stepInsertValuesOpeningParens @@ -351,123 +354,365 @@ func (p *parser) doParse() (query.Query, error) { } } -func (p *parser) peek() string { - peeked, _ := p.peekWithLength() - return peeked +func (p *parser) parseWhere() (bool, error) { + for { + if p.i >= len(p.sql) { + if len(p.query.Conditions) == 0 { + return true, newError(p.i, "at WHERE: empty WHERE clause") + } + // TODO detect closed + + return true, nil + } + switch p.step { + case stepWhereField: + identifier := p.peek(false) + if p.peekQuoted { + p.query.Conditions = append(p.query.Conditions, query.Condition{Operand1: identifier, Operand1Type: query.OpQuoted}) + } else { + if len(identifier) == 0 { + return false, newError(p.i, "at WHERE: empty WHERE clause") + } else if isId, _ := isIdentifier(identifier); !isId { + if len(p.query.Conditions) == 0 { + return true, newError(p.i, "at WHERE: expected field") + } + // TODO detect closed + + return true, nil + } + p.query.Conditions = append(p.query.Conditions, query.Condition{Operand1: identifier, Operand1Type: query.OpField}) + } + p.pop() + p.step = stepWhereOperator + case stepWhereOperator: + operatorStr := p.peek(false) + currentCondition := p.query.Conditions[len(p.query.Conditions)-1] + operator, _ := reservedWords[operatorStr] + switch operator { + case rEQ: + currentCondition.Operator = query.Eq + case rGT: + currentCondition.Operator = query.Gt + case rGTE: + currentCondition.Operator = query.Gte + case rLT: + currentCondition.Operator = query.Lt + case rLTE: + currentCondition.Operator = query.Lte + case rNE: + currentCondition.Operator = query.Ne + default: + return false, newError(p.i, "at WHERE: unknown operator") + } + p.query.Conditions[len(p.query.Conditions)-1] = currentCondition + p.pop() + p.step = stepWhereValue + case stepWhereValue: + currentCondition := p.query.Conditions[len(p.query.Conditions)-1] + identifier := p.peek(false) + if p.peekQuoted { + currentCondition.Operand2 = identifier + currentCondition.Operand2Type = query.OpQuoted + } else { + if isIdentifier, isNumber := isIdentifier(identifier); isIdentifier { + currentCondition.Operand2 = identifier + currentCondition.Operand2Type = query.OpField + } else if isNumber { + currentCondition.Operand2 = identifier + currentCondition.Operand2Type = query.OpNumber + } else { + return false, newError(p.i, "at WHERE: expected quoted value") + } + } + p.query.Conditions[len(p.query.Conditions)-1] = currentCondition + p.pop() + p.step = stepWhereAnd + case stepWhereAnd: + andRWord := p.peek(true) + if andRWord != "AND" { + return false, newError(p.i, "expected AND") + } + p.pop() + p.step = stepWhereField + default: + // TODO detect closed + + return false, nil + } + } +} + +func (p *parser) peekCurrent(upper bool) string { + if upper { + return p.sqlUpper[p.i : p.i+p.len] + } else { + return p.sql[p.i : p.i+p.len] + } +} + +func (p *parser) peek(upper bool) string { + p.peeked, p.len = p.peekWithLength(upper) + return p.peeked +} + +func (p *parser) peekQuotedString(upper bool) string { + p.peeked, p.len = p.peekQuotedStringWithLength(upper) + return p.peeked +} + +func (p *parser) peekIdentifier(upper bool) string { + p.peeked, p.len = p.peekQuotedStringWithLength(upper) + return p.peeked } func (p *parser) pop() string { - peeked, len := p.peekWithLength() - p.i += len + peeked := p.peeked + p.peeked = "" + p.i += p.len + p.len = 0 + p.peekQuoted = false p.popWhitespace() return peeked } +func (p *parser) popWithLength(len int) { + p.i += len + p.popWhitespace() +} + func (p *parser) popWhitespace() { for ; p.i < len(p.sql) && p.sql[p.i] == ' '; p.i++ { } } -var reservedWords = []string{ - "(", ")", ">=", "<=", "!=", ",", "=", ">", "<", "SELECT", "INSERT INTO", "VALUES", "UPDATE", "DELETE FROM", - "WHERE", "FROM", "SET", -} +type rWord int + +const ( + rUnknown rWord = iota + // reserwed words + rLeftBracket // "("" + rRightBracket // ")" + rGT // ">" + rGTE // ">=" + rLTE // "<=" + rLT // "<" + rEQ // "=" + rNE // "!=" + rCOMMA // "," + rSEMI //";" + rEX // "!" + rAS // "AS" + rSELECT // "SELECT" + rINSERT // "INSERT" + rINTO //"INTO" + rVALUES // "VALUES" + rUPDATE // "UPDATE" + rDELETE // "DELETE" + rWHERE // "WHERE" + rFROM // "FROM" + rSET // "SET" + r +) + +var ( + reservedSymbols = map[byte]rWord{ + '(': rLeftBracket, + ')': rRightBracket, + '>': rGT, + '<': rLT, + '=': rEQ, + '!': rEX, + ',': rCOMMA, + ';': rSEMI, + } + + reservedWords = map[string]rWord{ + "(": rLeftBracket, + ")": rRightBracket, + ">": rGT, + ">=": rGTE, + "<": rLT, + "<=": rLTE, + "=": rEQ, + "!=": rNE, + ",": rCOMMA, + ";": rSEMI, + "AS": rAS, + "SELECT": rSELECT, + "INSERT": rINSERT, + "INTO": rINTO, + "VALUES": rVALUES, + "UPDATE": rUPDATE, + "DELETE": rDELETE, + "FROM": rFROM, + "WHERE": rWHERE, + "SET": rSET, + } +) -func (p *parser) peekWithLength() (string, int) { +func (p *parser) peekWithLength(upper bool) (string, int) { if p.i >= len(p.sql) { return "", 0 } - for _, rWord := range reservedWords { - token := strings.ToUpper(p.sql[p.i:min(len(p.sql), p.i+len(rWord))]) - if token == rWord { - return token, len(token) - } - } + if p.sql[p.i] == '\'' { // Quoted string - return p.peekQuotedStringWithLength() + return p.peekQuotedStringWithLength(upper) } - return p.peekIdentifierWithLength() + + // for _, rWord := range reservedWords { + // token := p.sqlUpper[p.i:min(len(p.sqlUpper), p.i+len(rWord))] + // if token == rWord { + // if !upper { + // token = p.sql[p.i:min(len(p.sql), p.i+len(rWord))] + // } + + // return token, len(token) + // } + // } + + return p.peekIdentifierWithLength(upper) } -func (p *parser) peekQuotedStringWithLength() (string, int) { - if len(p.sql) < p.i || p.sql[p.i] != '\'' { - return "", 0 - } +func (p *parser) peekQuotedStringWithLength(upper bool) (string, int) { + p.peekQuoted = true for i := p.i + 1; i < len(p.sql); i++ { if p.sql[i] == '\'' && p.sql[i-1] != '\\' { + if upper { + return p.sqlUpper[p.i+1 : i], len(p.sqlUpper[p.i+1:i]) + 2 // +2 for the two quotes + } return p.sql[p.i+1 : i], len(p.sql[p.i+1:i]) + 2 // +2 for the two quotes } } return "", 0 } -func (p *parser) peekIdentifierWithLength() (string, int) { - for i := p.i; i < len(p.sql); i++ { - if matched, _ := regexp.MatchString(`[a-zA-Z0-9_*]`, string(p.sql[i])); !matched { +func (p *parser) peekIdentifierWithLength(upper bool) (string, int) { + i := p.i + if _, ok := reservedSymbols[p.sqlUpper[i]]; ok { + if p.sql[i] == '(' || p.sql[i] == ')' { + i++ + } else { + for i = p.i + 1; i < len(p.sql); i++ { + if _, ok := reservedSymbols[p.sqlUpper[i]]; !ok { + return p.sql[p.i:i], len(p.sql[p.i:i]) + } else if p.sql[i] == '(' || p.sql[i] == ')' { + break + } + } + } + return p.sql[p.i:i], len(p.sql[p.i:i]) + } + + for ; i < len(p.sql); i++ { + isIdentifierSymbol := (p.sql[i] >= 'a' && p.sql[i] <= 'z') || + (p.sql[i] >= 'A' && p.sql[i] <= 'Z') || + (p.sql[i] >= '0' && p.sql[i] <= '9') || + p.sql[i] == '*' || + p.sql[i] == '_' || + p.sql[i] == '-' || + p.sql[i] == '.' + if !isIdentifierSymbol { + if p.sql[i] == '(' { + // detect function + if end := strings.IndexByte(p.sql[i+1:], ')'); end >= 0 { + i += end + 2 + } + } + if upper { + return p.sqlUpper[p.i:i], len(p.sqlUpper[p.i:i]) + } return p.sql[p.i:i], len(p.sql[p.i:i]) } } + if upper { + return p.sqlUpper[p.i:], len(p.sqlUpper[p.i:]) + } return p.sql[p.i:], len(p.sql[p.i:]) } func (p *parser) validate() error { if len(p.query.Conditions) == 0 && p.step == stepWhereField { - return fmt.Errorf("at WHERE: empty WHERE clause") + return newError(p.i, "at WHERE: empty WHERE clause") } if p.query.Type == query.UnknownType { - return fmt.Errorf("query type cannot be empty") + return newError(p.i, "query type cannot be empty") } - if p.query.TableName == "" { - return fmt.Errorf("table name cannot be empty") + if (p.query.Type != query.Select || len(p.query.Fields) == 0) && p.query.TableName == "" { + return newError(p.i, "table name cannot be empty") } if len(p.query.Conditions) == 0 && (p.query.Type == query.Update || p.query.Type == query.Delete) { - return fmt.Errorf("at WHERE: WHERE clause is mandatory for UPDATE & DELETE") + return newError(p.i, "at WHERE: WHERE clause is mandatory for UPDATE & DELETE") } for _, c := range p.query.Conditions { if c.Operator == query.UnknownOperator { - return fmt.Errorf("at WHERE: condition without operator") + return newError(p.i, "at WHERE: condition without operator") } - if c.Operand1 == "" && c.Operand1IsField { - return fmt.Errorf("at WHERE: condition with empty left side operand") + if c.Operand1 == "" && c.Operand1Type == query.OpField { + return newError(p.i, "at WHERE: condition with empty left side operand") } - if c.Operand2 == "" && c.Operand2IsField { - return fmt.Errorf("at WHERE: condition with empty right side operand") + if c.Operand2 == "" && c.Operand2Type == query.OpField { + return newError(p.i, "at WHERE: condition with empty right side operand") } } if p.query.Type == query.Insert && len(p.query.Inserts) == 0 { - return fmt.Errorf("at INSERT INTO: need at least one row to insert") + return newError(p.i, "at INSERT INTO: need at least one row to insert") } if p.query.Type == query.Insert { for _, i := range p.query.Inserts { if len(i) != len(p.query.Fields) { - return fmt.Errorf("at INSERT INTO: value count doesn't match field count") + return newError(p.i, "at INSERT INTO: value count doesn't match field count") } } } + if p.query.Type == query.Select && len(p.query.Fields) != len(p.query.Aliases) { + return newError(p.i, "fileds and aliases count mismatch") + } return nil } -func (p *parser) logError() { - if p.err == nil { - return +func isIdentifier(s string) (bool, bool) { + if len(s) == 0 { + return false, false } - fmt.Println(p.sql) - fmt.Println(strings.Repeat(" ", p.i) + "^") - fmt.Println(p.err) -} + u := strings.ToUpper(s) -func isIdentifier(s string) bool { - for _, rw := range reservedWords { - if strings.ToUpper(s) == rw { - return false + if _, ok := reservedWords[u]; ok { + return false, false + } + + if s[0] == '-' || (s[0] >= '0' && s[0] <= '9') { + for i := 1; i < len(s); i++ { + allowedSymbol := (s[i] >= '0' && s[i] <= '9') || s[i] == '.' + if !allowedSymbol { + return false, false + } } + return false, true + } else if (s[0] >= 'a' && s[0] <= 'z') || + (s[0] >= 'A' && s[0] <= 'Z') || + s[0] == '_' { + for i := 1; i < len(s); i++ { + isIdentifierSymbol := (s[i] >= 'a' && s[i] <= 'z') || + (s[i] >= 'A' && s[i] <= 'Z') || + (s[i] >= '0' && s[i] <= '9') || + s[i] == '_' + if !isIdentifierSymbol { + if s[i] == '(' && s[len(s)-1] == ')' { + return true, false + } + return false, false + } + } + return true, false } - matched, _ := regexp.MatchString("[a-zA-Z_][a-zA-Z_0-9]*", s) - return matched + return false, false } -func isIdentifierOrAsterisk(s string) bool { - return isIdentifier(s) || s == "*" +func isIdentifierOrAsterisk(s string) (bool, bool) { + if s == "*" { + return true, false + } + return isIdentifier(s) } func min(a, b int) int { diff --git a/sql_test.go b/sql_test.go index f15cde3..685c767 100644 --- a/sql_test.go +++ b/sql_test.go @@ -5,10 +5,11 @@ import ( "io/ioutil" "log" "os" + "strings" "testing" "text/template" - "github.com/marianogappa/sqlparser/query" + "github.com/msaf1980/sqlparser/query" "github.com/stretchr/testify/require" ) @@ -17,6 +18,7 @@ type testCase struct { SQL string Expected query.Query Err error + Ended bool } type output struct { @@ -52,34 +54,58 @@ func TestSQL(t *testing.T) { Expected: query.Query{Type: query.Select}, Err: fmt.Errorf("at SELECT: expected field to SELECT"), }, + { + Name: "SELECT with incomplete alias fails", + SQL: "SELECT a AS", + Expected: query.Query{Type: query.Select}, + Err: fmt.Errorf("at AS: expected alias for a"), + }, + { + Name: "SELECT version() as version", + SQL: "SELECT version() as version", + Expected: query.Query{Type: query.Select, Fields: []string{"version()"}, Aliases: []string{"version"}}, + Err: nil, + }, { Name: "SELECT works", SQL: "SELECT a FROM 'b'", - Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a"}}, + Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a"}, Aliases: []string{""}}, + Err: nil, + }, + { + Name: "SELECT with alias works", + SQL: "SELECT a AS text FROM 'b'", + Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a"}, Aliases: []string{"text"}}, + Err: nil, + }, + { + Name: "SELECT with alias works", + SQL: "SELECT version(a) AS version FROM 'b'", + Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"version(a)"}, Aliases: []string{"version"}}, Err: nil, }, { Name: "SELECT works with lowercase", SQL: "select a fRoM 'b'", - Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a"}}, + Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a"}, Aliases: []string{""}}, Err: nil, }, { Name: "SELECT many fields works", SQL: "SELECT a, c, d FROM 'b'", - Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a", "c", "d"}}, + Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}}, Err: nil, }, { Name: "SELECT with empty WHERE fails", SQL: "SELECT a, c, d FROM 'b' WHERE", - Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a", "c", "d"}}, + Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}}, Err: fmt.Errorf("at WHERE: empty WHERE clause"), }, { Name: "SELECT with WHERE with only operand fails", SQL: "SELECT a, c, d FROM 'b' WHERE a", - Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a", "c", "d"}}, + Expected: query.Query{Type: query.Select, TableName: "b", Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}}, Err: fmt.Errorf("at WHERE: condition without operator"), }, { @@ -88,9 +114,9 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Eq, Operand2: "", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -101,9 +127,9 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Lt, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Lt, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -114,9 +140,9 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Lte, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Lte, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -127,9 +153,9 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Gt, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Gt, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -140,9 +166,9 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Gte, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Gte, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -153,9 +179,9 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Ne, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Ne, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -166,9 +192,9 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Ne, Operand2: "b", Operand2IsField: true}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Ne, Operand2: "b", Operand2Type: query.OpField}, }, }, Err: nil, @@ -180,6 +206,7 @@ func TestSQL(t *testing.T) { Type: query.Select, TableName: "b", Fields: []string{"*"}, + Aliases: []string{""}, Conditions: nil, }, Err: nil, @@ -188,9 +215,9 @@ func TestSQL(t *testing.T) { Name: "SELECT a, * works", SQL: "SELECT a, * FROM 'b'", Expected: query.Query{ - Type: query.Select, - TableName: "b", - Fields: []string{"a", "*"}, + Type: query.Select, + TableName: "b", + Fields: []string{"a", "*"}, Aliases: []string{"", ""}, Conditions: nil, }, Err: nil, @@ -201,10 +228,10 @@ func TestSQL(t *testing.T) { Expected: query.Query{ Type: query.Select, TableName: "b", - Fields: []string{"a", "c", "d"}, + Fields: []string{"a", "c", "d"}, Aliases: []string{"", "", ""}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Ne, Operand2: "1", Operand2IsField: false}, - {Operand1: "b", Operand1IsField: true, Operator: query.Eq, Operand2: "2", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Ne, Operand2: "1", Operand2Type: query.OpQuoted}, + {Operand1: "b", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "2", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -259,7 +286,7 @@ func TestSQL(t *testing.T) { TableName: "a", Updates: map[string]string{"b": "hello"}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Eq, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -272,7 +299,7 @@ func TestSQL(t *testing.T) { TableName: "a", Updates: map[string]string{"b": "hello\\'world"}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Eq, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -285,7 +312,7 @@ func TestSQL(t *testing.T) { TableName: "a", Updates: map[string]string{"b": "hello", "c": "bye"}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Eq, Operand2: "1", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -298,8 +325,8 @@ func TestSQL(t *testing.T) { TableName: "a", Updates: map[string]string{"b": "hello", "c": "bye"}, Conditions: []query.Condition{ - {Operand1: "a", Operand1IsField: true, Operator: query.Eq, Operand2: "1", Operand2IsField: false}, - {Operand1: "b", Operand1IsField: true, Operator: query.Eq, Operand2: "789", Operand2IsField: false}, + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "1", Operand2Type: query.OpQuoted}, + {Operand1: "b", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "789", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -335,7 +362,7 @@ func TestSQL(t *testing.T) { Type: query.Delete, TableName: "a", Conditions: []query.Condition{ - {Operand1: "b", Operand1IsField: true, Operator: query.Eq, Operand2: "1", Operand2IsField: false}, + {Operand1: "b", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "1", Operand2Type: query.OpQuoted}, }, }, Err: nil, @@ -394,10 +421,15 @@ func TestSQL(t *testing.T) { Err: nil, }, { - Name: "INSERT * fails", - SQL: "INSERT INTO 'a' (*) VALUES ('1')", - Expected: query.Query{}, - Err: fmt.Errorf("at INSERT INTO: expected at least one field to insert"), + Name: "INSERT * fails", + SQL: "INSERT INTO 'a' (*) VALUES ('1')", + Expected: query.Query{ + Type: query.Insert, + TableName: "a", + Fields: []string{"*"}, + Inserts: [][]string{{"1"}}, + }, + Err: fmt.Errorf("at INSERT INTO: expected at least one field to insert"), }, { Name: "INSERT with multiple fields works", @@ -427,6 +459,12 @@ func TestSQL(t *testing.T) { for _, tc := range ts { t.Run(tc.Name, func(t *testing.T) { actual, err := ParseMany([]string{tc.SQL}) + if err != nil { + if errPos, ok := err.(*ErrorWithPos); ok { + fmt.Fprintln(os.Stderr, "") + errPos.PrintPosError(tc.SQL, os.Stderr) + } + } if tc.Err != nil && err == nil { t.Errorf("Error should have been %v", tc.Err) } @@ -434,7 +472,7 @@ func TestSQL(t *testing.T) { t.Errorf("Error should have been nil but was %v", err) } if tc.Err != nil && err != nil { - require.Equal(t, tc.Err, err, "Unexpected error") + require.Equal(t, tc.Err.Error(), err.Error(), "Unexpected error") } if len(actual) > 0 { require.Equal(t, tc.Expected, actual[0], "Query didn't match expectation") @@ -449,6 +487,147 @@ func TestSQL(t *testing.T) { createReadme(output) } +func TestWhere(t *testing.T) { + ts := []testCase{ + { + Name: "empty query fails", + SQL: "", + Expected: query.Query{}, + Err: fmt.Errorf("at WHERE: empty WHERE clause"), + Ended: true, + }, + { + Name: "WHERE a", + SQL: "a ", + Expected: query.Query{ + Conditions: []query.Condition{ + {Operand1: "a", Operand1Type: query.OpField, Operator: query.UnknownOperator, Operand2: "", Operand2Type: query.OpUnknown}, + }, + }, + Err: nil, + Ended: true, + }, + { + Name: "WHERE a = ''", + SQL: "a = ''", + Expected: query.Query{ + Conditions: []query.Condition{ + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "", Operand2Type: query.OpQuoted}, + }, + }, + Err: nil, + Ended: true, + }, + { + Name: "WHERE a = 1", + SQL: "a>=1", + Expected: query.Query{ + Conditions: []query.Condition{ + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Gte, Operand2: "1", Operand2Type: query.OpNumber}, + }, + }, + Err: nil, + Ended: true, + }, + { + Name: "WHERE a = 1.24", + SQL: "a>= 1.24", + Expected: query.Query{ + Conditions: []query.Condition{ + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Gte, Operand2: "1.24", Operand2Type: query.OpNumber}, + }, + }, + Err: nil, + Ended: true, + }, + { + Name: "WHERE a = -1.21", + SQL: "a>=-1.21", + Expected: query.Query{ + Conditions: []query.Condition{ + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Gte, Operand2: "-1.21", Operand2Type: query.OpNumber}, + }, + }, + Err: nil, + Ended: true, + }, + { + Name: "WHERE a = 1 AND b > a1", + SQL: "a = 1 AND b > a1", + Expected: query.Query{ + Conditions: []query.Condition{ + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "1", Operand2Type: query.OpNumber}, + {Operand1: "b", Operand1Type: query.OpField, Operator: query.Gt, Operand2: "a1", Operand2Type: query.OpField}, + }, + }, + Err: nil, + Ended: true, + }, + { + Name: "ERROR (a1) WHERE a = 1a", + SQL: "a = 1a", + Expected: query.Query{ + Conditions: []query.Condition{ + {Operand1: "a", Operand1Type: query.OpField, Operator: query.Eq, Operand2: "", Operand2Type: query.OpUnknown}, + }, + }, + Err: fmt.Errorf("at WHERE: expected quoted value"), + Ended: false, + }, + } + + for _, tc := range ts { + t.Run(tc.Name, func(t *testing.T) { + var p parser + // init parser internals + p.step = stepWhereField + p.sql = tc.SQL + p.sqlUpper = strings.ToUpper(tc.SQL) + + ended, err := p.parseWhere() + if err != nil { + if errPos, ok := err.(*ErrorWithPos); ok { + fmt.Fprintln(os.Stderr, "") + errPos.PrintPosError(tc.SQL, os.Stderr) + } + } + if tc.Err != nil && err == nil { + t.Errorf("Error should have been %v", tc.Err) + } + if tc.Err == nil && err != nil { + t.Errorf("Error should have been nil but was %v", err) + } + if tc.Ended != ended { + t.Errorf("End not detected") + } + if tc.Err != nil && err != nil { + require.Equal(t, tc.Err.Error(), err.Error(), "Unexpected error") + } + require.Equal(t, tc.Expected, p.query, "Query didn't match expectation") + }) + } +} + +func BenchmarkSQLSelect(b *testing.B) { + sql := "SELECT a AS text FROM 'b' WHERE c = 'c' AND d = 'd'" + for i := 0; i < b.N; i++ { + q, err := Parse(sql) + if err != nil { + b.Errorf("Error should have been %v: %v", err, q) + } + } +} + +func BenchmarkSQLInsert(b *testing.B) { + sql := "INSERT INTO 'a' (b,c, d) VALUES ('1','2' , '3' )" + for i := 0; i < b.N; i++ { + q, err := Parse(sql) + if err != nil { + b.Errorf("Error should have been %v: %v", err, q) + } + } +} + func createReadme(out output) { content, err := ioutil.ReadFile("README.template") if err != nil {